Skip to content

Commit 7dc20eb

Browse files
committed
feat: add coderd_organization data source
1 parent 393c5a9 commit 7dc20eb

File tree

6 files changed

+361
-3
lines changed

6 files changed

+361
-3
lines changed

docs/data-sources/organization.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "coderd_organization Data Source - coderd"
4+
subcategory: ""
5+
description: |-
6+
An existing organization on the coder deployment.
7+
---
8+
9+
# coderd_organization (Data Source)
10+
11+
An existing organization on the coder deployment.
12+
13+
14+
15+
<!-- schema generated by tfplugindocs -->
16+
## Schema
17+
18+
### Optional
19+
20+
- `id` (String) The ID of the organization to retrieve. This field will be populated if the organization is found by name, or if the default organization is requested.
21+
- `is_default` (Boolean) Whether the organization is the default organization of the deployment. This field will be populated if the organization is found by ID or name.
22+
- `name` (String) The name of the organization to retrieve. This field will be populated if the organization is found by ID, or if the default organization is requested.
23+
24+
### Read-Only
25+
26+
- `created_at` (Number) Unix timestamp when the organization was created.
27+
- `members` (Set of String) Members of the organization, by ID
28+
- `updated_at` (Number) Unix timestamp when the organization was last updated.

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ toolchain go1.22.5
66

