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 4300c55..92d9fc7 100644 --- a/docs/resources/template.md +++ b/docs/resources/template.md @@ -3,12 +3,18 @@ page_title: "coderd_template Resource - terraform-provider-coderd" subcategory: "" description: |- - A Coder template + 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) -A Coder template +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 @@ -47,6 +53,7 @@ resource "coderd_template" "ubuntu-main" { id = coderd_user.coder1.id role = "admin" }] + groups = [] } } ``` 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/examples/resources/coderd_template/resource.tf b/examples/resources/coderd_template/resource.tf index b8d05bd..3cbc8fe 100644 --- a/examples/resources/coderd_template/resource.tf +++ b/examples/resources/coderd_template/resource.tf @@ -32,5 +32,6 @@ resource "coderd_template" "ubuntu-main" { id = coderd_user.coder1.id role = "admin" }] + groups = [] } } diff --git a/go.mod b/go.mod index bb2e38f..eebc6ee 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,8 @@ go 1.22.5 require ( cdr.dev/slog v1.6.2-0.20240126064726-20367d4aede6 - github.com/coder/coder/v2 v2.14.1 - github.com/docker/docker v27.1.1+incompatible + github.com/coder/coder/v2 v2.14.2 + github.com/docker/docker v27.1.2+incompatible github.com/docker/go-connections v0.4.0 github.com/google/uuid v1.6.0 github.com/hashicorp/terraform-plugin-docs v0.19.4 diff --git a/go.sum b/go.sum index 0381db4..b7bc394 100644 --- a/go.sum +++ b/go.sum @@ -81,8 +81,8 @@ github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/coder/coder/v2 v2.14.1 h1:tSYe7H4pNRL8hh9ynwHK7QYTOvG5hcuZJEOkn4ZPASE= -github.com/coder/coder/v2 v2.14.1/go.mod h1:dO79BI5XlP8rrtne1JpRcVehe27bNMXdZKyn1NsWbjA= +github.com/coder/coder/v2 v2.14.2 h1:RNNDDwjNK5D1XMQlK7LWrS4niVclkl1FXoaOaW7N5rs= +github.com/coder/coder/v2 v2.14.2/go.mod h1:dO79BI5XlP8rrtne1JpRcVehe27bNMXdZKyn1NsWbjA= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc= github.com/coder/serpent v0.7.0 h1:zGpD2GlF3lKIVkMjNGKbkip88qzd5r/TRcc30X/SrT0= @@ -103,8 +103,8 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczC github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= -github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v27.1.2+incompatible h1:AhGzR1xaQIy53qCkxARaFluI00WPGtXn0AJuoQsVYTY= +github.com/docker/docker v27.1.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 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 3bdde6d..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" @@ -229,7 +230,9 @@ 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", + 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.\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{ @@ -770,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),