Skip to content

Commit 4d7c44c

Browse files
committed
feat: add coderd_organization resource
1 parent 65c854f commit 4d7c44c

File tree

4 files changed

+505
-0
lines changed

4 files changed

+505
-0
lines changed

docs/resources/organization.md

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "coderd_organization Resource - coderd"
4+
subcategory: ""
5+
description: |-
6+
An organization on the coder deployment.
7+
---
8+
9+
# coderd_organization (Resource)
10+
11+
An organization on the coder deployment.
12+
13+
14+
15+
<!-- schema generated by tfplugindocs -->
16+
## Schema
17+
18+
### Required
19+
20+
- `name` (String)
21+
22+
### Optional
23+
24+
- `description` (String)
25+
- `display_name` (String)
26+
- `icon` (String)
27+
- `members` (Set of String) Members of the organization, by ID. If null, members will not be added or removed by Terraform.
28+
29+
### Read-Only
30+
31+
- `id` (String) The ID of this resource.
+310
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package provider
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
"github.com/coder/coder/v2/codersdk"
11+
"github.com/google/uuid"
12+
"github.com/hashicorp/terraform-plugin-framework/attr"
13+
"github.com/hashicorp/terraform-plugin-framework/path"
14+
"github.com/hashicorp/terraform-plugin-framework/resource"
15+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
16+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
17+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
18+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
19+
"github.com/hashicorp/terraform-plugin-framework/types"
20+
"github.com/hashicorp/terraform-plugin-log/tflog"
21+
)
22+
23+
// Ensure provider defined types fully satisfy framework interfaces.
24+
var _ resource.Resource = &OrganizationResource{}
25+
var _ resource.ResourceWithImportState = &OrganizationResource{}
26+
27+
func NewOrganizationResource() resource.Resource {
28+
return &OrganizationResource{}
29+
}
30+
31+
// OrganizationResource defines the resource implementation.
32+
type OrganizationResource struct {
33+
data *CoderdProviderData
34+
}
35+
36+
// OrganizationResourceModel describes the resource data model.
37+
type OrganizationResourceModel struct {
38+
ID UUID `tfsdk:"id"`
39+
40+
Name types.String `tfsdk:"name"`
41+
DisplayName types.String `tfsdk:"display_name"`
42+
Description types.String `tfsdk:"description"`
43+
Icon types.String `tfsdk:"icon"`
44+
Members types.Set `tfsdk:"members"`
45+
}
46+
47+
func (r *OrganizationResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
48+
resp.TypeName = req.ProviderTypeName + "_organization"
49+
}
50+
51+
func (r *OrganizationResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
52+
resp.Schema = schema.Schema{
53+
MarkdownDescription: "An organization on the coder deployment.",
54+
55+
Attributes: map[string]schema.Attribute{
56+
"id": schema.StringAttribute{
57+
CustomType: UUIDType,
58+
Computed: true,
59+
PlanModifiers: []planmodifier.String{
60+
stringplanmodifier.UseStateForUnknown(),
61+
},
62+
},
63+
"name": schema.StringAttribute{
64+
Required: true,
65+
},
66+
"display_name": schema.StringAttribute{
67+
Optional: true,
68+
Computed: true,
69+
},
70+
"description": schema.StringAttribute{
71+
Optional: true,
72+
Computed: true,
73+
Default: stringdefault.StaticString(""),
74+
},
75+
"icon": schema.StringAttribute{
76+
Optional: true,
77+
Computed: true,
78+
Default: stringdefault.StaticString(""),
79+
},
80+
"members": schema.SetAttribute{
81+
MarkdownDescription: "Members of the organization, by ID. If null, members will not be added or removed by Terraform.",
82+
ElementType: UUIDType,
83+
Optional: true,
84+
},
85+
// TODO: Custom roles, premium license gated
86+
},
87+
}
88+
}
89+
90+
func (r *OrganizationResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
91+
// Prevent panic if the provider has not been configured.
92+
if req.ProviderData == nil {
93+
return
94+
}
95+
96+
data, ok := req.ProviderData.(*CoderdProviderData)
97+
98+
if !ok {
99+
resp.Diagnostics.AddError(
100+
"Unexpected Resource Configure Type",
101+
fmt.Sprintf("Expected *CoderdProviderData, got: %T. Please report this issue to the provider developers.", req.ProviderData),
102+
)
103+
104+
return
105+
}
106+
107+
r.data = data
108+
}
109+
110+
func (r *OrganizationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
111+
var data OrganizationResourceModel
112+
113+
// Read Terraform plan data into the model
114+
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
115+
116+
if resp.Diagnostics.HasError() {
117+
return
118+
}
119+
120+
client := r.data.Client
121+
122+
displayName := data.Name.ValueString()
123+
if data.DisplayName.ValueString() != "" {
124+
displayName = data.DisplayName.ValueString()
125+
}
126+
127+
tflog.Trace(ctx, "creating organization")
128+
org, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
129+
Name: data.Name.ValueString(),
130+
DisplayName: displayName,
131+
Description: data.Description.ValueString(),
132+
Icon: data.Icon.ValueString(),
133+
})
134+
if err != nil {
135+
resp.Diagnostics.AddError("Failed to create organization", err.Error())
136+
return
137+
}
138+
tflog.Trace(ctx, "successfully created organization", map[string]any{
139+
"id": org.ID,
140+
})
141+
data.ID = UUIDValue(org.ID)
142+
data.DisplayName = types.StringValue(org.DisplayName)
143+
144+
tflog.Trace(ctx, "setting organization members")
145+
err = client.DeleteOrganizationMember(ctx, org.ID, codersdk.Me)
146+
if err != nil {
147+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to empty the organization member list, got error: %s", err))
148+
}
149+
var members []UUID
150+
resp.Diagnostics.Append(data.Members.ElementsAs(ctx, &members, false)...)
151+
if resp.Diagnostics.HasError() {
152+
return
153+
}
154+
for _, memberID := range members {
155+
_, err = client.PostOrganizationMember(ctx, org.ID, memberID.ValueString())
156+
if err != nil {
157+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to add member %s to organization %s, got error: %s", memberID, org.ID, err))
158+
return
159+
}
160+
}
161+
162+
tflog.Trace(ctx, "successfully set organization members")
163+
// Save data into Terraform state
164+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
165+
}
166+
167+
func (r *OrganizationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
168+
var data OrganizationResourceModel
169+
170+
// Read Terraform prior state data into the model
171+
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
172+
173+
if resp.Diagnostics.HasError() {
174+
return
175+
}
176+
177+
client := r.data.Client
178+
179+
orgID := data.ID.ValueUUID()
180+
org, err := client.Organization(ctx, orgID)
181+
if err != nil {
182+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization by ID, got error: %s", err))
183+
}
184+
185+
data.Name = types.StringValue(org.Name)
186+
data.DisplayName = types.StringValue(org.DisplayName)
187+
data.Description = types.StringValue(org.Description)
188+
data.Icon = types.StringValue(org.Icon)
189+
if !data.Members.IsNull() {
190+
members, err := client.OrganizationMembers(ctx, orgID)
191+
if err != nil {
192+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization members, got error: %s", err))
193+
return
194+
}
195+
memberIDs := make([]attr.Value, 0, len(members))
196+
for _, member := range members {
197+
memberIDs = append(memberIDs, UUIDValue(member.UserID))
198+
}
199+
data.Members = types.SetValueMust(UUIDType, memberIDs)
200+
}
201+
202+
// Save updated data into Terraform state
203+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
204+
}
205+
206+
func (r *OrganizationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
207+
var data OrganizationResourceModel
208+
209+
// Read Terraform plan data into the model
210+
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
211+
212+
if resp.Diagnostics.HasError() {
213+
return
214+
}
215+
216+
client := r.data.Client
217+
orgID := data.ID.ValueUUID()
218+
219+
orgMembers, err := client.OrganizationMembers(ctx, orgID)
220+
if err != nil {
221+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization members , got error: %s", err))
222+
return
223+
}
224+
225+
if !data.Members.IsNull() {
226+
var plannedMembers []UUID
227+
resp.Diagnostics.Append(data.Members.ElementsAs(ctx, &plannedMembers, false)...)
228+
if resp.Diagnostics.HasError() {
229+
return
230+
}
231+
curMembers := make([]uuid.UUID, 0, len(orgMembers))
232+
for _, member := range orgMembers {
233+
curMembers = append(curMembers, member.UserID)
234+
}
235+
add, remove := memberDiff(curMembers, plannedMembers)
236+
tflog.Trace(ctx, "updating organization members", map[string]any{
237+
"new_members": add,
238+
"removed_members": remove,
239+
})
240+
for _, memberID := range add {
241+
_, err := client.PostOrganizationMember(ctx, orgID, memberID)
242+
if err != nil {
243+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to add member %s to organization %s, got error: %s", memberID, orgID, err))
244+
return
245+
}
246+
}
247+
for _, memberID := range remove {
248+
err := client.DeleteOrganizationMember(ctx, orgID, memberID)
249+
if err != nil {
250+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to remove member %s from organization %s, got error: %s", memberID, orgID, err))
251+
return
252+
}
253+
}
254+
tflog.Trace(ctx, "successfully updated organization members")
255+
}
256+
257+
tflog.Trace(ctx, "updating organization", map[string]any{
258+
"id": orgID,
259+
"new_name": data.Name,
260+
"new_display_name": data.DisplayName,
261+
"new_description": data.Description,
262+
"new_icon": data.Icon,
263+
})
264+
_, err = client.UpdateOrganization(ctx, orgID.String(), codersdk.UpdateOrganizationRequest{
265+
Name: data.Name.ValueString(),
266+
DisplayName: data.DisplayName.ValueString(),
267+
Description: data.Description.ValueStringPointer(),
268+
Icon: data.Icon.ValueStringPointer(),
269+
})
270+
if err != nil {
271+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update organization %s, got error: %s", orgID, err))
272+
return
273+
}
274+
tflog.Trace(ctx, "successfully updated organization")
275+
276+
// Save updated data into Terraform state
277+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
278+
}
279+
280+
func (r *OrganizationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
281+
var data OrganizationResourceModel
282+
283+
// Read Terraform prior state data into the model
284+
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
285+
286+
if resp.Diagnostics.HasError() {
287+
return
288+
}
289+
290+
client := r.data.Client
291+
orgID := data.ID.ValueUUID()
292+
293+
tflog.Trace(ctx, "deleting organization", map[string]any{
294+
"id": orgID,
295+
})
296+
297+
err := client.DeleteOrganization(ctx, orgID.String())
298+
if err != nil {
299+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete organization %s, got error: %s", orgID, err))
300+
return
301+
}
302+
tflog.Trace(ctx, "successfully deleted organization")
303+
304+
// Read Terraform prior state data into the model
305+
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
306+
}
307+
308+
func (r *OrganizationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
309+
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
310+
}

0 commit comments

Comments
 (0)