Skip to content

Commit

Permalink
feat: add coderd_group resource
Browse files Browse the repository at this point in the history
  • Loading branch information
ethanndickson committed Jul 16, 2024
1 parent 375a205 commit dab2c45
Show file tree
Hide file tree
Showing 2 changed files with 326 additions and 0 deletions.
314 changes: 314 additions & 0 deletions internal/provider/group_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package provider

import (
"context"
"fmt"

"github.com/coder/coder/v2/codersdk"
"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/setdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)

// Ensure provider defined types fully satisfy framework interfaces.
var _ resource.Resource = &GroupResource{}
var _ resource.ResourceWithImportState = &GroupResource{}

func NewGroupResource() resource.Resource {
return &GroupResource{}
}

// GroupResource defines the resource implementation.
type GroupResource struct {
data *CoderdProviderData
}

// GroupResourceModel describes the resource data model.
type GroupResourceModel struct {
ID types.String `tfsdk:"id"`

Name types.String `tfsdk:"name"`
DisplayName types.String `tfsdk:"display_name"`
AvatarURL types.String `tfsdk:"avatar_url"`
QuotaAllowance types.Int32 `tfsdk:"quota_allowance"`
OrganizationID types.String `tfsdk:"organization_id"`
Members types.Set `tfsdk:"members"`
}

func (r *GroupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_group"
}

func (r *GroupResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "A group on the Coder deployment.",

Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "Group ID.",
Computed: true,
},
"name": schema.StringAttribute{
MarkdownDescription: "The unique name of the group.",
Required: true,
},
"display_name": schema.StringAttribute{
MarkdownDescription: "The display name of the group. Defaults to the group name.",
Computed: true,
Optional: true,
// Defaulted in Create
},
"avatar_url": schema.StringAttribute{
MarkdownDescription: "The URL of the group's avatar.",
Computed: true,
Optional: true,
Default: stringdefault.StaticString(""),
},
// Int32 in the db
"quota_allowance": schema.Int32Attribute{
MarkdownDescription: "The number of quota credits to allocate to each user in the group.",
Required: true,
},
"organization_id": schema.StringAttribute{
MarkdownDescription: "The organization ID that the group belongs to.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"members": schema.SetAttribute{
MarkdownDescription: "Members of the group, by ID.",
ElementType: types.StringType,
Computed: true,
Optional: true,
Default: setdefault.StaticValue(types.SetValueMust(types.StringType, []attr.Value{})),
},
},
}
}

func (r *GroupResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}

data, ok := req.ProviderData.(*CoderdProviderData)

if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *CoderdProviderData, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)

return
}

r.data = data
}

func (r *GroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data GroupResourceModel

// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

client := r.data.Client

orgID, err := uuid.Parse(data.OrganizationID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to parse supplied organization ID as UUID, got error: %s", err))
}

displayName := data.Name.ValueString()
if data.DisplayName.ValueString() != "" {
displayName = data.DisplayName.ValueString()
}

tflog.Trace(ctx, "creating group")
group, err := client.CreateGroup(ctx, orgID, codersdk.CreateGroupRequest{
Name: data.Name.ValueString(),
DisplayName: displayName,
AvatarURL: data.AvatarURL.ValueString(),
QuotaAllowance: 0,
})
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create group, got error: %s", err))
return
}
tflog.Trace(ctx, "successfully created group", map[string]any{
"id": group.ID.String(),
})
data.ID = types.StringValue(group.ID.String())
data.DisplayName = types.StringValue(group.DisplayName)

tflog.Trace(ctx, "setting group members")
var members []string
resp.Diagnostics.Append(
data.Members.ElementsAs(ctx, &members, false)...,
)
group, err = client.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
AddUsers: members,
})
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to add members to group, got error: %s", err))
return
}
tflog.Trace(ctx, "successfully set group members")

// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *GroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data GroupResourceModel

// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

client := r.data.Client

groupID, err := uuid.Parse(data.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to parse supplied group ID as UUID, got error: %s", err))
}

group, err := client.Group(ctx, groupID)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get group, got error: %s", err))
return
}

data.Name = types.StringValue(group.Name)
data.DisplayName = types.StringValue(group.DisplayName)
data.AvatarURL = types.StringValue(group.AvatarURL)
data.QuotaAllowance = types.Int32Value(int32(group.QuotaAllowance))
data.OrganizationID = types.StringValue(group.OrganizationID.String())
members := make([]attr.Value, 0, len(group.Members))
for _, member := range group.Members {
members = append(members, types.StringValue(member.ID.String()))
}
data.Members = types.SetValueMust(types.StringType, members)

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *GroupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data GroupResourceModel

// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

client := r.data.Client
groupID, err := uuid.Parse(data.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to parse supplied group ID as UUID, got error: %s", err))
}

group, err := client.Group(ctx, groupID)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get group, got error: %s", err))
return
}
var newMembers []string
resp.Diagnostics.Append(
data.Members.ElementsAs(ctx, &newMembers, false)...,
)
add, remove := memberDiff(group.Members, newMembers)
tflog.Trace(ctx, "updating group", map[string]any{
"new_members": add,
"removed_members": remove,
"new_name": data.Name,
"new_displayname": data.DisplayName,
"new_avatarurl": data.AvatarURL,
"new_quota": data.QuotaAllowance,
})

quotaAllowance := int(data.QuotaAllowance.ValueInt32())
client.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
AddUsers: add,
RemoveUsers: remove,
Name: data.Name.ValueString(),
DisplayName: data.DisplayName.ValueStringPointer(),
AvatarURL: data.AvatarURL.ValueStringPointer(),
QuotaAllowance: &quotaAllowance,
})
tflog.Trace(ctx, "successfully updated group")

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *GroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data GroupResourceModel

// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

client := r.data.Client
groupID, err := uuid.Parse(data.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to parse supplied group ID as UUID, got error: %s", err))
}

tflog.Trace(ctx, "deleting group")
err = client.DeleteGroup(ctx, groupID)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete group, got error: %s", err))
return
}
tflog.Trace(ctx, "successfully deleted group")
}

func (r *GroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}

func memberDiff(curMembers []codersdk.ReducedUser, newMembers []string) (add, remove []string) {
curSet := make(map[string]struct{}, len(curMembers))
newSet := make(map[string]struct{}, len(newMembers))

for _, user := range curMembers {
curSet[user.ID.String()] = struct{}{}
}
for _, userID := range newMembers {
newSet[userID] = struct{}{}
if _, exists := curSet[userID]; !exists {
add = append(add, userID)
}
}
for _, user := range curMembers {
if _, exists := newSet[user.ID.String()]; !exists {
remove = append(remove, user.ID.String())
}
}
return add, remove
}
12 changes: 12 additions & 0 deletions internal/provider/group_resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package provider

import (
"testing"
)

func TestAccGroupResource(t *testing.T) {

}

0 comments on commit dab2c45

Please sign in to comment.