@@ -4,20 +4,39 @@ import (
44 "context"
55 "fmt"
66
7- "github.com/coder/terraform-provider-coderd/internal"
8- "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
7+ "github.com/coder/coder/v2/coderd/util/slice"
8+ "github.com/coder/coder/v2/codersdk"
9+ "github.com/coder/terraform-provider-coderd/internal/codersdkvalidator"
10+ "github.com/google/uuid"
11+ "github.com/hashicorp/terraform-plugin-framework/attr"
12+ "github.com/hashicorp/terraform-plugin-framework/path"
913 "github.com/hashicorp/terraform-plugin-framework/resource"
1014 "github.com/hashicorp/terraform-plugin-framework/resource/schema"
1115 "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
16+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
1217 "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
1318 "github.com/hashicorp/terraform-plugin-framework/schema/validator"
19+ "github.com/hashicorp/terraform-plugin-framework/types"
20+ "github.com/hashicorp/terraform-plugin-log/tflog"
1421)
1522
1623// Ensure provider defined types fully satisfy framework interfaces.
1724var _ resource.Resource = & OrganizationResource {}
25+ var _ resource.ResourceWithImportState = & OrganizationResource {}
1826
1927type OrganizationResource struct {
20- data * CoderdProviderData
28+ * CoderdProviderData
29+ }
30+
31+ // OrganizationResourceModel describes the resource data model.
32+ type OrganizationResourceModel struct {
33+ ID UUID `tfsdk:"id"`
34+
35+ Name types.String `tfsdk:"name"`
36+ DisplayName types.String `tfsdk:"display_name"`
37+ Description types.String `tfsdk:"description"`
38+ Icon types.String `tfsdk:"icon"`
39+ Members types.Set `tfsdk:"members"`
2140}
2241
2342func NewOrganizationResource () resource.Resource {
@@ -33,30 +52,43 @@ func (r *OrganizationResource) Schema(ctx context.Context, req resource.SchemaRe
3352 MarkdownDescription : "An organization on the Coder deployment" ,
3453
3554 Attributes : map [string ]schema.Attribute {
55+ "id" : schema.StringAttribute {
56+ CustomType : UUIDType ,
57+ Computed : true ,
58+ MarkdownDescription : "Organization ID" ,
59+ PlanModifiers : []planmodifier.String {
60+ stringplanmodifier .UseStateForUnknown (),
61+ },
62+ },
3663 "name" : schema.StringAttribute {
37- MarkdownDescription : "Username of the user ." ,
64+ MarkdownDescription : "Username of the organization ." ,
3865 Required : true ,
3966 Validators : []validator.String {
40- stringvalidator .LengthBetween (1 , 32 ),
41- stringvalidator .RegexMatches (nameValidRegex , "Username must be alphanumeric with hyphens." ),
67+ codersdkvalidator .Name (),
4268 },
4369 },
44- "name " : schema.StringAttribute {
45- MarkdownDescription : "Display name of the user . Defaults to username ." ,
70+ "display_name " : schema.StringAttribute {
71+ MarkdownDescription : "Display name of the organization . Defaults to name ." ,
4672 Computed : true ,
4773 Optional : true ,
4874 Validators : []validator.String {
49- stringvalidator . LengthBetween ( 1 , 128 ),
75+ codersdkvalidator . DisplayName ( ),
5076 },
5177 },
52-
53- "id" : schema.StringAttribute {
54- CustomType : internal .UUIDType ,
55- Computed : true ,
56- MarkdownDescription : "Organization ID" ,
57- PlanModifiers : []planmodifier.String {
58- stringplanmodifier .UseStateForUnknown (),
59- },
78+ "description" : schema.StringAttribute {
79+ Optional : true ,
80+ Computed : true ,
81+ Default : stringdefault .StaticString ("" ),
82+ },
83+ "icon" : schema.StringAttribute {
84+ Optional : true ,
85+ Computed : true ,
86+ Default : stringdefault .StaticString ("" ),
87+ },
88+ "members" : schema.SetAttribute {
89+ MarkdownDescription : "Members of the organization, by ID. If null, members will not be added or removed by Terraform." ,
90+ ElementType : UUIDType ,
91+ Optional : true ,
6092 },
6193 },
6294 }
@@ -79,16 +111,216 @@ func (r *OrganizationResource) Configure(ctx context.Context, req resource.Confi
79111 return
80112 }
81113
82- r .data = data
114+ r .CoderdProviderData = data
83115}
84116
85117func (r * OrganizationResource ) Read (ctx context.Context , req resource.ReadRequest , resp * resource.ReadResponse ) {
118+ // Read Terraform prior state data into the model
119+ var data OrganizationResourceModel
120+ resp .Diagnostics .Append (req .State .Get (ctx , & data )... )
121+ if resp .Diagnostics .HasError () {
122+ return
123+ }
124+
125+ orgID := data .ID .ValueUUID ()
126+ org , err := r .Client .Organization (ctx , orgID )
127+ if err != nil {
128+ resp .Diagnostics .AddError ("Client Error" , fmt .Sprintf ("Unable to get organization by ID, got error: %s" , err ))
129+ return
130+ }
131+
132+ // We've fetched the organization ID from state, and the latest values for
133+ // everything else from the backend. Ensure that any mutable data is synced
134+ // with the backend.
135+ data .Name = types .StringValue (org .Name )
136+ data .DisplayName = types .StringValue (org .DisplayName )
137+ data .Description = types .StringValue (org .Description )
138+ data .Icon = types .StringValue (org .Icon )
139+ if ! data .Members .IsNull () {
140+ members , err := r .Client .OrganizationMembers (ctx , orgID )
141+ if err != nil {
142+ resp .Diagnostics .AddError ("Client Error" , fmt .Sprintf ("Unable to get organization members, got error: %s" , err ))
143+ return
144+ }
145+ memberIDs := make ([]attr.Value , 0 , len (members ))
146+ for _ , member := range members {
147+ memberIDs = append (memberIDs , UUIDValue (member .UserID ))
148+ }
149+ data .Members = types .SetValueMust (UUIDType , memberIDs )
150+ }
151+
152+ // Save updated data into Terraform state
153+ resp .Diagnostics .Append (resp .State .Set (ctx , & data )... )
86154}
87- func (r * OrganizationResource ) ImportState (ctx context.Context , req resource.ImportStateRequest , resp * resource.ImportStateResponse ) {
88- }
155+
89156func (r * OrganizationResource ) Create (ctx context.Context , req resource.CreateRequest , resp * resource.CreateResponse ) {
157+ // Read Terraform plan data into the model
158+ var data OrganizationResourceModel
159+ resp .Diagnostics .Append (req .Plan .Get (ctx , & data )... )
160+ if resp .Diagnostics .HasError () {
161+ return
162+ }
163+
164+ tflog .Trace (ctx , "creating organization" )
165+ org , err := r .Client .CreateOrganization (ctx , codersdk.CreateOrganizationRequest {
166+ Name : data .Name .ValueString (),
167+ DisplayName : data .DisplayName .ValueString (),
168+ Description : data .Description .ValueString (),
169+ Icon : data .Icon .ValueString (),
170+ })
171+ if err != nil {
172+ resp .Diagnostics .AddError ("Failed to create organization" , err .Error ())
173+ return
174+ }
175+ tflog .Trace (ctx , "successfully created organization" , map [string ]any {
176+ "id" : org .ID ,
177+ })
178+ // Fill in `ID` since it must be "computed".
179+ data .ID = UUIDValue (org .ID )
180+ // We also fill in `DisplayName`, since it's optional but the backend will
181+ // default it.
182+ data .DisplayName = types .StringValue (org .DisplayName )
183+
184+ // Only configure members if they're specified
185+ if ! data .Members .IsNull () {
186+ tflog .Trace (ctx , "setting organization members" )
187+ var members []UUID
188+ resp .Diagnostics .Append (data .Members .ElementsAs (ctx , & members , false )... )
189+ if resp .Diagnostics .HasError () {
190+ return
191+ }
192+
193+ for _ , memberID := range members {
194+ _ , err = r .Client .PostOrganizationMember (ctx , org .ID , memberID .ValueString ())
195+ if err != nil {
196+ resp .Diagnostics .AddError ("Client Error" , fmt .Sprintf ("Unable to add member %s to organization %s, got error: %s" , memberID , org .ID , err ))
197+ return
198+ }
199+ }
200+
201+ // Coder adds the user who creates the organization by default, but we may
202+ // actually be connected as a user who isn't in the list of members. If so
203+ // we should remove them!
204+ me , err := r .Client .User (ctx , codersdk .Me )
205+ if err != nil {
206+ resp .Diagnostics .AddError ("Client Error" , fmt .Sprintf ("Unable to get current user, got error: %s" , err ))
207+ return
208+ }
209+ if slice .Contains (members , UUIDValue (me .ID )) {
210+ err = r .Client .DeleteOrganizationMember (ctx , org .ID , codersdk .Me )
211+ if err != nil {
212+ resp .Diagnostics .AddError ("Client Error" , fmt .Sprintf ("Unable to delete self from new organization: %s" , err ))
213+ return
214+ }
215+ }
216+
217+ tflog .Trace (ctx , "successfully set organization members" )
218+ }
219+
220+ // Save data into Terraform state
221+ resp .Diagnostics .Append (resp .State .Set (ctx , & data )... )
90222}
223+
91224func (r * OrganizationResource ) Update (ctx context.Context , req resource.UpdateRequest , resp * resource.UpdateResponse ) {
225+ // Read Terraform plan data into the model
226+ var data OrganizationResourceModel
227+ resp .Diagnostics .Append (req .Plan .Get (ctx , & data )... )
228+ if resp .Diagnostics .HasError () {
229+ return
230+ }
231+
232+ orgID := data .ID .ValueUUID ()
233+
234+ // Update the organization metadata
235+ tflog .Trace (ctx , "updating organization" , map [string ]any {
236+ "id" : orgID ,
237+ "new_name" : data .Name ,
238+ "new_display_name" : data .DisplayName ,
239+ "new_description" : data .Description ,
240+ "new_icon" : data .Icon ,
241+ })
242+ _ , err := r .Client .UpdateOrganization (ctx , orgID .String (), codersdk.UpdateOrganizationRequest {
243+ Name : data .Name .ValueString (),
244+ DisplayName : data .DisplayName .ValueString (),
245+ Description : data .Description .ValueStringPointer (),
246+ Icon : data .Icon .ValueStringPointer (),
247+ })
248+ if err != nil {
249+ resp .Diagnostics .AddError ("Client Error" , fmt .Sprintf ("Unable to update organization %s, got error: %s" , orgID , err ))
250+ return
251+ }
252+ tflog .Trace (ctx , "successfully updated organization" )
253+
254+ // If the organization membership is managed, update them.
255+ if ! data .Members .IsNull () {
256+ orgMembers , err := r .Client .OrganizationMembers (ctx , orgID )
257+ if err != nil {
258+ resp .Diagnostics .AddError ("Client Error" , fmt .Sprintf ("Unable to get organization members , got error: %s" , err ))
259+ return
260+ }
261+ currentMembers := make ([]uuid.UUID , 0 , len (orgMembers ))
262+ for _ , member := range orgMembers {
263+ currentMembers = append (currentMembers , member .UserID )
264+ }
265+
266+ var plannedMembers []UUID
267+ resp .Diagnostics .Append (data .Members .ElementsAs (ctx , & plannedMembers , false )... )
268+ if resp .Diagnostics .HasError () {
269+ return
270+ }
271+
272+ add , remove := memberDiff (currentMembers , plannedMembers )
273+ tflog .Trace (ctx , "updating organization members" , map [string ]any {
274+ "new_members" : add ,
275+ "removed_members" : remove ,
276+ })
277+ for _ , memberID := range add {
278+ _ , err := r .Client .PostOrganizationMember (ctx , orgID , memberID )
279+ if err != nil {
280+ resp .Diagnostics .AddError ("Client Error" , fmt .Sprintf ("Unable to add member %s to organization %s, got error: %s" , memberID , orgID , err ))
281+ return
282+ }
283+ }
284+ for _ , memberID := range remove {
285+ err := r .Client .DeleteOrganizationMember (ctx , orgID , memberID )
286+ if err != nil {
287+ resp .Diagnostics .AddError ("Client Error" , fmt .Sprintf ("Unable to remove member %s from organization %s, got error: %s" , memberID , orgID , err ))
288+ return
289+ }
290+ }
291+ tflog .Trace (ctx , "successfully updated organization members" )
292+ }
293+
294+ // Save updated data into Terraform state
295+ resp .Diagnostics .Append (resp .State .Set (ctx , & data )... )
92296}
297+
93298func (r * OrganizationResource ) Delete (ctx context.Context , req resource.DeleteRequest , resp * resource.DeleteResponse ) {
299+ // Read Terraform prior state data into the model
300+ var data OrganizationResourceModel
301+ resp .Diagnostics .Append (req .State .Get (ctx , & data )... )
302+ if resp .Diagnostics .HasError () {
303+ return
304+ }
305+
306+ orgID := data .ID .ValueUUID ()
307+
308+ tflog .Trace (ctx , "deleting organization" , map [string ]any {
309+ "id" : orgID ,
310+ })
311+ err := r .Client .DeleteOrganization (ctx , orgID .String ())
312+ if err != nil {
313+ resp .Diagnostics .AddError ("Client Error" , fmt .Sprintf ("Unable to delete organization %s, got error: %s" , orgID , err ))
314+ return
315+ }
316+ tflog .Trace (ctx , "successfully deleted organization" )
317+
318+ // Read Terraform prior state data into the model
319+ resp .Diagnostics .Append (req .State .Get (ctx , & data )... )
320+ }
321+
322+ func (r * OrganizationResource ) ImportState (ctx context.Context , req resource.ImportStateRequest , resp * resource.ImportStateResponse ) {
323+ // Terraform will eventually `Read` in the rest of the fields after we have
324+ // set the `id` attribute.
325+ resource .ImportStatePassthroughID (ctx , path .Root ("id" ), req , resp )
94326}
0 commit comments