77
require (
88
cdr.dev/slog v1.6.2-0.20240126064726-20367d4aede6
9-
github.com/coder/coder/v2 v2.12.3
9+
github.com/coder/coder/v2 v2.13.0
1010
github.com/docker/docker v27.0.3+incompatible
1111
github.com/docker/go-connections v0.4.0
1212
github.com/google/uuid v1.6.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo
8181
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
8282
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
8383
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
84-
github.com/coder/coder/v2 v2.12.3 h1:tA+0lWIO7xXJ4guu+tqcram/6kKKX1pWd1WlipdhIpc=
85-
github.com/coder/coder/v2 v2.12.3/go.mod h1:io26dngPVP3a7zD1lL/bzEOGDSincJGomBKlqmRRVNA=
84+
github.com/coder/coder/v2 v2.13.0 h1:MlkRGqQcCAdwIkLc9iV8sQfT4jB3EThHopG0jF3BuFE=
85+
github.com/coder/coder/v2 v2.13.0/go.mod h1:Gxc79InMB6b+sncuDUORtFLWi7aKshvis3QrMUhpq5Q=
8686
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs=
8787
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc=
8888
github.com/coder/serpent v0.7.0 h1:zGpD2GlF3lKIVkMjNGKbkip88qzd5r/TRcc30X/SrT0=
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/coder/coder/v2/codersdk"
8+
"github.com/google/uuid"
9+
"github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator"
10+
"github.com/hashicorp/terraform-plugin-framework/attr"
11+
"github.com/hashicorp/terraform-plugin-framework/datasource"
12+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
13+
"github.com/hashicorp/terraform-plugin-framework/path"
14+
"github.com/hashicorp/terraform-plugin-framework/types"
15+
)
16+
17+
// Ensure provider defined types fully satisfy framework interfaces.
18+
var _ datasource.DataSource = &OrganizationDataSource{}
19+
var _ datasource.DataSourceWithConfigValidators = &OrganizationDataSource{}
20+
21+
func NewOrganizationDataSource() datasource.DataSource {
22+
return &OrganizationDataSource{}
23+
}
24+
25+
// OrganizationDataSource defines the data source implementation.
26+
type OrganizationDataSource struct {
27+
data *CoderdProviderData
28+
}
29+
30+
// OrganizationDataSourceModel describes the data source data model.
31+
type OrganizationDataSourceModel struct {
32+
// Exactly one of ID, IsDefault, or Name must be set.
33+
ID types.String `tfsdk:"id"`
34+
IsDefault types.Bool `tfsdk:"is_default"`
35+
Name types.String `tfsdk:"name"`
36+
37+
CreatedAt types.Int64 `tfsdk:"created_at"`
38+
UpdatedAt types.Int64 `tfsdk:"updated_at"`
39+
Members types.Set `tfsdk:"members"`
40+
}
41+
42+
func (d *OrganizationDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
43+
resp.TypeName = req.ProviderTypeName + "_organization"
44+
}
45+
46+
func (d *OrganizationDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
47+
resp.Schema = schema.Schema{
48+
MarkdownDescription: "An existing organization on the coder deployment.",
49+
50+
Attributes: map[string]schema.Attribute{
51+
"id": schema.StringAttribute{
52+
MarkdownDescription: "The ID of the organization to retrieve. This field will be populated if the organization is found by name, or if the default organization is requested.",
53+
Optional: true,
54+
Computed: true,
55+
},
56+
"is_default": schema.BoolAttribute{
57+
MarkdownDescription: "Whether the organization is the default organization of the deployment. This field will be populated if the organization is found by ID or name.",
58+
Optional: true,
59+
Computed: true,
60+
},
61+
"name": schema.StringAttribute{
62+
MarkdownDescription: "The name of the organization to retrieve. This field will be populated if the organization is found by ID, or if the default organization is requested.",
63+
Optional: true,
64+
Computed: true,
65+
},
66+
"created_at": schema.Int64Attribute{
67+
MarkdownDescription: "Unix timestamp when the organization was created.",
68+
Computed: true,
69+
},
70+
"updated_at": schema.Int64Attribute{
71+
MarkdownDescription: "Unix timestamp when the organization was last updated.",
72+
Computed: true,
73+
},
74+
75+
"members": schema.SetAttribute{
76+
MarkdownDescription: "Members of the organization, by ID",
77+
Computed: true,
78+
ElementType: types.StringType,
79+
},
80+
},
81+
}
82+
}
83+
84+
func (d *OrganizationDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
85+
// Prevent panic if the provider has not been configured.
86+
if req.ProviderData == nil {
87+
return
88+
}
89+
90+
data, ok := req.ProviderData.(*CoderdProviderData)
91+
92+
if !ok {
93+
resp.Diagnostics.AddError(
94+
"Unexpected Data Source Configure Type",
95+
fmt.Sprintf("Expected *CoderdProviderData, got: %T. Please report this issue to the provider developers.", req.ProviderData),
96+
)
97+
98+
return
99+
}
100+
101+
d.data = data
102+
}
103+
104+
func (d *OrganizationDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
105+
var data OrganizationDataSourceModel
106+
107+
// Read Terraform configuration data into the model
108+
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
109+
110+
if resp.Diagnostics.HasError() {
111+
return
112+
}
113+
114+
client := d.data.Client
115+
116+
var org codersdk.Organization
117+
if !data.ID.IsNull() { // By ID
118+
orgID, err := uuid.Parse(data.ID.ValueString())
119+
if err != nil {
120+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to parse supplied ID as UUID, got error: %s", err))
121+
return
122+
}
123+
org, err = client.Organization(ctx, orgID)
124+
if err != nil {
125+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization by ID, got error: %s", err))
126+
return
127+
}
128+
if org.ID.String() != data.ID.ValueString() {
129+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Organization ID %s does not match requested ID %s", org.ID, data.ID))
130+
return
131+
}
132+
} else if data.IsDefault.ValueBool() { // Get Default
133+
var err error
134+
org, err = client.OrganizationByName(ctx, "default")
135+
if err != nil {
136+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get default organization, got error: %s", err))
137+
return
138+
}
139+
if !org.IsDefault {
140+
resp.Diagnostics.AddError("Client Error", "Found organization was not the default organization")
141+
return
142+
}
143+
} else { // By Name
144+
var err error
145+
org, err = client.OrganizationByName(ctx, data.Name.ValueString())
146+
if err != nil {
147+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization by name, got error: %s", err))
148+
return
149+
}
150+
if org.Name != data.Name.ValueString() {
151+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Organization name %s does not match requested name %s", org.Name, data.Name))
152+
return
153+
}
154+
}
155+
data.ID = types.StringValue(org.ID.String())
156+
data.Name = types.StringValue(org.Name)
157+
data.IsDefault = types.BoolValue(org.IsDefault)
158+
data.CreatedAt = types.Int64Value(org.CreatedAt.Unix())
159+
data.UpdatedAt = types.Int64Value(org.UpdatedAt.Unix())
160+
members, err := client.OrganizationMembers(ctx, org.ID)
161+
if err != nil {
162+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization members, got error: %s", err))
163+
return
164+
}
165+
memberIDs := make([]attr.Value, 0, len(members))
166+
for _, member := range members {
167+
memberIDs = append(memberIDs, types.StringValue(member.UserID.String()))
168+
}
169+
data.Members = types.SetValueMust(types.StringType, memberIDs)
170+
171+
// Save data into Terraform state
172+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
173+
}
174+
175+
func (d *OrganizationDataSource) ConfigValidators(_ context.Context) []datasource.ConfigValidator {
176+
return []datasource.ConfigValidator{
177+
datasourcevalidator.ExactlyOneOf(
178+
path.MatchRoot("id"),
179+
path.MatchRoot("is_default"),
180+
path.MatchRoot("name"),
181+
),
182+
}
183+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
"os"
6+
"regexp"
7+
"strings"
8+
"testing"
9+
"text/template"
10+
11+
"github.com/coder/coder/v2/codersdk"
12+
"github.com/coder/terraform-provider-coderd/integration"
13+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func TestAccOrganizationDataSource(t *testing.T) {
18+
if os.Getenv("TF_ACC") == "" {
19+
t.Skip("Acceptance tests are disabled.")
20+
}
21+
ctx := context.Background()
22+
client := integration.StartCoder(ctx, t, "group_acc")
23+
firstUser, err := client.User(ctx, codersdk.Me)
24+
require.NoError(t, err)
25+
26+
defaultCheckFn := resource.ComposeAggregateTestCheckFunc(
27+
resource.TestCheckResourceAttr("data.coderd_organization.test", "id", firstUser.OrganizationIDs[0].String()),
28+
resource.TestCheckResourceAttr("data.coderd_organization.test", "is_default", "true"),
29+
resource.TestCheckResourceAttr("data.coderd_organization.test", "name", "first-organization"),
30+
resource.TestCheckResourceAttr("data.coderd_organization.test", "members.#", "1"),
31+
resource.TestCheckTypeSetElemAttr("data.coderd_organization.test", "members.*", firstUser.ID.String()),
32+
resource.TestCheckResourceAttrSet("data.coderd_organization.test", "created_at"),
33+
resource.TestCheckResourceAttrSet("data.coderd_organization.test", "updated_at"),
34+
)
35+
36+
t.Run("DefaultOrgByIDOk", func(t *testing.T) {
37+
cfg := testAccOrganizationDataSourceConfig{
38+
URL: client.URL.String(),
39+
Token: client.SessionToken(),
40+
ID: PtrTo(firstUser.OrganizationIDs[0].String()),
41+
}
42+
resource.Test(t, resource.TestCase{
43+
PreCheck: func() { testAccPreCheck(t) },
44+
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
45+
Steps: []resource.TestStep{
46+
{
47+
Config: cfg.String(t),
48+
Check: defaultCheckFn,
49+
},
50+
},
51+
})
52+
})
53+
54+
t.Run("DefaultOrgByNameOk", func(t *testing.T) {
55+
cfg := testAccOrganizationDataSourceConfig{
56+
URL: client.URL.String(),
57+
Token: client.SessionToken(),
58+
Name: PtrTo("first-organization"),
59+
}
60+
resource.Test(t, resource.TestCase{
61+
PreCheck: func() { testAccPreCheck(t) },
62+
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
63+
Steps: []resource.TestStep{
64+
{
65+
Config: cfg.String(t),
66+
Check: defaultCheckFn,
67+
},
68+
},
69+
})
70+
})
71+
72+
t.Run("DefaultOrgByIsDefaultOk", func(t *testing.T) {
73+
cfg := testAccOrganizationDataSourceConfig{
74+
URL: client.URL.String(),
75+
Token: client.SessionToken(),
76+
IsDefault: PtrTo(true),
77+
}
78+
resource.Test(t, resource.TestCase{
79+
PreCheck: func() { testAccPreCheck(t) },
80+
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
81+
Steps: []resource.TestStep{
82+
{
83+
Config: cfg.String(t),
84+
Check: defaultCheckFn,
85+
},
86+
},
87+
})
88+
})
89+
90+
t.Run("InvalidAttributesError", func(t *testing.T) {
91+
cfg := testAccOrganizationDataSourceConfig{
92+
URL: client.URL.String(),
93+
Token: client.SessionToken(),
94+
IsDefault: PtrTo(true),
95+
Name: PtrTo("first-organization"),
96+
}
97+
resource.Test(t, resource.TestCase{
98+
PreCheck: func() { testAccPreCheck(t) },
99+
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
100+
Steps: []resource.TestStep{
101+
{
102+
Config: cfg.String(t),
103+
ExpectError: regexp.MustCompile(`Exactly one of these attributes must be configured: \[id,is\_default,name\]`),
104+
},
105+
},
106+
})
107+
})
108+
109+
// TODO: Non-default org tests
110+
}
111+
112+
type testAccOrganizationDataSourceConfig struct {
113+
URL string
114+
Token string
115+
116+
ID *string
117+
Name *string
118+
IsDefault *bool
119+
}
120+
121+
func (c testAccOrganizationDataSourceConfig) String(t *testing.T) string {
122+
tpl := `
123+
provider coderd {
124+
url = "{{.URL}}"
125+
token = "{{.Token}}"
126+
}
127+
128+
data "coderd_organization" "test" {
129+
id = {{orNull .ID}}
130+
name = {{orNull .Name}}
131+
is_default = {{orNull .IsDefault}}
132+
}
133+
`
134+
135+
funcMap := template.FuncMap{
136+
"orNull": PrintOrNull(t),
137+
}
138+
139+
buf := strings.Builder{}
140+
tmpl, err := template.New("groupDataSource").Funcs(funcMap).Parse(tpl)
141+
require.NoError(t, err)
142+
143+
err = tmpl.Execute(&buf, c)
144+
require.NoError(t, err)
145+
return buf.String()
146+
}

internal/provider/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ func (p *CoderdProvider) DataSources(ctx context.Context) []func() datasource.Da
112112
return []func() datasource.DataSource{
113113
NewGroupDataSource,
114114
NewUserDataSource,
115+
NewOrganizationDataSource,
115116
}
116117
}
117118

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy