Skip to content

Commit 31f7669

Browse files
committed
feat: add coderd_group resource
1 parent 375a205 commit 31f7669

File tree

7 files changed

+549
-75
lines changed

7 files changed

+549
-75
lines changed

integration/integration.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,9 @@ func StartCoder(ctx context.Context, t *testing.T, name string) *codersdk.Client
7575

7676
// nolint:gosec // For testing only.
7777
var (
78-
testEmail = "testing@coder.com"
78+
testEmail = "admin@coder.com"
7979
testPassword = "InsecurePassw0rd!"
80-
testUsername = "testing"
80+
testUsername = "admin"
8181
)
8282

8383
// Perform first time setup
@@ -96,6 +96,7 @@ func StartCoder(ctx context.Context, t *testing.T, name string) *codersdk.Client
9696
Email: testEmail,
9797
Username: testUsername,
9898
Password: testPassword,
99+
Trial: true,
99100
})
100101
require.NoError(t, err, "create first user")
101102
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{

internal/provider/group_resource.go

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
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/setdefault"
18+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
19+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
20+
"github.com/hashicorp/terraform-plugin-framework/types"
21+
"github.com/hashicorp/terraform-plugin-log/tflog"
22+
)
23+
24+
// Ensure provider defined types fully satisfy framework interfaces.
25+
var _ resource.Resource = &GroupResource{}
26+
var _ resource.ResourceWithImportState = &GroupResource{}
27+
28+
func NewGroupResource() resource.Resource {
29+
return &GroupResource{}
30+
}
31+
32+
// GroupResource defines the resource implementation.
33+
type GroupResource struct {
34+
data *CoderdProviderData
35+
}
36+
37+
// GroupResourceModel describes the resource data model.
38+
type GroupResourceModel struct {
39+
ID types.String `tfsdk:"id"`
40+
41+
Name types.String `tfsdk:"name"`
42+
DisplayName types.String `tfsdk:"display_name"`
43+
AvatarURL types.String `tfsdk:"avatar_url"`
44+
QuotaAllowance types.Int32 `tfsdk:"quota_allowance"`
45+
OrganizationID types.String `tfsdk:"organization_id"`
46+
Members types.Set `tfsdk:"members"`
47+
}
48+
49+
func (r *GroupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
50+
resp.TypeName = req.ProviderTypeName + "_group"
51+
}
52+
53+
func (r *GroupResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
54+
resp.Schema = schema.Schema{
55+
MarkdownDescription: "A group on the Coder deployment.",
56+
57+
Attributes: map[string]schema.Attribute{
58+
"id": schema.StringAttribute{
59+
MarkdownDescription: "Group ID.",
60+
Computed: true,
61+
PlanModifiers: []planmodifier.String{
62+
stringplanmodifier.UseStateForUnknown(),
63+
},
64+
},
65+
"name": schema.StringAttribute{
66+
MarkdownDescription: "The unique name of the group.",
67+
Required: true,
68+
},
69+
"display_name": schema.StringAttribute{
70+
MarkdownDescription: "The display name of the group. Defaults to the group name.",
71+
Computed: true,
72+
Optional: true,
73+
// Defaulted in Create
74+
},
75+
"avatar_url": schema.StringAttribute{
76+
MarkdownDescription: "The URL of the group's avatar.",
77+
Computed: true,
78+
Optional: true,
79+
Default: stringdefault.StaticString(""),
80+
},
81+
// Int32 in the db
82+
"quota_allowance": schema.Int32Attribute{
83+
MarkdownDescription: "The number of quota credits to allocate to each user in the group.",
84+
Required: true,
85+
},
86+
"organization_id": schema.StringAttribute{
87+
MarkdownDescription: "The organization ID that the group belongs to.",
88+
Required: true,
89+
PlanModifiers: []planmodifier.String{
90+
stringplanmodifier.RequiresReplace(),
91+
},
92+
},
93+
"members": schema.SetAttribute{
94+
MarkdownDescription: "Members of the group, by ID.",
95+
ElementType: types.StringType,
96+
Computed: true,
97+
Optional: true,
98+
Default: setdefault.StaticValue(types.SetValueMust(types.StringType, []attr.Value{})),
99+
},
100+
},
101+
}
102+
}
103+
104+
func (r *GroupResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
105+
// Prevent panic if the provider has not been configured.
106+
if req.ProviderData == nil {
107+
return
108+
}
109+
110+
data, ok := req.ProviderData.(*CoderdProviderData)
111+
112+
if !ok {
113+
resp.Diagnostics.AddError(
114+
"Unexpected Resource Configure Type",
115+
fmt.Sprintf("Expected *CoderdProviderData, got: %T. Please report this issue to the provider developers.", req.ProviderData),
116+
)
117+
118+
return
119+
}
120+
121+
r.data = data
122+
}
123+
124+
func (r *GroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
125+
var data GroupResourceModel
126+
127+
// Read Terraform plan data into the model
128+
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
129+
130+
if resp.Diagnostics.HasError() {
131+
return
132+
}
133+
134+
client := r.data.Client
135+
136+
orgID, err := uuid.Parse(data.OrganizationID.ValueString())
137+
if err != nil {
138+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to parse supplied organization ID as UUID, got error: %s", err))
139+
return
140+
}
141+
142+
displayName := data.Name.ValueString()
143+
if data.DisplayName.ValueString() != "" {
144+
displayName = data.DisplayName.ValueString()
145+
}
146+
147+
tflog.Trace(ctx, "creating group")
148+
group, err := client.CreateGroup(ctx, orgID, codersdk.CreateGroupRequest{
149+
Name: data.Name.ValueString(),
150+
DisplayName: displayName,
151+
AvatarURL: data.AvatarURL.ValueString(),
152+
QuotaAllowance: int(data.QuotaAllowance.ValueInt32()),
153+
})
154+
if err != nil {
155+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create group, got error: %s", err))
156+
return
157+
}
158+
tflog.Trace(ctx, "successfully created group", map[string]any{
159+
"id": group.ID.String(),
160+
})
161+
data.ID = types.StringValue(group.ID.String())
162+
data.DisplayName = types.StringValue(group.DisplayName)
163+
164+
tflog.Trace(ctx, "setting group members")
165+
var members []string
166+
resp.Diagnostics.Append(
167+
data.Members.ElementsAs(ctx, &members, false)...,
168+
)
169+
group, err = client.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
170+
AddUsers: members,
171+
})
172+
if err != nil {
173+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to add members to group, got error: %s", err))
174+
return
175+
}
176+
tflog.Trace(ctx, "successfully set group members")
177+
178+
// Save data into Terraform state
179+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
180+
}
181+
182+
func (r *GroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
183+
var data GroupResourceModel
184+
185+
// Read Terraform prior state data into the model
186+
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
187+
188+
if resp.Diagnostics.HasError() {
189+
return
190+
}
191+
192+
client := r.data.Client
193+
194+
groupID, err := uuid.Parse(data.ID.ValueString())
195+
if err != nil {
196+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to parse supplied group ID as UUID, got error: %s", err))
197+
return
198+
}
199+
200+
group, err := client.Group(ctx, groupID)
201+
if err != nil {
202+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get group, got error: %s", err))
203+
return
204+
}
205+
206+
data.Name = types.StringValue(group.Name)
207+
data.DisplayName = types.StringValue(group.DisplayName)
208+
data.AvatarURL = types.StringValue(group.AvatarURL)
209+
data.QuotaAllowance = types.Int32Value(int32(group.QuotaAllowance))
210+
data.OrganizationID = types.StringValue(group.OrganizationID.String())
211+
members := make([]attr.Value, 0, len(group.Members))
212+
for _, member := range group.Members {
213+
members = append(members, types.StringValue(member.ID.String()))
214+
}
215+
data.Members = types.SetValueMust(types.StringType, members)
216+
217+
// Save updated data into Terraform state
218+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
219+
}
220+
221+
func (r *GroupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
222+
var data GroupResourceModel
223+
224+
// Read Terraform plan data into the model
225+
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
226+
227+
if resp.Diagnostics.HasError() {
228+
return
229+
}
230+
231+
client := r.data.Client
232+
groupID, err := uuid.Parse(data.ID.ValueString())
233+
if err != nil {
234+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to parse supplied group ID as UUID, got error: %s", err))
235+
return
236+
}
237+
238+
group, err := client.Group(ctx, groupID)
239+
if err != nil {
240+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get group, got error: %s", err))
241+
return
242+
}
243+
var newMembers []string
244+
resp.Diagnostics.Append(
245+
data.Members.ElementsAs(ctx, &newMembers, false)...,
246+
)
247+
add, remove := memberDiff(group.Members, newMembers)
248+
tflog.Trace(ctx, "updating group", map[string]any{
249+
"new_members": add,
250+
"removed_members": remove,
251+
"new_name": data.Name,
252+
"new_displayname": data.DisplayName,
253+
"new_avatarurl": data.AvatarURL,
254+
"new_quota": data.QuotaAllowance,
255+
})
256+
257+
quotaAllowance := int(data.QuotaAllowance.ValueInt32())
258+
client.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
259+
AddUsers: add,
260+
RemoveUsers: remove,
261+
Name: data.Name.ValueString(),
262+
DisplayName: data.DisplayName.ValueStringPointer(),
263+
AvatarURL: data.AvatarURL.ValueStringPointer(),
264+
QuotaAllowance: &quotaAllowance,
265+
})
266+
tflog.Trace(ctx, "successfully updated group")
267+
268+
// Save updated data into Terraform state
269+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
270+
}
271+
272+
func (r *GroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
273+
var data GroupResourceModel
274+
275+
// Read Terraform prior state data into the model
276+
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
277+
278+
if resp.Diagnostics.HasError() {
279+
return
280+
}
281+
282+
client := r.data.Client
283+
groupID, err := uuid.Parse(data.ID.ValueString())
284+
if err != nil {
285+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to parse supplied group ID as UUID, got error: %s", err))
286+
return
287+
}
288+
289+
tflog.Trace(ctx, "deleting group")
290+
err = client.DeleteGroup(ctx, groupID)
291+
if err != nil {
292+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete group, got error: %s", err))
293+
return
294+
}
295+
tflog.Trace(ctx, "successfully deleted group")
296+
}
297+
298+
func (r *GroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
299+
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
300+
}
301+
302+
func memberDiff(curMembers []codersdk.ReducedUser, newMembers []string) (add, remove []string) {
303+
curSet := make(map[string]struct{}, len(curMembers))
304+
newSet := make(map[string]struct{}, len(newMembers))
305+
306+
for _, user := range curMembers {
307+
curSet[user.ID.String()] = struct{}{}
308+
}
309+
for _, userID := range newMembers {
310+
newSet[userID] = struct{}{}
311+
if _, exists := curSet[userID]; !exists {
312+
add = append(add, userID)
313+
}
314+
}
315+
for _, user := range curMembers {
316+
if _, exists := newSet[user.ID.String()]; !exists {
317+
remove = append(remove, user.ID.String())
318+
}
319+
}
320+
return add, remove
321+
}

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