diff --git a/docs/data-sources/workspace_preset.md b/docs/data-sources/workspace_preset.md new file mode 100644 index 00000000..28f90faa --- /dev/null +++ b/docs/data-sources/workspace_preset.md @@ -0,0 +1,42 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "coder_workspace_preset Data Source - terraform-provider-coder" +subcategory: "" +description: |- + Use this data source to predefine common configurations for workspaces. +--- + +# coder_workspace_preset (Data Source) + +Use this data source to predefine common configurations for workspaces. + +## Example Usage + +```terraform +provider "coder" {} + +# presets can be used to predefine common configurations for workspaces +# Parameters are referenced by their name. Each parameter must be defined in the preset. +# Values defined by the preset must pass validation for the parameter. +# See the coder_parameter data source's documentation for examples of how to define +# parameters like the ones used below. +data "coder_workspace_preset" "example" { + name = "example" + parameters = { + (data.coder_parameter.example.name) = "us-central1-a" + (data.coder_parameter.ami.name) = "ami-xxxxxxxx" + } +} +``` + + +## Schema + +### Required + +- `name` (String) Name of the workspace preset. +- `parameters` (Map of String) Parameters of the workspace preset. + +### Read-Only + +- `id` (String) ID of the workspace preset. diff --git a/examples/data-sources/coder_workspace_preset/data-source.tf b/examples/data-sources/coder_workspace_preset/data-source.tf new file mode 100644 index 00000000..4f29a199 --- /dev/null +++ b/examples/data-sources/coder_workspace_preset/data-source.tf @@ -0,0 +1,14 @@ +provider "coder" {} + +# presets can be used to predefine common configurations for workspaces +# Parameters are referenced by their name. Each parameter must be defined in the preset. +# Values defined by the preset must pass validation for the parameter. +# See the coder_parameter data source's documentation for examples of how to define +# parameters like the ones used below. +data "coder_workspace_preset" "example" { + name = "example" + parameters = { + (data.coder_parameter.example.name) = "us-central1-a" + (data.coder_parameter.ami.name) = "ami-xxxxxxxx" + } +} diff --git a/integration/integration_test.go b/integration/integration_test.go index 8ac68f28..bbbd5587 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -73,18 +73,27 @@ func TestIntegration(t *testing.T) { name: "test-data-source", minVersion: "v0.0.0", expectedOutput: map[string]string{ - "provisioner.arch": runtime.GOARCH, - "provisioner.id": `[a-zA-Z0-9-]+`, - "provisioner.os": runtime.GOOS, - "workspace.access_port": `\d+`, - "workspace.access_url": `https?://\D+:\d+`, - "workspace.id": `[a-zA-z0-9-]+`, - "workspace.name": `test-data-source`, - "workspace.start_count": `1`, - "workspace.template_id": `[a-zA-Z0-9-]+`, - "workspace.template_name": `test-data-source`, - "workspace.template_version": `.+`, - "workspace.transition": `start`, + "provisioner.arch": runtime.GOARCH, + "provisioner.id": `[a-zA-Z0-9-]+`, + "provisioner.os": runtime.GOOS, + "workspace.access_port": `\d+`, + "workspace.access_url": `https?://\D+:\d+`, + "workspace.id": `[a-zA-z0-9-]+`, + "workspace.name": `test-data-source`, + "workspace.start_count": `1`, + "workspace.template_id": `[a-zA-Z0-9-]+`, + "workspace.template_name": `test-data-source`, + "workspace.template_version": `.+`, + "workspace.transition": `start`, + "workspace_parameter.name": `param`, + "workspace_parameter.description": `param description`, + // TODO (sasswart): the cli doesn't support presets yet. + // once it does, the value for workspace_parameter.value + // will be the preset value. + "workspace_parameter.value": `param value`, + "workspace_parameter.icon": `param icon`, + "workspace_preset.name": `preset`, + "workspace_preset.parameters.param": `preset param value`, }, }, { @@ -179,8 +188,18 @@ func TestIntegration(t *testing.T) { } _, rc := execContainer(ctx, t, ctrID, fmt.Sprintf(`coder templates %s %s --directory /src/integration/%s --var output_path=/tmp/%s.json --yes`, templateCreateCmd, tt.name, tt.name, tt.name)) require.Equal(t, 0, rc) + + // Check if parameters.yaml exists + _, rc = execContainer(ctx, t, ctrID, fmt.Sprintf(`stat /src/integration/%s/parameters.yaml 2>/dev/null > /dev/null`, tt.name)) + hasParameters := rc == 0 + var includeParameters string + if hasParameters { + // If it exists, include it in the create command + includeParameters = fmt.Sprintf(`--rich-parameter-file /src/integration/%s/parameters.yaml`, tt.name) + } + // Create a workspace - _, rc = execContainer(ctx, t, ctrID, fmt.Sprintf(`coder create %s -t %s --yes`, tt.name, tt.name)) + _, rc = execContainer(ctx, t, ctrID, fmt.Sprintf(`coder create %s -t %s %s --yes`, tt.name, tt.name, includeParameters)) require.Equal(t, 0, rc) // Fetch the output created by the template out, rc := execContainer(ctx, t, ctrID, fmt.Sprintf(`cat /tmp/%s.json`, tt.name)) diff --git a/integration/test-data-source/main.tf b/integration/test-data-source/main.tf index 6d4b85cd..5fb2e0e6 100644 --- a/integration/test-data-source/main.tf +++ b/integration/test-data-source/main.tf @@ -14,6 +14,17 @@ terraform { data "coder_provisioner" "me" {} data "coder_workspace" "me" {} data "coder_workspace_owner" "me" {} +data "coder_parameter" "param" { + name = "param" + description = "param description" + icon = "param icon" +} +data "coder_workspace_preset" "preset" { + name = "preset" + parameters = { + (data.coder_parameter.param.name) = "preset param value" + } +} locals { # NOTE: these must all be strings in the output @@ -30,6 +41,12 @@ locals { "workspace.template_name" : data.coder_workspace.me.template_name, "workspace.template_version" : data.coder_workspace.me.template_version, "workspace.transition" : data.coder_workspace.me.transition, + "workspace_parameter.name" : data.coder_parameter.param.name, + "workspace_parameter.description" : data.coder_parameter.param.description, + "workspace_parameter.value" : data.coder_parameter.param.value, + "workspace_parameter.icon" : data.coder_parameter.param.icon, + "workspace_preset.name" : data.coder_workspace_preset.preset.name, + "workspace_preset.parameters.param" : data.coder_workspace_preset.preset.parameters.param, } } diff --git a/integration/test-data-source/parameters.yaml b/integration/test-data-source/parameters.yaml new file mode 100644 index 00000000..0e75c133 --- /dev/null +++ b/integration/test-data-source/parameters.yaml @@ -0,0 +1 @@ +param: "param value" \ No newline at end of file diff --git a/provider/provider.go b/provider/provider.go index 1d78f2dd..d9780d76 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -61,12 +61,13 @@ func New() *schema.Provider { }, nil }, DataSourcesMap: map[string]*schema.Resource{ - "coder_workspace": workspaceDataSource(), - "coder_workspace_tags": workspaceTagDataSource(), - "coder_provisioner": provisionerDataSource(), - "coder_parameter": parameterDataSource(), - "coder_external_auth": externalAuthDataSource(), - "coder_workspace_owner": workspaceOwnerDataSource(), + "coder_workspace": workspaceDataSource(), + "coder_workspace_tags": workspaceTagDataSource(), + "coder_provisioner": provisionerDataSource(), + "coder_parameter": parameterDataSource(), + "coder_external_auth": externalAuthDataSource(), + "coder_workspace_owner": workspaceOwnerDataSource(), + "coder_workspace_preset": workspacePresetDataSource(), }, ResourcesMap: map[string]*schema.Resource{ "coder_agent": agentResource(), diff --git a/provider/workspace_preset.go b/provider/workspace_preset.go new file mode 100644 index 00000000..cd56c980 --- /dev/null +++ b/provider/workspace_preset.go @@ -0,0 +1,70 @@ +package provider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/mitchellh/mapstructure" +) + +type WorkspacePreset struct { + Name string `mapstructure:"name"` + Parameters map[string]string `mapstructure:"parameters"` +} + +func workspacePresetDataSource() *schema.Resource { + return &schema.Resource{ + SchemaVersion: 1, + + Description: "Use this data source to predefine common configurations for workspaces.", + ReadContext: func(ctx context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics { + var preset WorkspacePreset + err := mapstructure.Decode(struct { + Name interface{} + Parameters interface{} + }{ + Name: rd.Get("name"), + Parameters: rd.Get("parameters"), + }, &preset) + if err != nil { + return diag.Errorf("decode workspace preset: %s", err) + } + + // MinItems doesn't work with maps, so we need to check the length + // of the map manually. All other validation is handled by the + // schema. + if len(preset.Parameters) == 0 { + return diag.Errorf("expected \"parameters\" to not be an empty map") + } + + rd.SetId(preset.Name) + + return nil + }, + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Description: "ID of the workspace preset.", + Computed: true, + }, + "name": { + Type: schema.TypeString, + Description: "Name of the workspace preset.", + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + "parameters": { + Type: schema.TypeMap, + Description: "Parameters of the workspace preset.", + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + }, + } +} diff --git a/provider/workspace_preset_test.go b/provider/workspace_preset_test.go new file mode 100644 index 00000000..876e2044 --- /dev/null +++ b/provider/workspace_preset_test.go @@ -0,0 +1,128 @@ +package provider_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/stretchr/testify/require" +) + +func TestWorkspacePreset(t *testing.T) { + t.Parallel() + type testcase struct { + Name string + Config string + ExpectError *regexp.Regexp + Check func(state *terraform.State) error + } + testcases := []testcase{ + { + Name: "Happy Path", + Config: ` + data "coder_workspace_preset" "preset_1" { + name = "preset_1" + parameters = { + "region" = "us-east1-a" + } + }`, + Check: func(state *terraform.State) error { + require.Len(t, state.Modules, 1) + require.Len(t, state.Modules[0].Resources, 1) + resource := state.Modules[0].Resources["data.coder_workspace_preset.preset_1"] + require.NotNil(t, resource) + attrs := resource.Primary.Attributes + require.Equal(t, attrs["name"], "preset_1") + require.Equal(t, attrs["parameters.region"], "us-east1-a") + return nil + }, + }, + { + Name: "Name field is not provided", + Config: ` + data "coder_workspace_preset" "preset_1" { + parameters = { + "region" = "us-east1-a" + } + }`, + // This validation is done by Terraform, but it could still break if we misconfigure the schema. + // So we test it here to make sure we don't regress. + ExpectError: regexp.MustCompile("The argument \"name\" is required, but no definition was found"), + }, + { + Name: "Name field is empty", + Config: ` + data "coder_workspace_preset" "preset_1" { + name = "" + parameters = { + "region" = "us-east1-a" + } + }`, + // This validation is done by Terraform, but it could still break if we misconfigure the schema. + // So we test it here to make sure we don't regress. + ExpectError: regexp.MustCompile("expected \"name\" to not be an empty string"), + }, + { + Name: "Name field is not a string", + Config: ` + data "coder_workspace_preset" "preset_1" { + name = [1, 2, 3] + parameters = { + "region" = "us-east1-a" + } + }`, + // This validation is done by Terraform, but it could still break if we misconfigure the schema. + // So we test it here to make sure we don't regress. + ExpectError: regexp.MustCompile("Incorrect attribute value type"), + }, + { + Name: "Parameters field is not provided", + Config: ` + data "coder_workspace_preset" "preset_1" { + name = "preset_1" + }`, + // This validation is done by Terraform, but it could still break if we misconfigure the schema. + // So we test it here to make sure we don't regress. + ExpectError: regexp.MustCompile("The argument \"parameters\" is required, but no definition was found"), + }, + { + Name: "Parameters field is empty", + Config: ` + data "coder_workspace_preset" "preset_1" { + name = "preset_1" + parameters = {} + }`, + // This validation is *not* done by Terraform, because MinItems doesn't work with maps. + // We've implemented the validation in ReadContext, so we test it here to make sure we don't regress. + ExpectError: regexp.MustCompile("expected \"parameters\" to not be an empty map"), + }, + { + Name: "Parameters field is not a map", + Config: ` + data "coder_workspace_preset" "preset_1" { + name = "preset_1" + parameters = "not a map" + }`, + // This validation is done by Terraform, but it could still break if we misconfigure the schema. + // So we test it here to make sure we don't regress. + ExpectError: regexp.MustCompile("Inappropriate value for attribute \"parameters\": map of string required"), + }, + } + + for _, testcase := range testcases { + t.Run(testcase.Name, func(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + ProviderFactories: coderFactory(), + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: testcase.Config, + ExpectError: testcase.ExpectError, + Check: testcase.Check, + }}, + }) + }) + } +}