Skip to content

Commit

Permalink
chore: support importing resources by fully qualified names
Browse files Browse the repository at this point in the history
  • Loading branch information
ethanndickson committed Sep 3, 2024
1 parent 4d366f7 commit ced04dc
Show file tree
Hide file tree
Showing 9 changed files with 106 additions and 13 deletions.
3 changes: 3 additions & 0 deletions docs/resources/group.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <organization-name>/<group-name>.
---

# coderd_group (Resource)
Expand All @@ -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 `<organization-name>/<group-name>`.

## Example Usage

```terraform
Expand Down
3 changes: 3 additions & 0 deletions docs/resources/template.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <organization-name>/<template-name>.
---

# coderd_template (Resource)
Expand All @@ -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 `<organization-name>/<template-name>`.

## Example Usage

```terraform
Expand Down
3 changes: 3 additions & 0 deletions docs/resources/user.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 28 additions & 5 deletions internal/provider/group_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package provider
import (
"context"
"fmt"
"strings"

"github.com/coder/coder/v2/codersdk"
"github.com/google/uuid"
Expand Down Expand Up @@ -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 `<organization-name>/<group-name>`.",

Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Expand Down Expand Up @@ -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 `<organization-name>/<group-name>`")
return
}
group, err := client.Group(ctx, groupID)
Expand All @@ -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())...)
}
11 changes: 9 additions & 2 deletions internal/provider/group_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
27 changes: 25 additions & 2 deletions internal/provider/template_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"io"
"strings"

"cdr.dev/slog"
"github.com/coder/coder/v2/codersdk"
Expand Down Expand Up @@ -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 `<organization-name>/<template-name>`.",

Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Expand Down Expand Up @@ -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 `<organization-name>/<template-name>`")
return
}
}

// ConfigValidators implements resource.ResourceWithConfigValidators.
Expand Down
10 changes: 9 additions & 1 deletion internal/provider/template_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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),
Expand Down
18 changes: 16 additions & 2 deletions internal/provider/user_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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())...)
}
11 changes: 10 additions & 1 deletion internal/provider/user_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,23 @@ func TestAccUserResource(t *testing.T) {
resource.TestCheckResourceAttr("coderd_user.test", "suspended", "false"),
),
},
// ImportState testing
// Import by ID
{
ResourceName: "coderd_user.test",
ImportState: true,
ImportStateVerify: true,
// 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),
Expand Down

0 comments on commit ced04dc

Please sign in to comment.