From 4aa400fb686e3496115b65c0dbadd6e2e7323f98 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 7 Nov 2024 22:33:21 +0000 Subject: [PATCH] maybe --- internal/{provider => }/logger.go | 20 ++-- internal/provider/group_data_source.go | 41 ++++---- internal/provider/group_data_source_test.go | 3 +- internal/provider/group_resource.go | 42 ++++----- internal/provider/group_resource_test.go | 2 +- internal/provider/license_resource_test.go | 2 +- internal/provider/organization_data_source.go | 23 ++--- .../provider/organization_data_source_test.go | 2 +- internal/provider/organization_resource.go | 94 +++++++++++++++++++ internal/provider/provider.go | 4 +- internal/provider/template_data_source.go | 45 ++++----- .../provider/template_data_source_test.go | 2 +- internal/provider/template_resource.go | 91 +++++++++--------- internal/provider/template_resource_test.go | 6 +- internal/provider/user_data_source.go | 17 ++-- internal/provider/user_data_source_test.go | 2 +- internal/provider/user_resource.go | 14 +-- internal/provider/user_resource_test.go | 2 +- internal/provider/workspace_proxy_resource.go | 2 +- .../provider/workspace_proxy_resource_test.go | 2 +- internal/{provider => }/util.go | 41 ++++---- internal/{provider => }/uuid.go | 6 +- internal/{provider => }/uuid_internal_test.go | 2 +- 23 files changed, 287 insertions(+), 178 deletions(-) rename internal/{provider => }/logger.go (63%) create mode 100644 internal/provider/organization_resource.go rename internal/{provider => }/util.go (78%) rename internal/{provider => }/uuid.go (94%) rename internal/{provider => }/uuid_internal_test.go (99%) diff --git a/internal/provider/logger.go b/internal/logger.go similarity index 63% rename from internal/provider/logger.go rename to internal/logger.go index a17fc65..59706fc 100644 --- a/internal/provider/logger.go +++ b/internal/logger.go @@ -1,4 +1,4 @@ -package provider +package internal import ( "context" @@ -7,19 +7,19 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" ) -var _ slog.Sink = &tfLogSink{} +var _ slog.Sink = &tflogSink{} -type tfLogSink struct { - tfCtx context.Context +type tflogSink struct { + ctx context.Context } -func newTFLogSink(tfCtx context.Context) *tfLogSink { - return &tfLogSink{ - tfCtx: tfCtx, +func NewLogSink(ctx context.Context) slog.Sink { + return &tflogSink{ + ctx: ctx, } } -func (s *tfLogSink) LogEntry(ctx context.Context, e slog.SinkEntry) { +func (s *tflogSink) LogEntry(ctx context.Context, e slog.SinkEntry) { var logFn func(ctx context.Context, msg string, additionalFields ...map[string]interface{}) switch e.Level { case slog.LevelDebug: @@ -31,10 +31,10 @@ func (s *tfLogSink) LogEntry(ctx context.Context, e slog.SinkEntry) { default: logFn = tflog.Error } - logFn(s.tfCtx, e.Message, mapToFields(e.Fields)) + logFn(s.ctx, e.Message, mapToFields(e.Fields)) } -func (s *tfLogSink) Sync() {} +func (s *tflogSink) Sync() {} func mapToFields(m slog.Map) map[string]interface{} { fields := make(map[string]interface{}, len(m)) diff --git a/internal/provider/group_data_source.go b/internal/provider/group_data_source.go index 007abaa..0412e42 100644 --- a/internal/provider/group_data_source.go +++ b/internal/provider/group_data_source.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/coder/coder/v2/codersdk" + "github.com/coder/terraform-provider-coderd/internal" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" @@ -28,9 +29,9 @@ type GroupDataSource struct { // GroupDataSourceModel describes the data source data model. type GroupDataSourceModel struct { // ID or name and organization ID must be set - ID UUID `tfsdk:"id"` - Name types.String `tfsdk:"name"` - OrganizationID UUID `tfsdk:"organization_id"` + ID internal.UUID `tfsdk:"id"` + Name types.String `tfsdk:"name"` + OrganizationID internal.UUID `tfsdk:"organization_id"` DisplayName types.String `tfsdk:"display_name"` AvatarURL types.String `tfsdk:"avatar_url"` @@ -40,14 +41,14 @@ type GroupDataSourceModel struct { } type Member struct { - ID UUID `tfsdk:"id"` - Username types.String `tfsdk:"username"` - Email types.String `tfsdk:"email"` - CreatedAt types.Int64 `tfsdk:"created_at"` - LastSeenAt types.Int64 `tfsdk:"last_seen_at"` - Status types.String `tfsdk:"status"` - LoginType types.String `tfsdk:"login_type"` - ThemePreference types.String `tfsdk:"theme_preference"` + ID internal.UUID `tfsdk:"id"` + Username types.String `tfsdk:"username"` + Email types.String `tfsdk:"email"` + CreatedAt types.Int64 `tfsdk:"created_at"` + LastSeenAt types.Int64 `tfsdk:"last_seen_at"` + Status types.String `tfsdk:"status"` + LoginType types.String `tfsdk:"login_type"` + ThemePreference types.String `tfsdk:"theme_preference"` } func (d *GroupDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { @@ -63,7 +64,7 @@ func (d *GroupDataSource) Schema(ctx context.Context, req datasource.SchemaReque MarkdownDescription: "The ID of the group to retrieve. This field will be populated if a name and organization ID is supplied.", Optional: true, Computed: true, - CustomType: UUIDType, + CustomType: internal.UUIDType, Validators: []validator.String{ stringvalidator.AtLeastOneOf(path.Expressions{ path.MatchRoot("name"), @@ -78,7 +79,7 @@ func (d *GroupDataSource) Schema(ctx context.Context, req datasource.SchemaReque }, "organization_id": schema.StringAttribute{ MarkdownDescription: "The organization ID that the group belongs to. This field will be populated if an ID is supplied. Defaults to the provider default organization ID.", - CustomType: UUIDType, + CustomType: internal.UUIDType, Optional: true, Computed: true, }, @@ -102,7 +103,7 @@ func (d *GroupDataSource) Schema(ctx context.Context, req datasource.SchemaReque NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - CustomType: UUIDType, + CustomType: internal.UUIDType, Computed: true, }, "username": schema.StringAttribute{ @@ -176,7 +177,7 @@ func (d *GroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, client := d.data.Client if data.OrganizationID.IsNull() { - data.OrganizationID = UUIDValue(d.data.DefaultOrganizationID) + data.OrganizationID = internal.UUIDValue(d.data.DefaultOrganizationID) } var ( @@ -187,7 +188,7 @@ func (d *GroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, groupID := data.ID.ValueUUID() group, err = client.Group(ctx, groupID) if err != nil { - if isNotFound(err) { + if internal.IsNotFound(err) { resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("Group with ID %s not found. Marking as deleted.", groupID.String())) resp.State.RemoveResource(ctx) return @@ -196,11 +197,11 @@ func (d *GroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, return } data.Name = types.StringValue(group.Name) - data.OrganizationID = UUIDValue(group.OrganizationID) + data.OrganizationID = internal.UUIDValue(group.OrganizationID) } else { group, err = client.GroupByOrgAndName(ctx, data.OrganizationID.ValueUUID(), data.Name.ValueString()) if err != nil { - if isNotFound(err) { + if internal.IsNotFound(err) { resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("Group with name %s not found in organization with ID %s. Marking as deleted.", data.Name.ValueString(), data.OrganizationID.ValueString())) resp.State.RemoveResource(ctx) return @@ -208,7 +209,7 @@ func (d *GroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp.Diagnostics.AddError("Failed to get group by name and org ID", err.Error()) return } - data.ID = UUIDValue(group.ID) + data.ID = internal.UUIDValue(group.ID) } data.DisplayName = types.StringValue(group.DisplayName) @@ -217,7 +218,7 @@ func (d *GroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, members := make([]Member, 0, len(group.Members)) for _, member := range group.Members { members = append(members, Member{ - ID: UUIDValue(member.ID), + ID: internal.UUIDValue(member.ID), Username: types.StringValue(member.Username), Email: types.StringValue(member.Email), CreatedAt: types.Int64Value(member.CreatedAt.Unix()), diff --git a/internal/provider/group_data_source_test.go b/internal/provider/group_data_source_test.go index 5a081af..7ac42c2 100644 --- a/internal/provider/group_data_source_test.go +++ b/internal/provider/group_data_source_test.go @@ -11,6 +11,7 @@ import ( "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" "github.com/coder/terraform-provider-coderd/integration" + "github.com/coder/terraform-provider-coderd/internal" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/stretchr/testify/require" ) @@ -188,7 +189,7 @@ data "coderd_group" "test" { ` funcMap := template.FuncMap{ - "orNull": PrintOrNull, + "orNull": internal.PrintOrNull, } buf := strings.Builder{} diff --git a/internal/provider/group_resource.go b/internal/provider/group_resource.go index fa370a0..954034d 100644 --- a/internal/provider/group_resource.go +++ b/internal/provider/group_resource.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/coder/coder/v2/codersdk" + "github.com/coder/terraform-provider-coderd/internal" "github.com/coder/terraform-provider-coderd/internal/codersdkvalidator" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -37,14 +38,14 @@ type GroupResource struct { // GroupResourceModel describes the resource data model. type GroupResourceModel struct { - ID UUID `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 UUID `tfsdk:"organization_id"` - Members types.Set `tfsdk:"members"` + ID internal.UUID `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 internal.UUID `tfsdk:"organization_id"` + Members types.Set `tfsdk:"members"` } func CheckGroupEntitlements(ctx context.Context, features map[codersdk.FeatureName]codersdk.Feature) (diags diag.Diagnostics) { @@ -67,7 +68,7 @@ func (r *GroupResource) Schema(ctx context.Context, req resource.SchemaRequest, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ MarkdownDescription: "Group ID.", - CustomType: UUIDType, + CustomType: internal.UUIDType, Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -104,7 +105,7 @@ func (r *GroupResource) Schema(ctx context.Context, req resource.SchemaRequest, }, "organization_id": schema.StringAttribute{ MarkdownDescription: "The organization ID that the group belongs to. Defaults to the provider default organization ID.", - CustomType: UUIDType, + CustomType: internal.UUIDType, Optional: true, Computed: true, PlanModifiers: []planmodifier.String{ @@ -113,7 +114,7 @@ func (r *GroupResource) Schema(ctx context.Context, req resource.SchemaRequest, }, "members": schema.SetAttribute{ MarkdownDescription: "Members of the group, by ID. If `null`, members will not be added or removed by Terraform. To have a group resource with unmanaged members, but be able to read the members in Terraform, use `data.coderd_group`", - ElementType: UUIDType, + ElementType: internal.UUIDType, Optional: true, }, }, @@ -141,9 +142,8 @@ func (r *GroupResource) Configure(ctx context.Context, req resource.ConfigureReq } func (r *GroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var data GroupResourceModel - // Read Terraform plan data into the model + var data GroupResourceModel resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) if resp.Diagnostics.HasError() { @@ -158,7 +158,7 @@ func (r *GroupResource) Create(ctx context.Context, req resource.CreateRequest, client := r.data.Client if data.OrganizationID.IsUnknown() { - data.OrganizationID = UUIDValue(r.data.DefaultOrganizationID) + data.OrganizationID = internal.UUIDValue(r.data.DefaultOrganizationID) } orgID := data.OrganizationID.ValueUUID() @@ -177,7 +177,7 @@ func (r *GroupResource) Create(ctx context.Context, req resource.CreateRequest, tflog.Info(ctx, "successfully created group", map[string]any{ "id": group.ID.String(), }) - data.ID = UUIDValue(group.ID) + data.ID = internal.UUIDValue(group.ID) data.DisplayName = types.StringValue(group.DisplayName) tflog.Info(ctx, "setting group members") @@ -217,7 +217,7 @@ func (r *GroupResource) Read(ctx context.Context, req resource.ReadRequest, resp group, err := client.Group(ctx, groupID) if err != nil { - if isNotFound(err) { + if internal.IsNotFound(err) { resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("Group with ID %s not found. Marking as deleted.", groupID.String())) resp.State.RemoveResource(ctx) return @@ -230,13 +230,13 @@ func (r *GroupResource) Read(ctx context.Context, req resource.ReadRequest, resp data.DisplayName = types.StringValue(group.DisplayName) data.AvatarURL = types.StringValue(group.AvatarURL) data.QuotaAllowance = types.Int32Value(int32(group.QuotaAllowance)) - data.OrganizationID = UUIDValue(group.OrganizationID) + data.OrganizationID = internal.UUIDValue(group.OrganizationID) if !data.Members.IsNull() { members := make([]attr.Value, 0, len(group.Members)) for _, member := range group.Members { - members = append(members, UUIDValue(member.ID)) + members = append(members, internal.UUIDValue(member.ID)) } - data.Members = types.SetValueMust(UUIDType, members) + data.Members = types.SetValueMust(internal.UUIDType, members) } // Save updated data into Terraform state @@ -255,7 +255,7 @@ func (r *GroupResource) Update(ctx context.Context, req resource.UpdateRequest, client := r.data.Client if data.OrganizationID.IsUnknown() { - data.OrganizationID = UUIDValue(r.data.DefaultOrganizationID) + data.OrganizationID = internal.UUIDValue(r.data.DefaultOrganizationID) } groupID := data.ID.ValueUUID() @@ -267,7 +267,7 @@ func (r *GroupResource) Update(ctx context.Context, req resource.UpdateRequest, var add []string var remove []string if !data.Members.IsNull() { - var plannedMembers []UUID + var plannedMembers []internal.UUID resp.Diagnostics.Append( data.Members.ElementsAs(ctx, &plannedMembers, false)..., ) diff --git a/internal/provider/group_resource_test.go b/internal/provider/group_resource_test.go index 865851a..aeec502 100644 --- a/internal/provider/group_resource_test.go +++ b/internal/provider/group_resource_test.go @@ -195,7 +195,7 @@ resource "coderd_group" "test" { } ` funcMap := template.FuncMap{ - "orNull": PrintOrNull, + "orNull": printOrNull, } buf := strings.Builder{} diff --git a/internal/provider/license_resource_test.go b/internal/provider/license_resource_test.go index e2d13d6..a7f30f3 100644 --- a/internal/provider/license_resource_test.go +++ b/internal/provider/license_resource_test.go @@ -61,7 +61,7 @@ resource "coderd_license" "test" { } ` funcMap := template.FuncMap{ - "orNull": PrintOrNull, + "orNull": printOrNull, } buf := strings.Builder{} diff --git a/internal/provider/organization_data_source.go b/internal/provider/organization_data_source.go index 835a2ed..4cc7fcb 100644 --- a/internal/provider/organization_data_source.go +++ b/internal/provider/organization_data_source.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/coder/coder/v2/codersdk" + "github.com/coder/terraform-provider-coderd/internal" "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" @@ -29,9 +30,9 @@ type OrganizationDataSource struct { // OrganizationDataSourceModel describes the data source data model. type OrganizationDataSourceModel struct { // Exactly one of ID, IsDefault, or Name must be set. - ID UUID `tfsdk:"id"` - IsDefault types.Bool `tfsdk:"is_default"` - Name types.String `tfsdk:"name"` + ID internal.UUID `tfsdk:"id"` + IsDefault types.Bool `tfsdk:"is_default"` + Name types.String `tfsdk:"name"` CreatedAt types.Int64 `tfsdk:"created_at"` UpdatedAt types.Int64 `tfsdk:"updated_at"` @@ -55,7 +56,7 @@ This data source is only compatible with Coder version [2.13.0](https://github.c Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ MarkdownDescription: "The ID of the organization to retrieve. This field will be populated if the organization is found by name, or if the default organization is requested.", - CustomType: UUIDType, + CustomType: internal.UUIDType, Optional: true, Computed: true, }, @@ -81,7 +82,7 @@ This data source is only compatible with Coder version [2.13.0](https://github.c "members": schema.SetAttribute{ MarkdownDescription: "Members of the organization, by ID", Computed: true, - ElementType: UUIDType, + ElementType: internal.UUIDType, }, }, } @@ -127,7 +128,7 @@ func (d *OrganizationDataSource) Read(ctx context.Context, req datasource.ReadRe orgID := data.ID.ValueUUID() org, err = client.Organization(ctx, orgID) if err != nil { - if isNotFound(err) { + if internal.IsNotFound(err) { resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("Organization with ID %s not found. Marking as deleted.", data.ID.ValueString())) resp.State.RemoveResource(ctx) return @@ -142,7 +143,7 @@ func (d *OrganizationDataSource) Read(ctx context.Context, req datasource.ReadRe } else if data.IsDefault.ValueBool() { // Get Default org, err = client.OrganizationByName(ctx, "default") if err != nil { - if isNotFound(err) { + if internal.IsNotFound(err) { resp.Diagnostics.AddWarning("Client Warning", "Default organization not found. Marking as deleted.") resp.State.RemoveResource(ctx) return @@ -157,7 +158,7 @@ func (d *OrganizationDataSource) Read(ctx context.Context, req datasource.ReadRe } else { // By Name org, err = client.OrganizationByName(ctx, data.Name.ValueString()) if err != nil { - if isNotFound(err) { + if internal.IsNotFound(err) { resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("Organization with name %s not found. Marking as deleted.", data.Name)) resp.State.RemoveResource(ctx) return @@ -170,7 +171,7 @@ func (d *OrganizationDataSource) Read(ctx context.Context, req datasource.ReadRe return } } - data.ID = UUIDValue(org.ID) + data.ID = internal.UUIDValue(org.ID) data.Name = types.StringValue(org.Name) data.IsDefault = types.BoolValue(org.IsDefault) data.CreatedAt = types.Int64Value(org.CreatedAt.Unix()) @@ -182,9 +183,9 @@ func (d *OrganizationDataSource) Read(ctx context.Context, req datasource.ReadRe } memberIDs := make([]attr.Value, 0, len(members)) for _, member := range members { - memberIDs = append(memberIDs, UUIDValue(member.UserID)) + memberIDs = append(memberIDs, internal.UUIDValue(member.UserID)) } - data.Members = types.SetValueMust(UUIDType, memberIDs) + data.Members = types.SetValueMust(internal.UUIDType, memberIDs) // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) diff --git a/internal/provider/organization_data_source_test.go b/internal/provider/organization_data_source_test.go index c7bb982..0213704 100644 --- a/internal/provider/organization_data_source_test.go +++ b/internal/provider/organization_data_source_test.go @@ -134,7 +134,7 @@ data "coderd_organization" "test" { ` funcMap := template.FuncMap{ - "orNull": PrintOrNull, + "orNull": printOrNull, } buf := strings.Builder{} 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) { +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index bfeea5e..a5a81cf 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/coder/coder/v2/codersdk" + "github.com/coder/terraform-provider-coderd/internal" ) // Ensure CoderdProvider satisfies various provider interfaces. @@ -42,7 +43,7 @@ type CoderdProviderModel struct { URL types.String `tfsdk:"url"` Token types.String `tfsdk:"token"` - DefaultOrganizationID UUID `tfsdk:"default_organization_id"` + DefaultOrganizationID internal.UUID `tfsdk:"default_organization_id"` } func (p *CoderdProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) { @@ -78,7 +79,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() { diff --git a/internal/provider/template_data_source.go b/internal/provider/template_data_source.go index 3a5f59a..4d38547 100644 --- a/internal/provider/template_data_source.go +++ b/internal/provider/template_data_source.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/coder/coder/v2/codersdk" + "github.com/coder/terraform-provider-coderd/internal" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -30,18 +31,18 @@ type TemplateDataSource struct { // TemplateDataSourceModel describes the data source data model. type TemplateDataSourceModel struct { // ((Organization and Name) or ID) must be set - OrganizationID UUID `tfsdk:"organization_id"` - ID UUID `tfsdk:"id"` - Name types.String `tfsdk:"name"` + OrganizationID internal.UUID `tfsdk:"organization_id"` + ID internal.UUID `tfsdk:"id"` + Name types.String `tfsdk:"name"` DisplayName types.String `tfsdk:"display_name"` // TODO: Provisioner - Description types.String `tfsdk:"description"` - ActiveVersionID UUID `tfsdk:"active_version_id"` - ActiveUserCount types.Int64 `tfsdk:"active_user_count"` - Deprecated types.Bool `tfsdk:"deprecated"` - DeprecationMessage types.String `tfsdk:"deprecation_message"` - Icon types.String `tfsdk:"icon"` + Description types.String `tfsdk:"description"` + ActiveVersionID internal.UUID `tfsdk:"active_version_id"` + ActiveUserCount types.Int64 `tfsdk:"active_user_count"` + Deprecated types.Bool `tfsdk:"deprecated"` + DeprecationMessage types.String `tfsdk:"deprecation_message"` + Icon types.String `tfsdk:"icon"` DefaultTTLMillis types.Int64 `tfsdk:"default_ttl_ms"` ActivityBumpMillis types.Int64 `tfsdk:"activity_bump_ms"` @@ -59,9 +60,9 @@ type TemplateDataSourceModel struct { RequireActiveVersion types.Bool `tfsdk:"require_active_version"` MaxPortShareLevel types.String `tfsdk:"max_port_share_level"` - CreatedByUserID UUID `tfsdk:"created_by_user_id"` - CreatedAt types.Int64 `tfsdk:"created_at"` // Unix timestamp - UpdatedAt types.Int64 `tfsdk:"updated_at"` // Unix timestamp + CreatedByUserID internal.UUID `tfsdk:"created_by_user_id"` + CreatedAt types.Int64 `tfsdk:"created_at"` // Unix timestamp + UpdatedAt types.Int64 `tfsdk:"updated_at"` // Unix timestamp ACL types.Object `tfsdk:"acl"` } @@ -77,13 +78,13 @@ func (d *TemplateDataSource) Schema(ctx context.Context, req datasource.SchemaRe Attributes: map[string]schema.Attribute{ "organization_id": schema.StringAttribute{ MarkdownDescription: "ID of the organization the template is associated with. This field will be populated if an ID is supplied. Defaults to the provider default organization ID.", - CustomType: UUIDType, + CustomType: internal.UUIDType, Optional: true, Computed: true, }, "id": schema.StringAttribute{ MarkdownDescription: "The ID of the template to retrieve. This field will be populated if a template name is supplied.", - CustomType: UUIDType, + CustomType: internal.UUIDType, Optional: true, Computed: true, Validators: []validator.String{ @@ -108,7 +109,7 @@ func (d *TemplateDataSource) Schema(ctx context.Context, req datasource.SchemaRe }, "active_version_id": schema.StringAttribute{ MarkdownDescription: "ID of the active version of the template.", - CustomType: UUIDType, + CustomType: internal.UUIDType, Computed: true, }, "active_user_count": schema.Int64Attribute{ @@ -190,7 +191,7 @@ func (d *TemplateDataSource) Schema(ctx context.Context, req datasource.SchemaRe }, "created_by_user_id": schema.StringAttribute{ MarkdownDescription: "ID of the user who created the template.", - CustomType: UUIDType, + CustomType: internal.UUIDType, Computed: true, }, "created_at": schema.Int64Attribute{ @@ -253,7 +254,7 @@ func (d *TemplateDataSource) Read(ctx context.Context, req datasource.ReadReques template, err = client.Template(ctx, data.ID.ValueUUID()) } else { if data.OrganizationID.ValueUUID() == uuid.Nil { - data.OrganizationID = UUIDValue(d.data.DefaultOrganizationID) + data.OrganizationID = internal.UUIDValue(d.data.DefaultOrganizationID) } if data.OrganizationID.ValueUUID() == uuid.Nil { resp.Diagnostics.AddError("Client Error", "name requires organization_id to be set") @@ -262,7 +263,7 @@ func (d *TemplateDataSource) Read(ctx context.Context, req datasource.ReadReques template, err = client.TemplateByName(ctx, data.OrganizationID.ValueUUID(), data.Name.ValueString()) } if err != nil { - if isNotFound(err) { + if internal.IsNotFound(err) { resp.Diagnostics.AddWarning("Client Warning", "Template not found. Marking as deleted.") resp.State.RemoveResource(ctx) return @@ -310,12 +311,12 @@ func (d *TemplateDataSource) Read(ctx context.Context, req datasource.ReadReques data.ACL = aclObj data.AutostartPermittedDaysOfWeek = types.SetValueMust(types.StringType, autoStartDays) data.AutostopRequirement = asrObj - data.OrganizationID = UUIDValue(template.OrganizationID) - data.ID = UUIDValue(template.ID) + data.OrganizationID = internal.UUIDValue(template.OrganizationID) + data.ID = internal.UUIDValue(template.ID) data.Name = types.StringValue(template.Name) data.DisplayName = types.StringValue(template.DisplayName) data.Description = types.StringValue(template.Description) - data.ActiveVersionID = UUIDValue(template.ActiveVersionID) + data.ActiveVersionID = internal.UUIDValue(template.ActiveVersionID) data.ActiveUserCount = types.Int64Value(int64(template.ActiveUserCount)) data.Deprecated = types.BoolValue(template.Deprecated) data.DeprecationMessage = types.StringValue(template.DeprecationMessage) @@ -330,7 +331,7 @@ func (d *TemplateDataSource) Read(ctx context.Context, req datasource.ReadReques data.TimeTilDormantAutoDeleteMillis = types.Int64Value(template.TimeTilDormantAutoDeleteMillis) data.RequireActiveVersion = types.BoolValue(template.RequireActiveVersion) data.MaxPortShareLevel = types.StringValue(string(template.MaxPortShareLevel)) - data.CreatedByUserID = UUIDValue(template.CreatedByID) + data.CreatedByUserID = internal.UUIDValue(template.CreatedByID) data.CreatedAt = types.Int64Value(template.CreatedAt.Unix()) data.UpdatedAt = types.Int64Value(template.UpdatedAt.Unix()) diff --git a/internal/provider/template_data_source_test.go b/internal/provider/template_data_source_test.go index f6759b3..b81cab3 100644 --- a/internal/provider/template_data_source_test.go +++ b/internal/provider/template_data_source_test.go @@ -251,7 +251,7 @@ data "coderd_template" "test" { }` funcMap := template.FuncMap{ - "orNull": PrintOrNull, + "orNull": printOrNull, } buf := strings.Builder{} diff --git a/internal/provider/template_resource.go b/internal/provider/template_resource.go index a5834db..b25b02a 100644 --- a/internal/provider/template_resource.go +++ b/internal/provider/template_resource.go @@ -13,6 +13,7 @@ import ( "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/provisionersdk" + "github.com/coder/terraform-provider-coderd/internal" "github.com/coder/terraform-provider-coderd/internal/codersdkvalidator" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" @@ -52,26 +53,26 @@ type TemplateResource struct { // TemplateResourceModel describes the resource data model. type TemplateResourceModel struct { - ID UUID `tfsdk:"id"` - - Name types.String `tfsdk:"name"` - DisplayName types.String `tfsdk:"display_name"` - Description types.String `tfsdk:"description"` - OrganizationID UUID `tfsdk:"organization_id"` - Icon types.String `tfsdk:"icon"` - DefaultTTLMillis types.Int64 `tfsdk:"default_ttl_ms"` - ActivityBumpMillis types.Int64 `tfsdk:"activity_bump_ms"` - AutostopRequirement types.Object `tfsdk:"auto_stop_requirement"` - AutostartPermittedDaysOfWeek types.Set `tfsdk:"auto_start_permitted_days_of_week"` - AllowUserCancelWorkspaceJobs types.Bool `tfsdk:"allow_user_cancel_workspace_jobs"` - AllowUserAutostart types.Bool `tfsdk:"allow_user_auto_start"` - AllowUserAutostop types.Bool `tfsdk:"allow_user_auto_stop"` - FailureTTLMillis types.Int64 `tfsdk:"failure_ttl_ms"` - TimeTilDormantMillis types.Int64 `tfsdk:"time_til_dormant_ms"` - TimeTilDormantAutoDeleteMillis types.Int64 `tfsdk:"time_til_dormant_autodelete_ms"` - RequireActiveVersion types.Bool `tfsdk:"require_active_version"` - DeprecationMessage types.String `tfsdk:"deprecation_message"` - MaxPortShareLevel types.String `tfsdk:"max_port_share_level"` + ID internal.UUID `tfsdk:"id"` + + Name types.String `tfsdk:"name"` + DisplayName types.String `tfsdk:"display_name"` + Description types.String `tfsdk:"description"` + OrganizationID internal.UUID `tfsdk:"organization_id"` + Icon types.String `tfsdk:"icon"` + DefaultTTLMillis types.Int64 `tfsdk:"default_ttl_ms"` + ActivityBumpMillis types.Int64 `tfsdk:"activity_bump_ms"` + AutostopRequirement types.Object `tfsdk:"auto_stop_requirement"` + AutostartPermittedDaysOfWeek types.Set `tfsdk:"auto_start_permitted_days_of_week"` + AllowUserCancelWorkspaceJobs types.Bool `tfsdk:"allow_user_cancel_workspace_jobs"` + AllowUserAutostart types.Bool `tfsdk:"allow_user_auto_start"` + AllowUserAutostop types.Bool `tfsdk:"allow_user_auto_stop"` + FailureTTLMillis types.Int64 `tfsdk:"failure_ttl_ms"` + TimeTilDormantMillis types.Int64 `tfsdk:"time_til_dormant_ms"` + TimeTilDormantAutoDeleteMillis types.Int64 `tfsdk:"time_til_dormant_autodelete_ms"` + RequireActiveVersion types.Bool `tfsdk:"require_active_version"` + DeprecationMessage types.String `tfsdk:"deprecation_message"` + MaxPortShareLevel types.String `tfsdk:"max_port_share_level"` // If null, we are not managing ACL via Terraform (such as for AGPL). ACL types.Object `tfsdk:"acl"` @@ -150,19 +151,19 @@ func (m *TemplateResourceModel) CheckEntitlements(ctx context.Context, features } type TemplateVersion struct { - ID UUID `tfsdk:"id"` - Name types.String `tfsdk:"name"` - Message types.String `tfsdk:"message"` - Directory types.String `tfsdk:"directory"` - DirectoryHash types.String `tfsdk:"directory_hash"` - Active types.Bool `tfsdk:"active"` - TerraformVariables []Variable `tfsdk:"tf_vars"` - ProvisionerTags []Variable `tfsdk:"provisioner_tags"` + ID internal.UUID `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Message types.String `tfsdk:"message"` + Directory types.String `tfsdk:"directory"` + DirectoryHash types.String `tfsdk:"directory_hash"` + Active types.Bool `tfsdk:"active"` + TerraformVariables []Variable `tfsdk:"tf_vars"` + ProvisionerTags []Variable `tfsdk:"provisioner_tags"` } type Versions []TemplateVersion -func (v Versions) ByID(id UUID) *TemplateVersion { +func (v Versions) ByID(id internal.UUID) *TemplateVersion { for _, m := range v { if m.ID.Equal(id) { return &m @@ -249,7 +250,7 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ MarkdownDescription: "The ID of the template.", - CustomType: UUIDType, + CustomType: internal.UUIDType, Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -278,7 +279,7 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques }, "organization_id": schema.StringAttribute{ MarkdownDescription: "The ID of the organization. Defaults to the provider's default organization", - CustomType: UUIDType, + CustomType: internal.UUIDType, Optional: true, Computed: true, PlanModifiers: []planmodifier.String{ @@ -409,7 +410,7 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - CustomType: UUIDType, + CustomType: internal.UUIDType, Computed: true, }, "name": schema.StringAttribute{ @@ -489,7 +490,7 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques } if data.OrganizationID.IsUnknown() { - data.OrganizationID = UUIDValue(r.data.DefaultOrganizationID) + data.OrganizationID = internal.UUIDValue(r.data.DefaultOrganizationID) } if data.DisplayName.IsUnknown() { @@ -563,10 +564,10 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques return } } - data.Versions[idx].ID = UUIDValue(versionResp.ID) + data.Versions[idx].ID = internal.UUIDValue(versionResp.ID) data.Versions[idx].Name = types.StringValue(versionResp.Name) } - data.ID = UUIDValue(templateResp.ID) + data.ID = internal.UUIDValue(templateResp.ID) data.DisplayName = types.StringValue(templateResp.DisplayName) // TODO: Remove this update call once this provider requires a Coder @@ -610,7 +611,7 @@ func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, r template, err := client.Template(ctx, templateID) if err != nil { - if isNotFound(err) { + if internal.IsNotFound(err) { resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("Template with ID %s not found. Marking as deleted.", templateID.String())) resp.State.RemoveResource(ctx) return @@ -681,7 +682,7 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques } if newState.OrganizationID.IsUnknown() { - newState.OrganizationID = UUIDValue(r.data.DefaultOrganizationID) + newState.OrganizationID = internal.UUIDValue(r.data.DefaultOrganizationID) } if newState.DisplayName.IsUnknown() { @@ -760,7 +761,7 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to get template version: %s", err)) return } - newState.Versions[idx].ID = UUIDValue(versionResp.ID) + newState.Versions[idx].ID = internal.UUIDValue(versionResp.ID) newState.Versions[idx].Name = types.StringValue(versionResp.Name) if newState.Versions[idx].Active.ValueBool() { err := markActive(ctx, client, templateID, newState.Versions[idx].ID.ValueUUID()) @@ -957,7 +958,7 @@ func (d *versionsPlanModifier) PlanModifyList(ctx context.Context, req planmodif } for i := range planVersions { - hash, err := computeDirectoryHash(planVersions[i].Directory.ValueString()) + hash, err := internal.DirectoryHash(planVersions[i].Directory.ValueString()) if err != nil { resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to compute directory hash: %s", err)) return @@ -1063,7 +1064,7 @@ func newVersion(ctx context.Context, client *codersdk.Client, req newVersionRequ var logs []codersdk.ProvisionerJobLog directory := req.Version.Directory.ValueString() tflog.Info(ctx, "uploading directory") - uploadResp, err := uploadDirectory(ctx, client, slog.Make(newTFLogSink(ctx)), directory) + uploadResp, err := uploadDirectory(ctx, client, slog.Make(internal.NewLogSink(ctx)), directory) if err != nil { return nil, fmt.Errorf("failed to upload directory: %s", err), logs } @@ -1183,7 +1184,7 @@ func (r *TemplateResourceModel) readResponse(ctx context.Context, template *code r.Name = types.StringValue(template.Name) r.DisplayName = types.StringValue(template.DisplayName) r.Description = types.StringValue(template.Description) - r.OrganizationID = UUIDValue(template.OrganizationID) + r.OrganizationID = internal.UUIDValue(template.OrganizationID) r.Icon = types.StringValue(template.Icon) r.DefaultTTLMillis = types.Int64Value(template.DefaultTTLMillis) r.ActivityBumpMillis = types.Int64Value(template.ActivityBumpMillis) @@ -1364,7 +1365,7 @@ func (planVersions Versions) reconcileVersionIDs(lv LastVersionsByHash, configVe // Versions whose Terraform configuration has not changed will have known // IDs at this point, so we need to set this manually. if !ok { - planVersions[i].ID = NewUUIDUnknown() + planVersions[i].ID = internal.NewUUIDUnknown() // We might have the old randomly generated name in the plan, // so unless the user has set it to a new one, we need to set it to // unknown so that a new one is generated @@ -1377,7 +1378,7 @@ func (planVersions Versions) reconcileVersionIDs(lv LastVersionsByHash, configVe // If the name is the same, use the existing ID, and remove // it from the previous version candidates if planVersions[i].Name.ValueString() == prev.Name { - planVersions[i].ID = UUIDValue(prev.ID) + planVersions[i].ID = internal.UUIDValue(prev.ID) lv[planVersions[i].DirectoryHash.ValueString()] = append(prevList[:j], prevList[j+1:]...) break } @@ -1390,7 +1391,7 @@ func (planVersions Versions) reconcileVersionIDs(lv LastVersionsByHash, configVe for i := range planVersions { prevList := lv[planVersions[i].DirectoryHash.ValueString()] if len(prevList) > 0 && planVersions[i].ID.IsUnknown() { - planVersions[i].ID = UUIDValue(prevList[0].ID) + planVersions[i].ID = internal.UUIDValue(prevList[0].ID) if planVersions[i].Name.IsUnknown() { planVersions[i].Name = types.StringValue(prevList[0].Name) } @@ -1407,7 +1408,7 @@ func (planVersions Versions) reconcileVersionIDs(lv LastVersionsByHash, configVe continue } if tfVariablesChanged(prevs, &planVersions[i]) { - planVersions[i].ID = NewUUIDUnknown() + planVersions[i].ID = internal.NewUUIDUnknown() // We could always set the name to unknown here, to generate a // random one (this is what the Web UI currently does when // only updating tfvars). diff --git a/internal/provider/template_resource_test.go b/internal/provider/template_resource_test.go index b9d7ae3..7b2235a 100644 --- a/internal/provider/template_resource_test.go +++ b/internal/provider/template_resource_test.go @@ -764,7 +764,7 @@ func (c testAccTemplateACLConfig) String(t *testing.T) string { ` funcMap := template.FuncMap{ - "orNull": PrintOrNull, + "orNull": printOrNull, } buf := strings.Builder{} @@ -794,7 +794,7 @@ func (c testAccAutostopRequirementConfig) String(t *testing.T) string { } ` funcMap := template.FuncMap{ - "orNull": PrintOrNull, + "orNull": printOrNull, } buf := strings.Builder{} @@ -859,7 +859,7 @@ resource "coderd_template" "test" { ` funcMap := template.FuncMap{ - "orNull": PrintOrNull, + "orNull": printOrNull, } buf := strings.Builder{} diff --git a/internal/provider/user_data_source.go b/internal/provider/user_data_source.go index be367ea..cc493ca 100644 --- a/internal/provider/user_data_source.go +++ b/internal/provider/user_data_source.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/coder/coder/v2/codersdk" + "github.com/coder/terraform-provider-coderd/internal" "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" @@ -29,8 +30,8 @@ type UserDataSource struct { // UserDataSourceModel describes the data source data model. type UserDataSourceModel struct { // Username or ID must be set - ID UUID `tfsdk:"id"` - Username types.String `tfsdk:"username"` + ID internal.UUID `tfsdk:"id"` + Username types.String `tfsdk:"username"` Name types.String `tfsdk:"name"` Email types.String `tfsdk:"email"` @@ -55,7 +56,7 @@ func (d *UserDataSource) Schema(ctx context.Context, req datasource.SchemaReques // Validation handled by ConfigValidators Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - CustomType: UUIDType, + CustomType: internal.UUIDType, MarkdownDescription: "The ID of the user to retrieve. This field will be populated if a username is supplied.", Optional: true, }, @@ -91,7 +92,7 @@ func (d *UserDataSource) Schema(ctx context.Context, req datasource.SchemaReques "organization_ids": schema.SetAttribute{ MarkdownDescription: "IDs of organizations the user is associated with.", Computed: true, - ElementType: UUIDType, + ElementType: internal.UUIDType, }, "created_at": schema.Int64Attribute{ MarkdownDescription: "Unix timestamp of when the user was created.", @@ -149,7 +150,7 @@ func (d *UserDataSource) Read(ctx context.Context, req datasource.ReadRequest, r } user, err := client.User(ctx, ident) if err != nil { - if isNotFound(err) { + if internal.IsNotFound(err) { resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("User with identifier %q not found. Marking as deleted.", ident)) resp.State.RemoveResource(ctx) return @@ -169,7 +170,7 @@ func (d *UserDataSource) Read(ctx context.Context, req datasource.ReadRequest, r return } - data.ID = UUIDValue(user.ID) + data.ID = internal.UUIDValue(user.ID) data.Username = types.StringValue(user.Username) data.Name = types.StringValue(user.Name) data.Email = types.StringValue(user.Email) @@ -183,9 +184,9 @@ func (d *UserDataSource) Read(ctx context.Context, req datasource.ReadRequest, r orgIDs := make([]attr.Value, 0, len(user.OrganizationIDs)) for _, orgID := range user.OrganizationIDs { - orgIDs = append(orgIDs, UUIDValue(orgID)) + orgIDs = append(orgIDs, internal.UUIDValue(orgID)) } - data.OrganizationIDs = types.SetValueMust(UUIDType, orgIDs) + data.OrganizationIDs = types.SetValueMust(internal.UUIDType, orgIDs) data.CreatedAt = types.Int64Value(user.CreatedAt.Unix()) data.LastSeenAt = types.Int64Value(user.LastSeenAt.Unix()) data.ThemePreference = types.StringValue(user.ThemePreference) diff --git a/internal/provider/user_data_source_test.go b/internal/provider/user_data_source_test.go index ebdab83..46fb9cc 100644 --- a/internal/provider/user_data_source_test.go +++ b/internal/provider/user_data_source_test.go @@ -128,7 +128,7 @@ data "coderd_user" "test" { }` funcMap := template.FuncMap{ - "orNull": PrintOrNull, + "orNull": printOrNull, } buf := strings.Builder{} diff --git a/internal/provider/user_resource.go b/internal/provider/user_resource.go index 3fa570e..8970569 100644 --- a/internal/provider/user_resource.go +++ b/internal/provider/user_resource.go @@ -5,6 +5,9 @@ import ( "fmt" "strings" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/terraform-provider-coderd/internal" + "github.com/coder/terraform-provider-coderd/internal/codersdkvalidator" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -20,9 +23,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" - - "github.com/coder/coder/v2/codersdk" - "github.com/coder/terraform-provider-coderd/internal/codersdkvalidator" ) // Ensure provider defined types fully satisfy framework interfaces. @@ -40,7 +40,7 @@ type UserResource struct { // UserResourceModel describes the resource data model. type UserResourceModel struct { - ID UUID `tfsdk:"id"` + ID internal.UUID `tfsdk:"id"` Username types.String `tfsdk:"username"` Name types.String `tfsdk:"name"` @@ -61,7 +61,7 @@ func (r *UserResource) Schema(ctx context.Context, req resource.SchemaRequest, r Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - CustomType: UUIDType, + CustomType: internal.UUIDType, Computed: true, MarkdownDescription: "User ID", PlanModifiers: []planmodifier.String{ @@ -191,7 +191,7 @@ func (r *UserResource) Create(ctx context.Context, req resource.CreateRequest, r tflog.Info(ctx, "successfully created user", map[string]any{ "id": user.ID.String(), }) - data.ID = UUIDValue(user.ID) + data.ID = internal.UUIDValue(user.ID) tflog.Info(ctx, "updating user profile") name := data.Username @@ -250,7 +250,7 @@ func (r *UserResource) Read(ctx context.Context, req resource.ReadRequest, resp user, err := client.User(ctx, data.ID.ValueString()) if err != nil { - if isNotFound(err) { + if internal.IsNotFound(err) { resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("User with ID %q not found. Marking as deleted.", data.ID.ValueString())) resp.State.RemoveResource(ctx) return diff --git a/internal/provider/user_resource_test.go b/internal/provider/user_resource_test.go index b9c6bb7..a605d97 100644 --- a/internal/provider/user_resource_test.go +++ b/internal/provider/user_resource_test.go @@ -137,7 +137,7 @@ resource "coderd_user" "test" { ` // Define template functions funcMap := template.FuncMap{ - "orNull": PrintOrNull, + "orNull": printOrNull, } buf := strings.Builder{} diff --git a/internal/provider/workspace_proxy_resource.go b/internal/provider/workspace_proxy_resource.go index 211c778..9575227 100644 --- a/internal/provider/workspace_proxy_resource.go +++ b/internal/provider/workspace_proxy_resource.go @@ -142,7 +142,7 @@ func (r *WorkspaceProxyResource) Read(ctx context.Context, req resource.ReadRequ client := r.data.Client wsp, err := client.WorkspaceProxyByID(ctx, data.ID.ValueUUID()) if err != nil { - if isNotFound(err) { + if IsNotFound(err) { resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("Workspace proxy with ID %s not found. Marking as deleted.", data.ID.ValueString())) resp.State.RemoveResource(ctx) return diff --git a/internal/provider/workspace_proxy_resource_test.go b/internal/provider/workspace_proxy_resource_test.go index 88d7013..9684e26 100644 --- a/internal/provider/workspace_proxy_resource_test.go +++ b/internal/provider/workspace_proxy_resource_test.go @@ -109,7 +109,7 @@ resource "coderd_workspace_proxy" "test" { ` // Define template functions funcMap := template.FuncMap{ - "orNull": PrintOrNull, + "orNull": printOrNull, } buf := strings.Builder{} diff --git a/internal/provider/util.go b/internal/util.go similarity index 78% rename from internal/provider/util.go rename to internal/util.go index 720259c..1481a76 100644 --- a/internal/provider/util.go +++ b/internal/util.go @@ -1,4 +1,4 @@ -package provider +package internal import ( "crypto/sha256" @@ -8,6 +8,7 @@ import ( "net/http" "os" "path/filepath" + "strconv" "github.com/coder/coder/v2/codersdk" "github.com/google/uuid" @@ -57,35 +58,43 @@ func PrintOrNull(v any) string { } } -func computeDirectoryHash(directory string) (string, error) { - var files []string +func DirectoryHash(directory string) (string, error) { + hash := sha256.New() + count := 0 + + // filepath.Walk always proceeds in lexical order, so we don't need to worry + // about order variance from call to call producing different hash results. err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error { if err != nil { return err } - if !info.IsDir() { - files = append(files, path) + if info.IsDir() { + return nil + } + + count++ + data, err := os.ReadFile(path) + if err != nil { + return err } + hash.Write([]byte(path)) + hash.Write(data) + return nil }) + if err != nil { return "", err } - hash := sha256.New() - for _, file := range files { - data, err := os.ReadFile(file) - if err != nil { - return "", err - } - hash.Write(data) - } + hash.Write([]byte(strconv.Itoa(count))) + return hex.EncodeToString(hash.Sum(nil)), nil } -// memberDiff returns the members to add and remove from the group, given the current members and the planned members. +// 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) { +func MemberDiff(curMembers []uuid.UUID, plannedMembers []UUID) (add, remove []string) { curSet := make(map[uuid.UUID]struct{}, len(curMembers)) planSet := make(map[uuid.UUID]struct{}, len(plannedMembers)) @@ -106,7 +115,7 @@ func memberDiff(curMembers []uuid.UUID, plannedMembers []UUID) (add, remove []st return add, remove } -func isNotFound(err error) bool { +func IsNotFound(err error) bool { var sdkErr *codersdk.Error return errors.As(err, &sdkErr) && sdkErr.StatusCode() == http.StatusNotFound } diff --git a/internal/provider/uuid.go b/internal/uuid.go similarity index 94% rename from internal/provider/uuid.go rename to internal/uuid.go index 8cd8912..13c7b5d 100644 --- a/internal/provider/uuid.go +++ b/internal/uuid.go @@ -1,4 +1,4 @@ -package provider +package internal import ( "context" @@ -25,7 +25,7 @@ func (t uuidType) String() string { return "UUID" } -func (t uuidType) ValueType(ctx context.Context) attr.Value { +func (t uuidType) ValueType(_ context.Context) attr.Value { return UUID{} } @@ -38,7 +38,7 @@ func (t uuidType) Equal(o attr.Type) bool { } // ValueFromString implements basetypes.StringTypable. -func (t uuidType) ValueFromString(ctx context.Context, in basetypes.StringValue) (basetypes.StringValuable, diag.Diagnostics) { +func (t uuidType) ValueFromString(_ context.Context, in basetypes.StringValue) (basetypes.StringValuable, diag.Diagnostics) { var diags diag.Diagnostics if in.IsNull() { diff --git a/internal/provider/uuid_internal_test.go b/internal/uuid_internal_test.go similarity index 99% rename from internal/provider/uuid_internal_test.go rename to internal/uuid_internal_test.go index 6283bb9..25073ef 100644 --- a/internal/provider/uuid_internal_test.go +++ b/internal/uuid_internal_test.go @@ -1,4 +1,4 @@ -package provider +package internal import ( "context"