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

Skip to content

Commit 0d1743d

Browse files
committed
feat: add coderd_group resource
1 parent 375a205 commit 0d1743d

File tree

8 files changed

+585
-75
lines changed

8 files changed

+585
-75
lines changed

docs/resources/group.md

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "coderd_group Resource - coderd"
4+
subcategory: ""
5+
description: |-
6+
A group on the Coder deployment.
7+
---
8+
9+
# coderd_group (Resource)
10+
11+
A group on the Coder deployment.
12+
13+
14+
15+
<!-- schema generated by tfplugindocs -->
16+
## Schema
17+
18+
### Required
19+
20+
- `name` (String) The unique name of the group.
21+
- `organization_id` (String) The organization ID that the group belongs to.
22+
- `quota_allowance` (Number) The number of quota credits to allocate to each user in the group.
23+
24+
### Optional
25+
26+
- `avatar_url` (String) The URL of the group's avatar.
27+
- `display_name` (String) The display name of the group. Defaults to the group name.
28+
- `members` (Set of String) Members of the group, by ID.
29+
30+
### Read-Only
31+
32+
- `id` (String) Group ID.

integration/integration.go

+3-2
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

+325
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
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+
_, err = 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+
if err != nil {
267+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update group, got error: %s", err))
268+
return
269+
}
270+
tflog.Trace(ctx, "successfully updated group")
271+
272+
// Save updated data into Terraform state
273+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
274+
}
275+
276+
func (r *GroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
277+
var data GroupResourceModel
278+
279+
// Read Terraform prior state data into the model
280+
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
281+
282+
if resp.Diagnostics.HasError() {
283+
return
284+
}
285+
286+
client := r.data.Client
287+
groupID, err := uuid.Parse(data.ID.ValueString())
288+
if err != nil {
289+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to parse supplied group ID as UUID, got error: %s", err))
290+
return
291+
}
292+
293+
tflog.Trace(ctx, "deleting group")
294+
err = client.DeleteGroup(ctx, groupID)
295+
if err != nil {
296+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete group, got error: %s", err))
297+
return
298+
}
299+
tflog.Trace(ctx, "successfully deleted group")
300+
}
301+
302+
func (r *GroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
303+
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
304+
}
305+
306+
func memberDiff(curMembers []codersdk.ReducedUser, newMembers []string) (add, remove []string) {
307+
curSet := make(map[string]struct{}, len(curMembers))
308+
newSet := make(map[string]struct{}, len(newMembers))
309+
310+
for _, user := range curMembers {
311+
curSet[user.ID.String()] = struct{}{}
312+
}
313+
for _, userID := range newMembers {
314+
newSet[userID] = struct{}{}
315+
if _, exists := curSet[userID]; !exists {
316+
add = append(add, userID)
317+
}
318+
}
319+
for _, user := range curMembers {
320+
if _, exists := newSet[user.ID.String()]; !exists {
321+
remove = append(remove, user.ID.String())
322+
}
323+
}
324+
return add, remove
325+
}

0 commit comments

Comments
 (0)