Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit aa86273

Browse files
committed
feat: add coderd_organization data source
1 parent a00ba07 commit aa86273

File tree

7 files changed

+366
-4
lines changed

7 files changed

+366
-4
lines changed

docs/data-sources/group.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ An existing group on the coder deployment.
1919

2020
- `id` (String) The ID of the group to retrieve. This field will be populated if a name and organization ID is supplied.
2121
- `name` (String) The name of the group to retrieve. This field will be populated if an ID is supplied.
22-
- `organization_id` (String) The organization ID that the group belongs to. This field will be populated if an ID is supplied.
22+
- `organization_id` (String) The organization ID that the group belongs to. This field will be populated if an ID is supplied. Defaults to the provider default organization ID.
2323

2424
### Read-Only
2525

docs/data-sources/organization.md

+28
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

+1-1
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.1
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

+4-2
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,10 @@ 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=
86+
github.com/coder/coder/v2 v2.13.1 h1:tCd8ljqIAufbVcBr8ODS1QbsrjJbmOIvgDkvdd/JMXc=
87+
github.com/coder/coder/v2 v2.13.1/go.mod h1:Gxc79InMB6b+sncuDUORtFLWi7aKshvis3QrMUhpq5Q=
8688
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs=
8789
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc=
8890
github.com/coder/serpent v0.7.0 h1:zGpD2GlF3lKIVkMjNGKbkip88qzd5r/TRcc30X/SrT0=
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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+
// TODO: This could reasonably store some User object - though we may need to make additional queries depending on what fields we
40+
// want, or to have one consistent user type for all data sources.
41+
Members types.Set `tfsdk:"members"`
42+
}
43+
44+
func (d *OrganizationDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
45+
resp.TypeName = req.ProviderTypeName + "_organization"
46+
}
47+
48+
func (d *OrganizationDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
49+
resp.Schema = schema.Schema{
50+
MarkdownDescription: "An existing organization on the coder deployment.",
51+
52+
Attributes: map[string]schema.Attribute{
53+
"id": schema.StringAttribute{
54+
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.",
55+
Optional: true,
56+
Computed: true,
57+
},
58+
"is_default": schema.BoolAttribute{
59+
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.",
60+
Optional: true,
61+
Computed: true,
62+
},
63+
"name": schema.StringAttribute{
64+
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.",
65+
Optional: true,
66+
Computed: true,
67+
},
68+
"created_at": schema.Int64Attribute{
69+
MarkdownDescription: "Unix timestamp when the organization was created.",
70+
Computed: true,
71+
},
72+
"updated_at": schema.Int64Attribute{
73+
MarkdownDescription: "Unix timestamp when the organization was last updated.",
74+
Computed: true,
75+
},
76+
77+
"members": schema.SetAttribute{
78+
MarkdownDescription: "Members of the organization, by ID",
79+
Computed: true,
80+
ElementType: types.StringType,
81+
},
82+
},
83+
}
84+
}
85+
86+
func (d *OrganizationDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
87+
// Prevent panic if the provider has not been configured.
88+
if req.ProviderData == nil {
89+
return
90+
}
91+
92+
data, ok := req.ProviderData.(*CoderdProviderData)
93+
94+
if !ok {
95+
resp.Diagnostics.AddError(
96+
"Unexpected Data Source Configure Type",
97+
fmt.Sprintf("Expected *CoderdProviderData, got: %T. Please report this issue to the provider developers.", req.ProviderData),
98+
)
99+
100+
return
101+
}
102+
103+
d.data = data
104+
}
105+
106+
func (d *OrganizationDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
107+
var data OrganizationDataSourceModel
108+
109+
// Read Terraform configuration data into the model
110+
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
111+
112+
if resp.Diagnostics.HasError() {
113+
return
114+
}
115+
116+
client := d.data.Client
117+
118+
var org codersdk.Organization
119+
if !data.ID.IsNull() { // By ID
120+
orgID, err := uuid.Parse(data.ID.ValueString())
121+
if err != nil {
122+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to parse supplied ID as UUID, got error: %s", err))
123+
return
124+
}
125+
org, err = client.Organization(ctx, orgID)
126+
if err != nil {
127+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization by ID, got error: %s", err))
128+
return
129+
}
130+
if org.ID.String() != data.ID.ValueString() {
131+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Organization ID %s does not match requested ID %s", org.ID, data.ID))
132+
return
133+
}
134+
} else if data.IsDefault.ValueBool() { // Get Default
135+
var err error
136+
org, err = client.OrganizationByName(ctx, "default")
137+
if err != nil {
138+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get default organization, got error: %s", err))
139+
return
140+
}
141+
if !org.IsDefault {
142+
resp.Diagnostics.AddError("Client Error", "Found organization was not the default organization")
143+
return
144+
}
145+
} else { // By Name
146+
var err error
147+
org, err = client.OrganizationByName(ctx, data.Name.ValueString())
148+
if err != nil {
149+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization by name, got error: %s", err))
150+
return
151+
}
152+
if org.Name != data.Name.ValueString() {
153+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Organization name %s does not match requested name %s", org.Name, data.Name))
154+
return
155+
}
156+
}
157+
data.ID = types.StringValue(org.ID.String())
158+
data.Name = types.StringValue(org.Name)
159+
data.IsDefault = types.BoolValue(org.IsDefault)
160+
data.CreatedAt = types.Int64Value(org.CreatedAt.Unix())
161+
data.UpdatedAt = types.Int64Value(org.UpdatedAt.Unix())
162+
members, err := client.OrganizationMembers(ctx, org.ID)
163+
if err != nil {
164+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization members, got error: %s", err))
165+
return
166+
}
167+
memberIDs := make([]attr.Value, 0, len(members))
168+
for _, member := range members {
169+
memberIDs = append(memberIDs, types.StringValue(member.UserID.String()))
170+
}
171+
data.Members = types.SetValueMust(types.StringType, memberIDs)
172+
173+
// Save data into Terraform state
174+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
175+
}
176+
177+
func (d *OrganizationDataSource) ConfigValidators(_ context.Context) []datasource.ConfigValidator {
178+
return []datasource.ConfigValidator{
179+
datasourcevalidator.ExactlyOneOf(
180+
path.MatchRoot("id"),
181+
path.MatchRoot("is_default"),
182+
path.MatchRoot("name"),
183+
),
184+
}
185+
}
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", true)
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,
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+
}

0 commit comments

Comments
 (0)