diff --git a/docs/resources/group.md b/docs/resources/group.md index 5579753..2d265d9 100644 --- a/docs/resources/group.md +++ b/docs/resources/group.md @@ -5,6 +5,7 @@ subcategory: "" description: |- A group on the Coder deployment. Creating groups requires an Enterprise license. + When importing, the ID supplied can be either a group UUID retrieved via the API or /. --- # coderd_group (Resource) @@ -13,6 +14,8 @@ A group on the Coder deployment. Creating groups requires an Enterprise license. +When importing, the ID supplied can be either a group UUID retrieved via the API or `/`. + ## Example Usage ```terraform diff --git a/docs/resources/template.md b/docs/resources/template.md index 03af184..92d9fc7 100644 --- a/docs/resources/template.md +++ b/docs/resources/template.md @@ -5,6 +5,7 @@ subcategory: "" description: |- A Coder template. Logs from building template versions are streamed from the provisioner when the TF_LOG environment variable is INFO or higher. + When importing, the ID supplied can be either a template UUID retrieved via the API or /. --- # coderd_template (Resource) @@ -13,6 +14,8 @@ A Coder template. Logs from building template versions are streamed from the provisioner when the `TF_LOG` environment variable is `INFO` or higher. +When importing, the ID supplied can be either a template UUID retrieved via the API or `/`. + ## Example Usage ```terraform diff --git a/docs/resources/user.md b/docs/resources/user.md index e1c6b68..1671fa6 100644 --- a/docs/resources/user.md +++ b/docs/resources/user.md @@ -4,12 +4,15 @@ page_title: "coderd_user Resource - terraform-provider-coderd" subcategory: "" description: |- A user on the Coder deployment. + When importing, the ID supplied can be either a user UUID or a username. --- # coderd_user (Resource) A user on the Coder deployment. +When importing, the ID supplied can be either a user UUID or a username. + ## Example Usage ```terraform diff --git a/internal/provider/group_resource.go b/internal/provider/group_resource.go index 2599f9f..c7f11bd 100644 --- a/internal/provider/group_resource.go +++ b/internal/provider/group_resource.go @@ -3,6 +3,7 @@ package provider import ( "context" "fmt" + "strings" "github.com/coder/coder/v2/codersdk" "github.com/google/uuid" @@ -60,7 +61,9 @@ func (r *GroupResource) Metadata(ctx context.Context, req resource.MetadataReque func (r *GroupResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ - MarkdownDescription: "A group on the Coder deployment.\n\nCreating groups requires an Enterprise license.", + MarkdownDescription: "A group on the Coder deployment.\n\n" + + "Creating groups requires an Enterprise license.\n\n" + + "When importing, the ID supplied can be either a group UUID retrieved via the API or `/`.", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ @@ -324,10 +327,30 @@ func (r *GroupResource) Delete(ctx context.Context, req resource.DeleteRequest, } func (r *GroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + var groupID uuid.UUID client := r.data.Client - groupID, err := uuid.Parse(req.ID) - if err != nil { - resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to parse import group ID as UUID, got error: %s", err)) + idParts := strings.Split(req.ID, "/") + if len(idParts) == 1 { + var err error + groupID, err = uuid.Parse(req.ID) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to parse import group ID as UUID, got error: %s", err)) + return + } + } else if len(idParts) == 2 { + org, err := client.OrganizationByName(ctx, idParts[0]) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to get organization with name %s: %s", idParts[0], err)) + return + } + group, err := client.GroupByOrgAndName(ctx, org.ID, idParts[1]) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to get group with name %s: %s", idParts[1], err)) + return + } + groupID = group.ID + } else { + resp.Diagnostics.AddError("Client Error", "Invalid import ID format, expected a single UUID or `/`") return } group, err := client.Group(ctx, groupID) @@ -339,5 +362,5 @@ func (r *GroupResource) ImportState(ctx context.Context, req resource.ImportStat resp.Diagnostics.AddError("Client Error", "Cannot import groups created via OIDC") return } - resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), groupID.String())...) } diff --git a/internal/provider/group_resource_test.go b/internal/provider/group_resource_test.go index fbf751e..159856f 100644 --- a/internal/provider/group_resource_test.go +++ b/internal/provider/group_resource_test.go @@ -78,14 +78,21 @@ func TestAccGroupResource(t *testing.T) { resource.TestCheckResourceAttr("coderd_group.test", "members.0", user1.ID.String()), ), }, - // Import + // Import by ID { - Config: cfg1.String(t), ResourceName: "coderd_group.test", ImportState: true, ImportStateVerify: true, ImportStateVerifyIgnore: []string{"members"}, }, + // Import by org name and group name + { + ResourceName: "coderd_group.test", + ImportState: true, + ImportStateId: "default/example-group", + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"members"}, + }, // Update and Read { Config: cfg2.String(t), diff --git a/internal/provider/template_resource.go b/internal/provider/template_resource.go index 57c60a9..9255f2f 100644 --- a/internal/provider/template_resource.go +++ b/internal/provider/template_resource.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "io" + "strings" "cdr.dev/slog" "github.com/coder/coder/v2/codersdk" @@ -230,7 +231,8 @@ func (r *TemplateResource) Metadata(ctx context.Context, req resource.MetadataRe func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: "A Coder template.\n\nLogs from building template versions are streamed from the provisioner " + - "when the `TF_LOG` environment variable is `INFO` or higher.", + "when the `TF_LOG` environment variable is `INFO` or higher.\n\n" + + "When importing, the ID supplied can be either a template UUID retrieved via the API or `/`.", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ @@ -771,7 +773,28 @@ func (r *TemplateResource) Delete(ctx context.Context, req resource.DeleteReques } func (r *TemplateResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + idParts := strings.Split(req.ID, "/") + if len(idParts) == 1 { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + return + } else if len(idParts) == 2 { + client := r.data.Client + org, err := client.OrganizationByName(ctx, idParts[0]) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to get organization with name %s: %s", idParts[0], err)) + return + } + template, err := client.TemplateByName(ctx, org.ID, idParts[1]) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to get template with name %s: %s", idParts[1], err)) + return + } + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), template.ID.String())...) + return + } else { + resp.Diagnostics.AddError("Client Error", "Invalid import ID format, expected a single UUID or `/`") + return + } } // ConfigValidators implements resource.ResourceWithConfigValidators. diff --git a/internal/provider/template_resource_test.go b/internal/provider/template_resource_test.go index bea6b4b..b8b7f19 100644 --- a/internal/provider/template_resource_test.go +++ b/internal/provider/template_resource_test.go @@ -145,7 +145,7 @@ func TestAccTemplateResource(t *testing.T) { }, Check: testAccCheckNumTemplateVersions(ctx, client, 3), }, - // Import + // Import by ID { Config: cfg1.String(t), ResourceName: "coderd_template.test", @@ -155,6 +155,14 @@ func TestAccTemplateResource(t *testing.T) { // We can't import ACL as we can't currently differentiate between managed and unmanaged ACL ImportStateVerifyIgnore: []string{"versions", "acl"}, }, + // Import by org name and template name + { + ResourceName: "coderd_template.test", + ImportState: true, + ImportStateVerify: true, + ImportStateId: "default/example-template", + ImportStateVerifyIgnore: []string{"versions", "acl"}, + }, // Change existing version directory & name, update template metadata. Creates a fourth version. { Config: cfg2.String(t), diff --git a/internal/provider/user_resource.go b/internal/provider/user_resource.go index 3eee654..4e8de49 100644 --- a/internal/provider/user_resource.go +++ b/internal/provider/user_resource.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -55,7 +56,8 @@ func (r *UserResource) Metadata(ctx context.Context, req resource.MetadataReques func (r *UserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ - MarkdownDescription: "A user on the Coder deployment.", + MarkdownDescription: "A user on the Coder deployment.\n\n" + + "When importing, the ID supplied can be either a user UUID or a username.", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ @@ -371,6 +373,18 @@ func (r *UserResource) Delete(ctx context.Context, req resource.DeleteRequest, r tflog.Info(ctx, "successfully deleted user") } +// Req.ID can be either a UUID or a username. func (r *UserResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + _, err := uuid.Parse(req.ID) + if err == nil { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + return + } + client := r.data.Client + user, err := client.User(ctx, req.ID) + if err != nil { + resp.Diagnostics.AddError("Client Error", "Invalid import ID format, expected a single UUID or a valid username") + return + } + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), user.ID.String())...) } diff --git a/internal/provider/user_resource_test.go b/internal/provider/user_resource_test.go index 0c3a233..a7bb470 100644 --- a/internal/provider/user_resource_test.go +++ b/internal/provider/user_resource_test.go @@ -60,7 +60,7 @@ func TestAccUserResource(t *testing.T) { resource.TestCheckResourceAttr("coderd_user.test", "suspended", "false"), ), }, - // ImportState testing + // Import by ID { ResourceName: "coderd_user.test", ImportState: true, @@ -68,6 +68,15 @@ func TestAccUserResource(t *testing.T) { // We can't pull the password from the API. ImportStateVerifyIgnore: []string{"password"}, }, + // ImportState by username + { + ResourceName: "coderd_user.test", + ImportState: true, + ImportStateVerify: true, + ImportStateId: "example", + // We can't pull the password from the API. + ImportStateVerifyIgnore: []string{"password"}, + }, // Update and Read testing { Config: cfg2.String(t),