diff --git a/docs/resources/provisioner_key.md b/docs/resources/provisioner_key.md new file mode 100644 index 0000000..d64b724 --- /dev/null +++ b/docs/resources/provisioner_key.md @@ -0,0 +1,29 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "coderd_provisioner_key Resource - terraform-provider-coderd" +subcategory: "" +description: |- + A provisioner key for a Coder deployment. +--- + +# coderd_provisioner_key (Resource) + +A provisioner key for a Coder deployment. + + + + +## Schema + +### Required + +- `name` (String) The name of the key. +- `organization_id` (String) The organization that provisioners connected with this key will be connected to. + +### Optional + +- `tags` (Map of String) The tags that provisioners connected with this key will accept jobs for. + +### Read-Only + +- `key` (String, Sensitive) The acquired provisioner key diff --git a/internal/provider/license_resource_test.go b/internal/provider/license_resource_test.go index e2d13d6..9a4abc7 100644 --- a/internal/provider/license_resource_test.go +++ b/internal/provider/license_resource_test.go @@ -24,7 +24,7 @@ func TestAccLicenseResource(t *testing.T) { t.Skip("No license found for license resource tests, skipping") } - cfg1 := testAccLicenseResourceconfig{ + cfg1 := testAccLicenseResourceConfig{ URL: client.URL.String(), Token: client.SessionToken(), License: license, @@ -42,13 +42,13 @@ func TestAccLicenseResource(t *testing.T) { }) } -type testAccLicenseResourceconfig struct { +type testAccLicenseResourceConfig struct { URL string Token string License string } -func (c testAccLicenseResourceconfig) String(t *testing.T) string { +func (c testAccLicenseResourceConfig) String(t *testing.T) string { t.Helper() tpl := ` provider coderd { diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 7b7d165..b8b9fa7 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 NewWorkspaceProxyResource, NewLicenseResource, NewOrganizationResource, + NewProvisionerKeyResource, } } diff --git a/internal/provider/provisioner_key_resource.go b/internal/provider/provisioner_key_resource.go new file mode 100644 index 0000000..5904df0 --- /dev/null +++ b/internal/provider/provisioner_key_resource.go @@ -0,0 +1,154 @@ +package provider + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/coder/coder/v2/codersdk" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &ProvisionerKeyResource{} + +func NewProvisionerKeyResource() resource.Resource { + return &ProvisionerKeyResource{} +} + +// ProvisionerKeyResource defines the resource implementation. +type ProvisionerKeyResource struct { + *CoderdProviderData +} + +// ProvisionerKeyResourceModel describes the resource data model. +type ProvisionerKeyResourceModel struct { + OrganizationID UUID `tfsdk:"organization_id"` + Name types.String `tfsdk:"name"` + Tags types.Map `tfsdk:"tags"` + Key types.String `tfsdk:"key"` +} + +func (r *ProvisionerKeyResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_provisioner_key" +} + +func (r *ProvisionerKeyResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "A provisioner key for a Coder deployment.", + + Attributes: map[string]schema.Attribute{ + "organization_id": schema.StringAttribute{ + CustomType: UUIDType, + MarkdownDescription: "The organization that provisioners connected with this key will be connected to.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "The name of the key.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "tags": schema.MapAttribute{ + MarkdownDescription: "The tags that provisioners connected with this key will accept jobs for.", + ElementType: types.StringType, + Optional: true, + PlanModifiers: []planmodifier.Map{ + mapplanmodifier.RequiresReplace(), + }, + }, + "key": schema.StringAttribute{ + MarkdownDescription: "The acquired provisioner key", + Computed: true, + Sensitive: true, + }, + }, + } +} + +func (r *ProvisionerKeyResource) 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( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *CoderdProviderData, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.CoderdProviderData = data +} + +func (r *ProvisionerKeyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Read Terraform plan data into the model + var data ProvisionerKeyResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + var tags map[string]string + resp.Diagnostics.Append(data.Tags.ElementsAs(ctx, &tags, false)...) + createKeyResult, err := r.Client.CreateProvisionerKey(ctx, data.OrganizationID.ValueUUID(), codersdk.CreateProvisionerKeyRequest{ + Name: data.Name.ValueString(), + Tags: tags, + }) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create provisioner_key, got error: %s", err)) + return + } + + data.Key = types.StringValue(createKeyResult.Key) + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ProvisionerKeyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Read Terraform prior state data into the model + var data ProvisionerKeyResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Provisioner keys are immutable, no reading necessary. + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ProvisionerKeyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Provisioner keys are immutable, updating is always invalid. + resp.Diagnostics.AddError("Invalid Update", "Terraform is attempting to update a resource which must be replaced") +} + +func (r *ProvisionerKeyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Read Terraform prior state data into the model + var data ProvisionerKeyResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + err := r.Client.DeleteProvisionerKey(ctx, data.OrganizationID.ValueUUID(), data.Name.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete provisionerkey, got error: %s", err)) + return + } +} diff --git a/internal/provider/provisioner_key_resource_test.go b/internal/provider/provisioner_key_resource_test.go new file mode 100644 index 0000000..9c89647 --- /dev/null +++ b/internal/provider/provisioner_key_resource_test.go @@ -0,0 +1,114 @@ +package provider + +import ( + "context" + "os" + "strings" + "testing" + "text/template" + + "github.com/coder/terraform-provider-coderd/integration" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/stretchr/testify/require" +) + +func TestAccProvisionerKeyResource(t *testing.T) { + if os.Getenv("TF_ACC") == "" { + t.Skip("Acceptance tests are disabled.") + } + ctx := context.Background() + client := integration.StartCoder(ctx, t, "provisioner_key_acc", true) + orgs, err := client.Organizations(ctx) + require.NoError(t, err) + firstOrg := orgs[0].ID + + cfg1 := testAccProvisionerKeyResourceConfig{ + URL: client.URL.String(), + Token: client.SessionToken(), + + OrganizationID: firstOrg, + Name: "example-provisioner-key", + } + + cfg2 := cfg1 + cfg2.Tags = map[string]string{ + "wibble": "wobble", + } + + cfg3 := cfg2 + cfg3.Name = "different-provisioner-key" + + resource.Test(t, resource.TestCase{ + IsUnitTest: true, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: cfg1.String(t), + }, + { + Config: cfg2.String(t), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("coderd_provisioner_key.test", plancheck.ResourceActionReplace), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("coderd_provisioner_key.test", tfjsonpath.New("tags").AtMapKey("wibble"), knownvalue.StringExact("wobble")), + }, + }, + { + Config: cfg3.String(t), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("coderd_provisioner_key.test", plancheck.ResourceActionReplace), + }, + }, + }, + }, + }) +} + +type testAccProvisionerKeyResourceConfig struct { + URL string + Token string + + OrganizationID uuid.UUID + Name string + Tags map[string]string +} + +func (c testAccProvisionerKeyResourceConfig) String(t *testing.T) string { + t.Helper() + + tpl := ` +provider coderd { + url = "{{.URL}}" + token = "{{.Token}}" +} + +resource "coderd_provisioner_key" "test" { + organization_id = "{{.OrganizationID}}" + name = "{{.Name}}" + + tags = { + {{- range $key, $value := .Tags}} + {{$key}} = "{{$value}}" + {{- end}} + } +} +` + + buf := strings.Builder{} + tmpl, err := template.New("provisionerKeyResource").Parse(tpl) + require.NoError(t, err) + + err = tmpl.Execute(&buf, c) + require.NoError(t, err) + return buf.String() +} diff --git a/internal/provider/template_resource_test.go b/internal/provider/template_resource_test.go index b9d7ae3..c844da0 100644 --- a/internal/provider/template_resource_test.go +++ b/internal/provider/template_resource_test.go @@ -424,7 +424,7 @@ func TestAccTemplateResourceEnterprise(t *testing.T) { t.Skip("Acceptance tests are disabled.") } ctx := context.Background() - client := integration.StartCoder(ctx, t, "template_acc", true) + client := integration.StartCoder(ctx, t, "template_resource_acc", true) firstUser, err := client.User(ctx, codersdk.Me) require.NoError(t, err) @@ -565,7 +565,7 @@ func TestAccTemplateResourceAGPL(t *testing.T) { t.Skip("Acceptance tests are disabled.") } ctx := context.Background() - client := integration.StartCoder(ctx, t, "template_acc", false) + client := integration.StartCoder(ctx, t, "template_resource_agpl_acc", false) firstUser, err := client.User(ctx, codersdk.Me) require.NoError(t, err) @@ -689,7 +689,7 @@ resource "coderd_template" "sample" { }` ctx := context.Background() - client := integration.StartCoder(ctx, t, "template_acc", false) + client := integration.StartCoder(ctx, t, "template_resource_variables_acc", false) cfg = fmt.Sprintf(cfg, client.URL.String(), client.SessionToken())