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

Skip to content

Commit ae0d572

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

File tree

6 files changed

+361
-3
lines changed

6 files changed

+361
-3
lines changed

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

+2-2
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=
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+
}
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+
}

internal/provider/provider.go

+1
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ func (p *CoderdProvider) DataSources(ctx context.Context) []func() datasource.Da
130130
return []func() datasource.DataSource{
131131
NewGroupDataSource,
132132
NewUserDataSource,
133+
NewOrganizationDataSource,
133134
}
134135
}
135136

0 commit comments

Comments
 (0)