Skip to content

Commit bb10474

Browse files
committed
feat: add coderd_organization resource
1 parent cf6f843 commit bb10474

File tree

3 files changed

+474
-0
lines changed

3 files changed

+474
-0
lines changed
Lines changed: 310 additions & 0 deletions
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)
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