From 8b73a3e10127964ea1e148a086be00c4de639f0b Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Tue, 5 Nov 2024 22:56:20 +0000 Subject: [PATCH 01/17] add organization resource --- internal/provider/organization_resource.go | 94 ++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 internal/provider/organization_resource.go diff --git a/internal/provider/organization_resource.go b/internal/provider/organization_resource.go new file mode 100644 index 0000000..ad33d88 --- /dev/null +++ b/internal/provider/organization_resource.go @@ -0,0 +1,94 @@ +package provider + +import ( + "context" + "fmt" + + "github.com/coder/terraform-provider-coderd/internal" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &OrganizationResource{} + +type OrganizationResource struct { + data *CoderdProviderData +} + +func NewOrganizationResource() resource.Resource { + return &OrganizationResource{} +} + +func (r *OrganizationResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_organization" +} + +func (r *OrganizationResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "An organization on the Coder deployment", + + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + MarkdownDescription: "Username of the user.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 32), + stringvalidator.RegexMatches(nameValidRegex, "Username must be alphanumeric with hyphens."), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "Display name of the user. Defaults to username.", + Computed: true, + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 128), + }, + }, + + "id": schema.StringAttribute{ + CustomType: internal.UUIDType, + Computed: true, + MarkdownDescription: "Organization ID", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + } +} + +func (r *OrganizationResource) 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( + "Unable to configure provider data", + fmt.Sprintf("Expected *CoderdProviderData, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.data = data +} + +func (r *OrganizationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { +} +func (r *OrganizationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { +} +func (r *OrganizationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { +} +func (r *OrganizationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { +} +func (r *OrganizationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} From 4743b9ec37b03f3eec1d3072ae57b0d50d568447 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 7 Nov 2024 23:26:16 +0000 Subject: [PATCH 02/17] flesh out the organization resource --- internal/provider/organization_resource.go | 272 +++++++++++++++++++-- internal/provider/util.go | 8 +- 2 files changed, 256 insertions(+), 24 deletions(-) diff --git a/internal/provider/organization_resource.go b/internal/provider/organization_resource.go index ad33d88..14b7a8f 100644 --- a/internal/provider/organization_resource.go +++ b/internal/provider/organization_resource.go @@ -4,20 +4,39 @@ import ( "context" "fmt" - "github.com/coder/terraform-provider-coderd/internal" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/coder/coder/v2/coderd/util/slice" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/terraform-provider-coderd/internal/codersdkvalidator" + "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/stringdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "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 = &OrganizationResource{} +var _ resource.ResourceWithImportState = &OrganizationResource{} type OrganizationResource struct { - data *CoderdProviderData + *CoderdProviderData +} + +// OrganizationResourceModel describes the resource data model. +type OrganizationResourceModel struct { + ID UUID `tfsdk:"id"` + + Name types.String `tfsdk:"name"` + DisplayName types.String `tfsdk:"display_name"` + Description types.String `tfsdk:"description"` + Icon types.String `tfsdk:"icon"` + Members types.Set `tfsdk:"members"` } func NewOrganizationResource() resource.Resource { @@ -33,30 +52,43 @@ func (r *OrganizationResource) Schema(ctx context.Context, req resource.SchemaRe MarkdownDescription: "An organization on the Coder deployment", Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + CustomType: UUIDType, + Computed: true, + MarkdownDescription: "Organization ID", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, "name": schema.StringAttribute{ - MarkdownDescription: "Username of the user.", + MarkdownDescription: "Username of the organization.", Required: true, Validators: []validator.String{ - stringvalidator.LengthBetween(1, 32), - stringvalidator.RegexMatches(nameValidRegex, "Username must be alphanumeric with hyphens."), + codersdkvalidator.Name(), }, }, - "name": schema.StringAttribute{ - MarkdownDescription: "Display name of the user. Defaults to username.", + "display_name": schema.StringAttribute{ + MarkdownDescription: "Display name of the organization. Defaults to name.", Computed: true, Optional: true, Validators: []validator.String{ - stringvalidator.LengthBetween(1, 128), + codersdkvalidator.DisplayName(), }, }, - - "id": schema.StringAttribute{ - CustomType: internal.UUIDType, - Computed: true, - MarkdownDescription: "Organization ID", - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, + "description": schema.StringAttribute{ + Optional: true, + Computed: true, + Default: stringdefault.StaticString(""), + }, + "icon": schema.StringAttribute{ + Optional: true, + Computed: true, + Default: stringdefault.StaticString(""), + }, + "members": schema.SetAttribute{ + MarkdownDescription: "Members of the organization, by ID. If null, members will not be added or removed by Terraform.", + ElementType: UUIDType, + Optional: true, }, }, } @@ -79,16 +111,216 @@ func (r *OrganizationResource) Configure(ctx context.Context, req resource.Confi return } - r.data = data + r.CoderdProviderData = data } func (r *OrganizationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Read Terraform prior state data into the model + var data OrganizationResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + orgID := data.ID.ValueUUID() + org, err := r.Client.Organization(ctx, orgID) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization by ID, got error: %s", err)) + return + } + + // We've fetched the organization ID from state, and the latest values for + // everything else from the backend. Ensure that any mutable data is synced + // with the backend. + data.Name = types.StringValue(org.Name) + data.DisplayName = types.StringValue(org.DisplayName) + data.Description = types.StringValue(org.Description) + data.Icon = types.StringValue(org.Icon) + if !data.Members.IsNull() { + members, err := r.Client.OrganizationMembers(ctx, orgID) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization members, got error: %s", err)) + return + } + memberIDs := make([]attr.Value, 0, len(members)) + for _, member := range members { + memberIDs = append(memberIDs, UUIDValue(member.UserID)) + } + data.Members = types.SetValueMust(UUIDType, memberIDs) + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } -func (r *OrganizationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { -} + func (r *OrganizationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Read Terraform plan data into the model + var data OrganizationResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Trace(ctx, "creating organization") + org, err := r.Client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ + Name: data.Name.ValueString(), + DisplayName: data.DisplayName.ValueString(), + Description: data.Description.ValueString(), + Icon: data.Icon.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError("Failed to create organization", err.Error()) + return + } + tflog.Trace(ctx, "successfully created organization", map[string]any{ + "id": org.ID, + }) + // Fill in `ID` since it must be "computed". + data.ID = UUIDValue(org.ID) + // We also fill in `DisplayName`, since it's optional but the backend will + // default it. + data.DisplayName = types.StringValue(org.DisplayName) + + // Only configure members if they're specified + if !data.Members.IsNull() { + tflog.Trace(ctx, "setting organization members") + var members []UUID + resp.Diagnostics.Append(data.Members.ElementsAs(ctx, &members, false)...) + if resp.Diagnostics.HasError() { + return + } + + for _, memberID := range members { + _, err = r.Client.PostOrganizationMember(ctx, org.ID, memberID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to add member %s to organization %s, got error: %s", memberID, org.ID, err)) + return + } + } + + // Coder adds the user who creates the organization by default, but we may + // actually be connected as a user who isn't in the list of members. If so + // we should remove them! + me, err := r.Client.User(ctx, codersdk.Me) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get current user, got error: %s", err)) + return + } + if slice.Contains(members, UUIDValue(me.ID)) { + err = r.Client.DeleteOrganizationMember(ctx, org.ID, codersdk.Me) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete self from new organization: %s", err)) + return + } + } + + tflog.Trace(ctx, "successfully set organization members") + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } + func (r *OrganizationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Read Terraform plan data into the model + var data OrganizationResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + orgID := data.ID.ValueUUID() + + // Update the organization metadata + tflog.Trace(ctx, "updating organization", map[string]any{ + "id": orgID, + "new_name": data.Name, + "new_display_name": data.DisplayName, + "new_description": data.Description, + "new_icon": data.Icon, + }) + _, err := r.Client.UpdateOrganization(ctx, orgID.String(), codersdk.UpdateOrganizationRequest{ + Name: data.Name.ValueString(), + DisplayName: data.DisplayName.ValueString(), + Description: data.Description.ValueStringPointer(), + Icon: data.Icon.ValueStringPointer(), + }) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update organization %s, got error: %s", orgID, err)) + return + } + tflog.Trace(ctx, "successfully updated organization") + + // If the organization membership is managed, update them. + if !data.Members.IsNull() { + orgMembers, err := r.Client.OrganizationMembers(ctx, orgID) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization members , got error: %s", err)) + return + } + currentMembers := make([]uuid.UUID, 0, len(orgMembers)) + for _, member := range orgMembers { + currentMembers = append(currentMembers, member.UserID) + } + + var plannedMembers []UUID + resp.Diagnostics.Append(data.Members.ElementsAs(ctx, &plannedMembers, false)...) + if resp.Diagnostics.HasError() { + return + } + + add, remove := memberDiff(currentMembers, plannedMembers) + tflog.Trace(ctx, "updating organization members", map[string]any{ + "new_members": add, + "removed_members": remove, + }) + for _, memberID := range add { + _, err := r.Client.PostOrganizationMember(ctx, orgID, memberID) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to add member %s to organization %s, got error: %s", memberID, orgID, err)) + return + } + } + for _, memberID := range remove { + err := r.Client.DeleteOrganizationMember(ctx, orgID, memberID) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to remove member %s from organization %s, got error: %s", memberID, orgID, err)) + return + } + } + tflog.Trace(ctx, "successfully updated organization members") + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } + func (r *OrganizationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Read Terraform prior state data into the model + var data OrganizationResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + orgID := data.ID.ValueUUID() + + tflog.Trace(ctx, "deleting organization", map[string]any{ + "id": orgID, + }) + err := r.Client.DeleteOrganization(ctx, orgID.String()) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete organization %s, got error: %s", orgID, err)) + return + } + tflog.Trace(ctx, "successfully deleted organization") + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) +} + +func (r *OrganizationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // Terraform will eventually `Read` in the rest of the fields after we have + // set the `id` attribute. + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } diff --git a/internal/provider/util.go b/internal/provider/util.go index 720259c..169286f 100644 --- a/internal/provider/util.go +++ b/internal/provider/util.go @@ -85,11 +85,11 @@ func computeDirectoryHash(directory string) (string, error) { // memberDiff returns the members to add and remove from the group, given the current members and the planned members. // plannedMembers is deliberately our custom type, as Terraform cannot automatically produce `[]uuid.UUID` from a set. -func memberDiff(curMembers []uuid.UUID, plannedMembers []UUID) (add, remove []string) { - curSet := make(map[uuid.UUID]struct{}, len(curMembers)) +func memberDiff(currentMembers []uuid.UUID, plannedMembers []UUID) (add, remove []string) { + curSet := make(map[uuid.UUID]struct{}, len(currentMembers)) planSet := make(map[uuid.UUID]struct{}, len(plannedMembers)) - for _, userID := range curMembers { + for _, userID := range currentMembers { curSet[userID] = struct{}{} } for _, plannedUserID := range plannedMembers { @@ -98,7 +98,7 @@ func memberDiff(curMembers []uuid.UUID, plannedMembers []UUID) (add, remove []st add = append(add, plannedUserID.ValueString()) } } - for _, curUserID := range curMembers { + for _, curUserID := range currentMembers { if _, exists := planSet[curUserID]; !exists { remove = append(remove, curUserID.String()) } From 435032b2a0475e87b840103068a39a27dfe41a60 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 7 Nov 2024 23:34:21 +0000 Subject: [PATCH 03/17] register new resource type + gen --- docs/resources/organization.md | 31 +++++++++++++++++++++++++++++++ internal/provider/provider.go | 1 + 2 files changed, 32 insertions(+) create mode 100644 docs/resources/organization.md diff --git a/docs/resources/organization.md b/docs/resources/organization.md new file mode 100644 index 0000000..e284875 --- /dev/null +++ b/docs/resources/organization.md @@ -0,0 +1,31 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "coderd_organization Resource - terraform-provider-coderd" +subcategory: "" +description: |- + An organization on the Coder deployment +--- + +# coderd_organization (Resource) + +An organization on the Coder deployment + + + + +## Schema + +### Required + +- `name` (String) Username of the organization. + +### Optional + +- `description` (String) +- `display_name` (String) Display name of the organization. Defaults to name. +- `icon` (String) +- `members` (Set of String) Members of the organization, by ID. If null, members will not be added or removed by Terraform. + +### Read-Only + +- `id` (String) Organization ID diff --git a/internal/provider/provider.go b/internal/provider/provider.go index bfeea5e..cc79997 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -139,6 +139,7 @@ func (p *CoderdProvider) Resources(ctx context.Context) []func() resource.Resour NewTemplateResource, NewWorkspaceProxyResource, NewLicenseResource, + NewOrganizationResource, } } From 71dc51bc554202dc389ce5fa9d46389897192840 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 8 Nov 2024 17:23:48 +0000 Subject: [PATCH 04/17] start with tests from ethan --- .../provider/organization_resource_test.go | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 internal/provider/organization_resource_test.go diff --git a/internal/provider/organization_resource_test.go b/internal/provider/organization_resource_test.go new file mode 100644 index 0000000..df9bcb3 --- /dev/null +++ b/internal/provider/organization_resource_test.go @@ -0,0 +1,164 @@ +package provider + +import ( + "context" + "os" + "strings" + "testing" + "text/template" + + "github.com/coder/coder/v2/coderd/util/ptr" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/terraform-provider-coderd/integration" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/stretchr/testify/require" +) + +func TestAccOrganizationResource(t *testing.T) { + if os.Getenv("TF_ACC") == "" { + t.Skip("Acceptance tests are disabled.") + } + + ctx := context.Background() + client := integration.StartCoder(ctx, t, "group_acc", true) + firstUser, err := client.User(ctx, codersdk.Me) + require.NoError(t, err) + + user1, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + Email: "example@coder.com", + Username: "example", + Password: "SomeSecurePassword!", + UserLoginType: "password", + OrganizationID: firstUser.OrganizationIDs[0], + }) + require.NoError(t, err) + + user2, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + Email: "example2@coder.com", + Username: "example2", + Password: "SomeSecurePassword!", + UserLoginType: "password", + OrganizationID: firstUser.OrganizationIDs[0], + }) + require.NoError(t, err) + + cfg1 := testAccOrganizationResourceConfig{ + URL: client.URL.String(), + Token: client.SessionToken(), + Name: ptr.Ref("example-org"), + DisplayName: ptr.Ref("Example Organization"), + Description: ptr.Ref("This is an example organization"), + Icon: ptr.Ref("/icon/coder.svg"), + Members: ptr.Ref([]string{user1.ID.String()}), + } + + cfg2 := cfg1 + cfg2.Name = ptr.Ref("example-org-new") + cfg2.DisplayName = ptr.Ref("Example Organization New") + cfg2.Members = ptr.Ref([]string{user2.ID.String()}) + + cfg3 := cfg2 + cfg3.Members = nil + + t.Run("CreateImportUpdateReadOk", func(t *testing.T) { + resource.Test(t, resource.TestCase{ + IsUnitTest: true, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read + { + Config: cfg1.String(t), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("coderd_organization.test", "name", "example-org"), + resource.TestCheckResourceAttr("coderd_organization.test", "display_name", "Example Organization"), + resource.TestCheckResourceAttr("coderd_organization.test", "icon", "/icon/coder.svg"), + resource.TestCheckResourceAttr("coderd_organization.test", "members.#", "1"), + resource.TestCheckResourceAttr("coderd_organization.test", "members.0", user1.ID.String()), + ), + }, + // Import + { + Config: cfg1.String(t), + ResourceName: "coderd_organization.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"members"}, + }, + // Update and Read + { + Config: cfg2.String(t), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("coderd_organization.test", "name", "example-org-new"), + resource.TestCheckResourceAttr("coderd_organization.test", "display_name", "Example Organization New"), + resource.TestCheckResourceAttr("coderd_organization.test", "members.#", "1"), + resource.TestCheckResourceAttr("coderd_organization.test", "members.0", user2.ID.String()), + ), + }, + // Unmanaged members + { + Config: cfg3.String(t), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckNoResourceAttr("coderd_organization.test", "members"), + ), + }, + }, + }) + }) + + t.Run("CreateUnmanagedMembersOk", func(t *testing.T) { + resource.Test(t, resource.TestCase{ + IsUnitTest: true, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: cfg3.String(t), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckNoResourceAttr("coderd_organization.test", "members"), + ), + }, + }, + }) + }) +} + +type testAccOrganizationResourceConfig struct { + URL string + Token string + + Name *string + DisplayName *string + Description *string + Icon *string + Members *[]string +} + +func (c testAccOrganizationResourceConfig) String(t *testing.T) string { + t.Helper() + tpl := ` +provider coderd { + url = "{{.URL}}" + token = "{{.Token}}" +} + +resource "coderd_organization" "test" { + name = {{orNull .Name}} + display_name = {{orNull .DisplayName}} + description = {{orNull .Description}} + icon = {{orNull .Icon}} + members = {{orNull .Members}} +} +` + funcMap := template.FuncMap{ + "orNull": PrintOrNull, + } + + buf := strings.Builder{} + tmpl, err := template.New("organizationResource").Funcs(funcMap).Parse(tpl) + require.NoError(t, err) + + err = tmpl.Execute(&buf, c) + require.NoError(t, err) + return buf.String() +} From 33979849794cd2c0172f1be28a59a50ce720ee0f Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 8 Nov 2024 18:39:54 +0000 Subject: [PATCH 05/17] ooooh, I get it, that was correct :^) --- internal/provider/organization_resource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/provider/organization_resource.go b/internal/provider/organization_resource.go index 14b7a8f..5c02585 100644 --- a/internal/provider/organization_resource.go +++ b/internal/provider/organization_resource.go @@ -206,7 +206,7 @@ func (r *OrganizationResource) Create(ctx context.Context, req resource.CreateRe resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get current user, got error: %s", err)) return } - if slice.Contains(members, UUIDValue(me.ID)) { + if !slice.Contains(members, UUIDValue(me.ID)) { err = r.Client.DeleteOrganizationMember(ctx, org.ID, codersdk.Me) if err != nil { resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete self from new organization: %s", err)) From 75c08589630820b395ebe84d745c18c14db3f28f Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 8 Nov 2024 19:44:52 +0000 Subject: [PATCH 06/17] hmm --- internal/provider/organization_resource_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/internal/provider/organization_resource_test.go b/internal/provider/organization_resource_test.go index df9bcb3..aa65b8e 100644 --- a/internal/provider/organization_resource_test.go +++ b/internal/provider/organization_resource_test.go @@ -73,8 +73,6 @@ func TestAccOrganizationResource(t *testing.T) { resource.TestCheckResourceAttr("coderd_organization.test", "name", "example-org"), resource.TestCheckResourceAttr("coderd_organization.test", "display_name", "Example Organization"), resource.TestCheckResourceAttr("coderd_organization.test", "icon", "/icon/coder.svg"), - resource.TestCheckResourceAttr("coderd_organization.test", "members.#", "1"), - resource.TestCheckResourceAttr("coderd_organization.test", "members.0", user1.ID.String()), ), }, // Import @@ -91,8 +89,6 @@ func TestAccOrganizationResource(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("coderd_organization.test", "name", "example-org-new"), resource.TestCheckResourceAttr("coderd_organization.test", "display_name", "Example Organization New"), - resource.TestCheckResourceAttr("coderd_organization.test", "members.#", "1"), - resource.TestCheckResourceAttr("coderd_organization.test", "members.0", user2.ID.String()), ), }, // Unmanaged members From cc2bb2eecbc1b36c84015c5a0814d5b712e6816c Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 8 Nov 2024 20:10:42 +0000 Subject: [PATCH 07/17] lets do members differently actually --- internal/provider/organization_resource.go | 97 ------------------- .../provider/organization_resource_test.go | 34 +------ 2 files changed, 5 insertions(+), 126 deletions(-) diff --git a/internal/provider/organization_resource.go b/internal/provider/organization_resource.go index 5c02585..f518f50 100644 --- a/internal/provider/organization_resource.go +++ b/internal/provider/organization_resource.go @@ -4,11 +4,8 @@ import ( "context" "fmt" - "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/codersdk" "github.com/coder/terraform-provider-coderd/internal/codersdkvalidator" - "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" @@ -36,7 +33,6 @@ type OrganizationResourceModel struct { DisplayName types.String `tfsdk:"display_name"` Description types.String `tfsdk:"description"` Icon types.String `tfsdk:"icon"` - Members types.Set `tfsdk:"members"` } func NewOrganizationResource() resource.Resource { @@ -85,11 +81,6 @@ func (r *OrganizationResource) Schema(ctx context.Context, req resource.SchemaRe Computed: true, Default: stringdefault.StaticString(""), }, - "members": schema.SetAttribute{ - MarkdownDescription: "Members of the organization, by ID. If null, members will not be added or removed by Terraform.", - ElementType: UUIDType, - Optional: true, - }, }, } } @@ -136,18 +127,6 @@ func (r *OrganizationResource) Read(ctx context.Context, req resource.ReadReques data.DisplayName = types.StringValue(org.DisplayName) data.Description = types.StringValue(org.Description) data.Icon = types.StringValue(org.Icon) - if !data.Members.IsNull() { - members, err := r.Client.OrganizationMembers(ctx, orgID) - if err != nil { - resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization members, got error: %s", err)) - return - } - memberIDs := make([]attr.Value, 0, len(members)) - for _, member := range members { - memberIDs = append(memberIDs, UUIDValue(member.UserID)) - } - data.Members = types.SetValueMust(UUIDType, memberIDs) - } // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) @@ -181,42 +160,6 @@ func (r *OrganizationResource) Create(ctx context.Context, req resource.CreateRe // default it. data.DisplayName = types.StringValue(org.DisplayName) - // Only configure members if they're specified - if !data.Members.IsNull() { - tflog.Trace(ctx, "setting organization members") - var members []UUID - resp.Diagnostics.Append(data.Members.ElementsAs(ctx, &members, false)...) - if resp.Diagnostics.HasError() { - return - } - - for _, memberID := range members { - _, err = r.Client.PostOrganizationMember(ctx, org.ID, memberID.ValueString()) - if err != nil { - resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to add member %s to organization %s, got error: %s", memberID, org.ID, err)) - return - } - } - - // Coder adds the user who creates the organization by default, but we may - // actually be connected as a user who isn't in the list of members. If so - // we should remove them! - me, err := r.Client.User(ctx, codersdk.Me) - if err != nil { - resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get current user, got error: %s", err)) - return - } - if !slice.Contains(members, UUIDValue(me.ID)) { - err = r.Client.DeleteOrganizationMember(ctx, org.ID, codersdk.Me) - if err != nil { - resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete self from new organization: %s", err)) - return - } - } - - tflog.Trace(ctx, "successfully set organization members") - } - // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -251,46 +194,6 @@ func (r *OrganizationResource) Update(ctx context.Context, req resource.UpdateRe } tflog.Trace(ctx, "successfully updated organization") - // If the organization membership is managed, update them. - if !data.Members.IsNull() { - orgMembers, err := r.Client.OrganizationMembers(ctx, orgID) - if err != nil { - resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization members , got error: %s", err)) - return - } - currentMembers := make([]uuid.UUID, 0, len(orgMembers)) - for _, member := range orgMembers { - currentMembers = append(currentMembers, member.UserID) - } - - var plannedMembers []UUID - resp.Diagnostics.Append(data.Members.ElementsAs(ctx, &plannedMembers, false)...) - if resp.Diagnostics.HasError() { - return - } - - add, remove := memberDiff(currentMembers, plannedMembers) - tflog.Trace(ctx, "updating organization members", map[string]any{ - "new_members": add, - "removed_members": remove, - }) - for _, memberID := range add { - _, err := r.Client.PostOrganizationMember(ctx, orgID, memberID) - if err != nil { - resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to add member %s to organization %s, got error: %s", memberID, orgID, err)) - return - } - } - for _, memberID := range remove { - err := r.Client.DeleteOrganizationMember(ctx, orgID, memberID) - if err != nil { - resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to remove member %s from organization %s, got error: %s", memberID, orgID, err)) - return - } - } - tflog.Trace(ctx, "successfully updated organization members") - } - // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } diff --git a/internal/provider/organization_resource_test.go b/internal/provider/organization_resource_test.go index aa65b8e..fa1cda5 100644 --- a/internal/provider/organization_resource_test.go +++ b/internal/provider/organization_resource_test.go @@ -21,25 +21,7 @@ func TestAccOrganizationResource(t *testing.T) { ctx := context.Background() client := integration.StartCoder(ctx, t, "group_acc", true) - firstUser, err := client.User(ctx, codersdk.Me) - require.NoError(t, err) - - user1, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ - Email: "example@coder.com", - Username: "example", - Password: "SomeSecurePassword!", - UserLoginType: "password", - OrganizationID: firstUser.OrganizationIDs[0], - }) - require.NoError(t, err) - - user2, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ - Email: "example2@coder.com", - Username: "example2", - Password: "SomeSecurePassword!", - UserLoginType: "password", - OrganizationID: firstUser.OrganizationIDs[0], - }) + _, err := client.User(ctx, codersdk.Me) require.NoError(t, err) cfg1 := testAccOrganizationResourceConfig{ @@ -49,16 +31,13 @@ func TestAccOrganizationResource(t *testing.T) { DisplayName: ptr.Ref("Example Organization"), Description: ptr.Ref("This is an example organization"), Icon: ptr.Ref("/icon/coder.svg"), - Members: ptr.Ref([]string{user1.ID.String()}), } cfg2 := cfg1 cfg2.Name = ptr.Ref("example-org-new") cfg2.DisplayName = ptr.Ref("Example Organization New") - cfg2.Members = ptr.Ref([]string{user2.ID.String()}) cfg3 := cfg2 - cfg3.Members = nil t.Run("CreateImportUpdateReadOk", func(t *testing.T) { resource.Test(t, resource.TestCase{ @@ -77,11 +56,10 @@ func TestAccOrganizationResource(t *testing.T) { }, // Import { - Config: cfg1.String(t), - ResourceName: "coderd_organization.test", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"members"}, + Config: cfg1.String(t), + ResourceName: "coderd_organization.test", + ImportState: true, + ImportStateVerify: true, }, // Update and Read { @@ -127,7 +105,6 @@ type testAccOrganizationResourceConfig struct { DisplayName *string Description *string Icon *string - Members *[]string } func (c testAccOrganizationResourceConfig) String(t *testing.T) string { @@ -143,7 +120,6 @@ resource "coderd_organization" "test" { display_name = {{orNull .DisplayName}} description = {{orNull .Description}} icon = {{orNull .Icon}} - members = {{orNull .Members}} } ` funcMap := template.FuncMap{ From d23168a97391884915834b330d0fc60405183a93 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 8 Nov 2024 20:13:03 +0000 Subject: [PATCH 08/17] gen --- Makefile | 4 ++++ docs/resources/organization.md | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 54a7a12..b1f903b 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,12 @@ default: testacc fmt: + go fmt ./... terraform fmt -recursive +vet: + go vet ./... + gen: go generate ./... diff --git a/docs/resources/organization.md b/docs/resources/organization.md index e284875..edef201 100644 --- a/docs/resources/organization.md +++ b/docs/resources/organization.md @@ -24,7 +24,6 @@ An organization on the Coder deployment - `description` (String) - `display_name` (String) Display name of the organization. Defaults to name. - `icon` (String) -- `members` (Set of String) Members of the organization, by ID. If null, members will not be added or removed by Terraform. ### Read-Only From 28b395ae07b642f48c1361153ee7917edcc2cd53 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 8 Nov 2024 22:38:47 +0000 Subject: [PATCH 09/17] statecheck --- .../provider/organization_resource_test.go | 28 ++++++++----------- internal/provider/provider.go | 1 - 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/internal/provider/organization_resource_test.go b/internal/provider/organization_resource_test.go index fa1cda5..174a8ca 100644 --- a/internal/provider/organization_resource_test.go +++ b/internal/provider/organization_resource_test.go @@ -11,6 +11,9 @@ import ( "github.com/coder/coder/v2/codersdk" "github.com/coder/terraform-provider-coderd/integration" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" "github.com/stretchr/testify/require" ) @@ -48,11 +51,11 @@ func TestAccOrganizationResource(t *testing.T) { // Create and Read { Config: cfg1.String(t), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("coderd_organization.test", "name", "example-org"), - resource.TestCheckResourceAttr("coderd_organization.test", "display_name", "Example Organization"), - resource.TestCheckResourceAttr("coderd_organization.test", "icon", "/icon/coder.svg"), - ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("name"), knownvalue.StringExact("example-org")), + statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("display_name"), knownvalue.StringExact("Example Organization")), + statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("icon"), knownvalue.StringExact("/icon/coder.svg")), + }, }, // Import { @@ -64,17 +67,10 @@ func TestAccOrganizationResource(t *testing.T) { // Update and Read { Config: cfg2.String(t), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("coderd_organization.test", "name", "example-org-new"), - resource.TestCheckResourceAttr("coderd_organization.test", "display_name", "Example Organization New"), - ), - }, - // Unmanaged members - { - Config: cfg3.String(t), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckNoResourceAttr("coderd_organization.test", "members"), - ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("name"), knownvalue.StringExact("example-org-new")), + statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("display_name"), knownvalue.StringExact("Example Organization New")), + }, }, }, }) diff --git a/internal/provider/provider.go b/internal/provider/provider.go index cc79997..7b7d165 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -78,7 +78,6 @@ This provider is only compatible with Coder version [2.10.1](https://github.com/ func (p *CoderdProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { var data CoderdProviderModel - resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) if resp.Diagnostics.HasError() { From f2d3e3ccb057f3a0ad77baad03279744ef3e1602 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Tue, 12 Nov 2024 21:19:55 +0000 Subject: [PATCH 10/17] feedback --- internal/provider/organization_resource.go | 7 ++++--- internal/provider/organization_resource_test.go | 16 ---------------- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/internal/provider/organization_resource.go b/internal/provider/organization_resource.go index f518f50..05bcc9a 100644 --- a/internal/provider/organization_resource.go +++ b/internal/provider/organization_resource.go @@ -57,7 +57,7 @@ func (r *OrganizationResource) Schema(ctx context.Context, req resource.SchemaRe }, }, "name": schema.StringAttribute{ - MarkdownDescription: "Username of the organization.", + MarkdownDescription: "Name of the organization.", Required: true, Validators: []validator.String{ codersdkvalidator.Name(), @@ -67,6 +67,7 @@ func (r *OrganizationResource) Schema(ctx context.Context, req resource.SchemaRe MarkdownDescription: "Display name of the organization. Defaults to name.", Computed: true, Optional: true, + Default: stringdefault.StaticString(""), Validators: []validator.String{ codersdkvalidator.DisplayName(), }, @@ -224,6 +225,6 @@ func (r *OrganizationResource) Delete(ctx context.Context, req resource.DeleteRe func (r *OrganizationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { // Terraform will eventually `Read` in the rest of the fields after we have - // set the `id` attribute. - resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + // set the `name` attribute. + resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp) } diff --git a/internal/provider/organization_resource_test.go b/internal/provider/organization_resource_test.go index 174a8ca..4dce520 100644 --- a/internal/provider/organization_resource_test.go +++ b/internal/provider/organization_resource_test.go @@ -75,22 +75,6 @@ func TestAccOrganizationResource(t *testing.T) { }, }) }) - - t.Run("CreateUnmanagedMembersOk", func(t *testing.T) { - resource.Test(t, resource.TestCase{ - IsUnitTest: true, - PreCheck: func() { testAccPreCheck(t) }, - ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, - Steps: []resource.TestStep{ - { - Config: cfg3.String(t), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckNoResourceAttr("coderd_organization.test", "members"), - ), - }, - }, - }) - }) } type testAccOrganizationResourceConfig struct { From 236c11e01d239ea875c8b1af85253550de64c4c7 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Tue, 12 Nov 2024 21:30:33 +0000 Subject: [PATCH 11/17] hiyo --- docs/resources/organization.md | 2 +- examples/resources/coderd_organization/import.sh | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 examples/resources/coderd_organization/import.sh diff --git a/docs/resources/organization.md b/docs/resources/organization.md index edef201..0b4b817 100644 --- a/docs/resources/organization.md +++ b/docs/resources/organization.md @@ -17,7 +17,7 @@ An organization on the Coder deployment ### Required -- `name` (String) Username of the organization. +- `name` (String) Name of the organization. ### Optional diff --git a/examples/resources/coderd_organization/import.sh b/examples/resources/coderd_organization/import.sh new file mode 100644 index 0000000..cd93ce2 --- /dev/null +++ b/examples/resources/coderd_organization/import.sh @@ -0,0 +1 @@ +terraform import coderd_organization.our_org our_org From 16d10e7e9fe052329ea5b0bc23b108a6448e5c6c Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Tue, 12 Nov 2024 21:39:46 +0000 Subject: [PATCH 12/17] :^) --- docs/resources/organization.md | 9 +++++++++ examples/resources/coderd_organization/import.sh | 1 + internal/provider/organization_resource.go | 4 ++-- internal/provider/organization_resource_test.go | 2 -- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/resources/organization.md b/docs/resources/organization.md index 0b4b817..a5e2402 100644 --- a/docs/resources/organization.md +++ b/docs/resources/organization.md @@ -28,3 +28,12 @@ An organization on the Coder deployment ### Read-Only - `id` (String) Organization ID + +## Import + +Import is supported using the following syntax: + +```shell +# Organizations can be imported by their name +terraform import coderd_organization.our_org our_org +``` diff --git a/examples/resources/coderd_organization/import.sh b/examples/resources/coderd_organization/import.sh index cd93ce2..882dce6 100644 --- a/examples/resources/coderd_organization/import.sh +++ b/examples/resources/coderd_organization/import.sh @@ -1 +1,2 @@ +# Organizations can be imported by their name terraform import coderd_organization.our_org our_org diff --git a/internal/provider/organization_resource.go b/internal/provider/organization_resource.go index 05bcc9a..9b5fc49 100644 --- a/internal/provider/organization_resource.go +++ b/internal/provider/organization_resource.go @@ -114,8 +114,8 @@ func (r *OrganizationResource) Read(ctx context.Context, req resource.ReadReques return } - orgID := data.ID.ValueUUID() - org, err := r.Client.Organization(ctx, orgID) + orgName := data.Name.ValueString() + org, err := r.Client.OrganizationByName(ctx, orgName) if err != nil { resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization by ID, got error: %s", err)) return diff --git a/internal/provider/organization_resource_test.go b/internal/provider/organization_resource_test.go index 4dce520..6003103 100644 --- a/internal/provider/organization_resource_test.go +++ b/internal/provider/organization_resource_test.go @@ -40,8 +40,6 @@ func TestAccOrganizationResource(t *testing.T) { cfg2.Name = ptr.Ref("example-org-new") cfg2.DisplayName = ptr.Ref("Example Organization New") - cfg3 := cfg2 - t.Run("CreateImportUpdateReadOk", func(t *testing.T) { resource.Test(t, resource.TestCase{ IsUnitTest: true, From f3ff5fb699a85409d837f28b3d4890515b180984 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Tue, 12 Nov 2024 21:52:01 +0000 Subject: [PATCH 13/17] how about --- internal/provider/organization_resource.go | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/internal/provider/organization_resource.go b/internal/provider/organization_resource.go index 9b5fc49..f3dd8c9 100644 --- a/internal/provider/organization_resource.go +++ b/internal/provider/organization_resource.go @@ -114,11 +114,23 @@ func (r *OrganizationResource) Read(ctx context.Context, req resource.ReadReques return } - orgName := data.Name.ValueString() - org, err := r.Client.OrganizationByName(ctx, orgName) - if err != nil { - resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization by ID, got error: %s", err)) - return + var org codersdk.Organization + var err error + if data.ID.IsNull() { + orgName := data.Name.ValueString() + org, err = r.Client.OrganizationByName(ctx, orgName) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization by name, got error: %s", err)) + return + } + data.ID = UUIDValue(org.ID) + } else { + orgID := data.ID.ValueUUID() + org, err = r.Client.Organization(ctx, orgID) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization by ID, got error: %s", err)) + return + } } // We've fetched the organization ID from state, and the latest values for From a716a58ed3241d8a5686e3d321ebd8544bd98f4e Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 14 Nov 2024 18:53:07 +0000 Subject: [PATCH 14/17] add log fields --- internal/provider/organization_resource.go | 40 ++++++++++++++++------ 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/internal/provider/organization_resource.go b/internal/provider/organization_resource.go index f3dd8c9..1575ce3 100644 --- a/internal/provider/organization_resource.go +++ b/internal/provider/organization_resource.go @@ -153,7 +153,13 @@ func (r *OrganizationResource) Create(ctx context.Context, req resource.CreateRe return } - tflog.Trace(ctx, "creating organization") + tflog.Trace(ctx, "creating organization", map[string]any{ + "id": data.ID.ValueUUID(), + "name": data.Name.ValueString(), + "display_name": data.DisplayName.ValueString(), + "description": data.Description.ValueString(), + "icon": data.Icon.ValueString(), + }) org, err := r.Client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ Name: data.Name.ValueString(), DisplayName: data.DisplayName.ValueString(), @@ -165,7 +171,11 @@ func (r *OrganizationResource) Create(ctx context.Context, req resource.CreateRe return } tflog.Trace(ctx, "successfully created organization", map[string]any{ - "id": org.ID, + "id": org.ID, + "name": org.Name, + "display_name": org.DisplayName, + "description": org.Description, + "icon": org.Icon, }) // Fill in `ID` since it must be "computed". data.ID = UUIDValue(org.ID) @@ -190,12 +200,12 @@ func (r *OrganizationResource) Update(ctx context.Context, req resource.UpdateRe // Update the organization metadata tflog.Trace(ctx, "updating organization", map[string]any{ "id": orgID, - "new_name": data.Name, - "new_display_name": data.DisplayName, - "new_description": data.Description, - "new_icon": data.Icon, + "new_name": data.Name.ValueString(), + "new_display_name": data.DisplayName.ValueString(), + "new_description": data.Description.ValueString(), + "new_icon": data.Icon.ValueString(), }) - _, err := r.Client.UpdateOrganization(ctx, orgID.String(), codersdk.UpdateOrganizationRequest{ + org, err := r.Client.UpdateOrganization(ctx, orgID.String(), codersdk.UpdateOrganizationRequest{ Name: data.Name.ValueString(), DisplayName: data.DisplayName.ValueString(), Description: data.Description.ValueStringPointer(), @@ -205,7 +215,13 @@ func (r *OrganizationResource) Update(ctx context.Context, req resource.UpdateRe resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update organization %s, got error: %s", orgID, err)) return } - tflog.Trace(ctx, "successfully updated organization") + tflog.Trace(ctx, "successfully updated organization", map[string]any{ + "id": orgID, + "name": org.Name, + "display_name": org.DisplayName, + "description": org.Description, + "icon": org.Icon, + }) // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) @@ -222,14 +238,18 @@ func (r *OrganizationResource) Delete(ctx context.Context, req resource.DeleteRe orgID := data.ID.ValueUUID() tflog.Trace(ctx, "deleting organization", map[string]any{ - "id": orgID, + "id": orgID, + "name": data.Name.ValueString(), }) err := r.Client.DeleteOrganization(ctx, orgID.String()) if err != nil { resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete organization %s, got error: %s", orgID, err)) return } - tflog.Trace(ctx, "successfully deleted organization") + tflog.Trace(ctx, "successfully deleted organization", map[string]any{ + "id": orgID, + "name": data.Name.ValueString(), + }) // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) From c2e661e908c921d03f724bcf25b7d513b6ce08d9 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 14 Nov 2024 19:07:11 +0000 Subject: [PATCH 15/17] fix import --- internal/provider/organization_resource_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/provider/organization_resource_test.go b/internal/provider/organization_resource_test.go index 6003103..19e4822 100644 --- a/internal/provider/organization_resource_test.go +++ b/internal/provider/organization_resource_test.go @@ -57,10 +57,11 @@ func TestAccOrganizationResource(t *testing.T) { }, // Import { - Config: cfg1.String(t), + Config: cfg2.String(t), ResourceName: "coderd_organization.test", ImportState: true, ImportStateVerify: true, + ImportStateId: "name", }, // Update and Read { From 0a2f33011b9062fe7344f7c4ba0d36f4ec40baa4 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 14 Nov 2024 20:09:56 +0000 Subject: [PATCH 16/17] ? --- internal/provider/organization_resource_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/provider/organization_resource_test.go b/internal/provider/organization_resource_test.go index 19e4822..55d8d92 100644 --- a/internal/provider/organization_resource_test.go +++ b/internal/provider/organization_resource_test.go @@ -61,7 +61,7 @@ func TestAccOrganizationResource(t *testing.T) { ResourceName: "coderd_organization.test", ImportState: true, ImportStateVerify: true, - ImportStateId: "name", + ImportStateId: *cfg2.Name, }, // Update and Read { From 435cb20f5d4de22b6b738135e1921bfbd308f1ef Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 14 Nov 2024 20:14:58 +0000 Subject: [PATCH 17/17] sure --- internal/provider/organization_resource_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/provider/organization_resource_test.go b/internal/provider/organization_resource_test.go index 55d8d92..b633265 100644 --- a/internal/provider/organization_resource_test.go +++ b/internal/provider/organization_resource_test.go @@ -57,11 +57,11 @@ func TestAccOrganizationResource(t *testing.T) { }, // Import { - Config: cfg2.String(t), + Config: cfg1.String(t), ResourceName: "coderd_organization.test", ImportState: true, ImportStateVerify: true, - ImportStateId: *cfg2.Name, + ImportStateId: *cfg1.Name, }, // Update and Read {