From 5418ed7084b46f0d2bfd1db8bc8522004b5ba07c Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Thu, 3 Apr 2025 12:02:00 +0000 Subject: [PATCH 1/7] feat: allow presets to define prebuilds --- README.md | 2 +- provider/workspace.go | 22 +++++++++++++++++++ provider/workspace_preset.go | 35 ++++++++++++++++++++++++++++-- provider/workspace_preset_test.go | 36 +++++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b8ee884..f055961 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ to setup your local Terraform to use your local version rather than the registry } ``` 2. Run `terraform init` and observe a warning like `Warning: Provider development overrides are in effect` -4. Run `go build -o terraform-provider-coder` to build the provider binary, which Terraform will try locate and execute +4. Run `make build` to build the provider binary, which Terraform will try locate and execute 5. All local Terraform runs will now use your local provider! 6. _**NOTE**: we vendor in this provider into `github.com/coder/coder`, so if you're testing with a local clone then you should also run `go mod edit -replace github.com/coder/terraform-provider-coder=/path/to/terraform-provider-coder` in your clone._ diff --git a/provider/workspace.go b/provider/workspace.go index fde742b..19da0d0 100644 --- a/provider/workspace.go +++ b/provider/workspace.go @@ -27,6 +27,14 @@ func workspaceDataSource() *schema.Resource { } _ = rd.Set("start_count", count) + prebuild := helpers.OptionalEnv(IsPrebuildEnvironmentVariable()) + prebuildCount := 0 + if prebuild == "true" { + prebuildCount = 1 + _ = rd.Set("is_prebuild", true) + } + _ = rd.Set("prebuild_count", prebuildCount) + name := helpers.OptionalEnvOrDefault("CODER_WORKSPACE_NAME", "default") rd.Set("name", name) @@ -88,6 +96,16 @@ func workspaceDataSource() *schema.Resource { Computed: true, Description: "A computed count based on `transition` state. If `start`, count will equal 1.", }, + "prebuild_count": { + Type: schema.TypeInt, + Computed: true, + Description: "TODO", + }, + "is_prebuild": { + Type: schema.TypeBool, + Computed: true, + Description: "TODO", + }, "transition": { Type: schema.TypeString, Computed: true, @@ -121,3 +139,7 @@ func workspaceDataSource() *schema.Resource { }, } } + +func IsPrebuildEnvironmentVariable() string { + return "CODER_WORKSPACE_IS_PREBUILD" +} diff --git a/provider/workspace_preset.go b/provider/workspace_preset.go index cd56c98..eafc39e 100644 --- a/provider/workspace_preset.go +++ b/provider/workspace_preset.go @@ -10,8 +10,13 @@ import ( ) type WorkspacePreset struct { - Name string `mapstructure:"name"` - Parameters map[string]string `mapstructure:"parameters"` + Name string `mapstructure:"name"` + Parameters map[string]string `mapstructure:"parameters"` + Prebuild []WorkspacePrebuild `mapstructure:"prebuilds"` +} + +type WorkspacePrebuild struct { + Instances int `mapstructure:"instances"` } func workspacePresetDataSource() *schema.Resource { @@ -24,9 +29,19 @@ func workspacePresetDataSource() *schema.Resource { err := mapstructure.Decode(struct { Name interface{} Parameters interface{} + Prebuilds []struct { + Instances interface{} + } }{ Name: rd.Get("name"), Parameters: rd.Get("parameters"), + Prebuilds: []struct { + Instances interface{} + }{ + { + Instances: rd.Get("prebuilds.0.instances"), + }, + }, }, &preset) if err != nil { return diag.Errorf("decode workspace preset: %s", err) @@ -65,6 +80,22 @@ func workspacePresetDataSource() *schema.Resource { ValidateFunc: validation.StringIsNotEmpty, }, }, + "prebuilds": { + Type: schema.TypeSet, + Description: "Prebuilds of the workspace preset.", + Optional: true, + MaxItems: 1, // TODO: is this always true? More than 1 prebuilds config per preset? + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "instances": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + ValidateFunc: validation.IntAtLeast(0), + }, + }, + }, + }, }, } } diff --git a/provider/workspace_preset_test.go b/provider/workspace_preset_test.go index 876e204..8f0d31e 100644 --- a/provider/workspace_preset_test.go +++ b/provider/workspace_preset_test.go @@ -108,6 +108,42 @@ func TestWorkspacePreset(t *testing.T) { // So we test it here to make sure we don't regress. ExpectError: regexp.MustCompile("Inappropriate value for attribute \"parameters\": map of string required"), }, + { + Name: "Prebuilds is set, but not its required fields", + Config: ` + data "coder_workspace_preset" "preset_1" { + name = "preset_1" + parameters = { + "region" = "us-east1-a" + } + prebuilds {} + }`, + ExpectError: regexp.MustCompile("The argument \"instances\" is required, but no definition was found."), + }, + { + Name: "Prebuilds is set, and so are its required fields", + Config: ` + data "coder_workspace_preset" "preset_1" { + name = "preset_1" + parameters = { + "region" = "us-east1-a" + } + prebuilds { + instances = 1 + } + }`, + ExpectError: nil, + 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["prebuilds.0.instances"], "1") + return nil + }, + }, } for _, testcase := range testcases { From af250375660c21ab9c2ecfa4001834b44e6db46f Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Thu, 3 Apr 2025 12:06:34 +0000 Subject: [PATCH 2/7] document prebuild parameters --- provider/workspace.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/provider/workspace.go b/provider/workspace.go index 19da0d0..30e7ad8 100644 --- a/provider/workspace.go +++ b/provider/workspace.go @@ -91,20 +91,15 @@ func workspaceDataSource() *schema.Resource { Computed: true, Description: "The access port of the Coder deployment provisioning this workspace.", }, - "start_count": { - Type: schema.TypeInt, - Computed: true, - Description: "A computed count based on `transition` state. If `start`, count will equal 1.", - }, "prebuild_count": { Type: schema.TypeInt, Computed: true, - Description: "TODO", + Description: "A computed count, equal to 1 if the workspace was prebuilt.", }, - "is_prebuild": { - Type: schema.TypeBool, + "start_count": { + Type: schema.TypeInt, Computed: true, - Description: "TODO", + Description: "A computed count based on `transition` state. If `start`, count will equal 1.", }, "transition": { Type: schema.TypeString, @@ -116,6 +111,11 @@ func workspaceDataSource() *schema.Resource { Computed: true, Description: "UUID of the workspace.", }, + "is_prebuild": { + Type: schema.TypeBool, + Computed: true, + Description: "Whether the workspace is a prebuild.", + }, "name": { Type: schema.TypeString, Computed: true, From 56d1ab72a05ac67eaf2409d8826868234ffa1109 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Thu, 3 Apr 2025 12:07:59 +0000 Subject: [PATCH 3/7] remove todo --- provider/workspace_preset.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provider/workspace_preset.go b/provider/workspace_preset.go index eafc39e..b29ecba 100644 --- a/provider/workspace_preset.go +++ b/provider/workspace_preset.go @@ -84,7 +84,7 @@ func workspacePresetDataSource() *schema.Resource { Type: schema.TypeSet, Description: "Prebuilds of the workspace preset.", Optional: true, - MaxItems: 1, // TODO: is this always true? More than 1 prebuilds config per preset? + MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "instances": { From c8c510180877b66ca4098f5eeaeaf751343de3e0 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Thu, 3 Apr 2025 12:09:44 +0000 Subject: [PATCH 4/7] make gen --- docs/data-sources/workspace.md | 2 ++ docs/data-sources/workspace_preset.md | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/docs/data-sources/workspace.md b/docs/data-sources/workspace.md index 26396ba..30deee2 100644 --- a/docs/data-sources/workspace.md +++ b/docs/data-sources/workspace.md @@ -69,7 +69,9 @@ resource "docker_container" "workspace" { - `access_port` (Number) The access port of the Coder deployment provisioning this workspace. - `access_url` (String) The access URL of the Coder deployment provisioning this workspace. - `id` (String) UUID of the workspace. +- `is_prebuild` (Boolean) Whether the workspace is a prebuild. - `name` (String) Name of the workspace. +- `prebuild_count` (Number) A computed count, equal to 1 if the workspace was prebuilt. - `start_count` (Number) A computed count based on `transition` state. If `start`, count will equal 1. - `template_id` (String) ID of the workspace's template. - `template_name` (String) Name of the workspace's template. diff --git a/docs/data-sources/workspace_preset.md b/docs/data-sources/workspace_preset.md index 28f90fa..9c393fa 100644 --- a/docs/data-sources/workspace_preset.md +++ b/docs/data-sources/workspace_preset.md @@ -37,6 +37,17 @@ data "coder_workspace_preset" "example" { - `name` (String) Name of the workspace preset. - `parameters` (Map of String) Parameters of the workspace preset. +### Optional + +- `prebuilds` (Block Set, Max: 1) Prebuilds of the workspace preset. (see [below for nested schema](#nestedblock--prebuilds)) + ### Read-Only - `id` (String) ID of the workspace preset. + + +### Nested Schema for `prebuilds` + +Required: + +- `instances` (Number) From 4e37a00071c1f1568f4f231ea5d5185e7921cf67 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Fri, 4 Apr 2025 13:30:44 +0000 Subject: [PATCH 5/7] Add integration testing --- integration/integration_test.go | 9 +++++---- integration/test-data-source/main.tf | 5 +++++ provider/workspace_preset.go | 21 ++++++--------------- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/integration/integration_test.go b/integration/integration_test.go index 65aa5ae..9803aa4 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -90,10 +90,11 @@ func TestIntegration(t *testing.T) { // 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`, + "workspace_parameter.value": `param value`, + "workspace_parameter.icon": `param icon`, + "workspace_preset.name": `preset`, + "workspace_preset.parameters.param": `preset param value`, + "workspace_preset.prebuilds.instances": `1`, }, }, { diff --git a/integration/test-data-source/main.tf b/integration/test-data-source/main.tf index 5fb2e0e..f18fa34 100644 --- a/integration/test-data-source/main.tf +++ b/integration/test-data-source/main.tf @@ -24,6 +24,10 @@ data "coder_workspace_preset" "preset" { parameters = { (data.coder_parameter.param.name) = "preset param value" } + + prebuilds { + instances = 1 + } } locals { @@ -47,6 +51,7 @@ locals { "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, + "workspace_preset.prebuilds.instances" : tostring(one(data.coder_workspace_preset.preset.prebuilds).instances), } } diff --git a/provider/workspace_preset.go b/provider/workspace_preset.go index b29ecba..686cc4c 100644 --- a/provider/workspace_preset.go +++ b/provider/workspace_preset.go @@ -10,9 +10,9 @@ import ( ) type WorkspacePreset struct { - Name string `mapstructure:"name"` - Parameters map[string]string `mapstructure:"parameters"` - Prebuild []WorkspacePrebuild `mapstructure:"prebuilds"` + Name string `mapstructure:"name"` + Parameters map[string]string `mapstructure:"parameters"` + Prebuilds WorkspacePrebuild `mapstructure:"prebuilds"` } type WorkspacePrebuild struct { @@ -29,31 +29,22 @@ func workspacePresetDataSource() *schema.Resource { err := mapstructure.Decode(struct { Name interface{} Parameters interface{} - Prebuilds []struct { + Prebuilds struct { Instances interface{} } }{ Name: rd.Get("name"), Parameters: rd.Get("parameters"), - Prebuilds: []struct { + Prebuilds: struct { Instances interface{} }{ - { - Instances: rd.Get("prebuilds.0.instances"), - }, + Instances: rd.Get("prebuilds.0.instances"), }, }, &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 From 06cf76056672cdc0215582ed3f882c7f55f31a90 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Fri, 4 Apr 2025 14:20:51 +0000 Subject: [PATCH 6/7] Allow presets to define 0 parameters --- provider/workspace_preset.go | 2 +- provider/workspace_preset_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/provider/workspace_preset.go b/provider/workspace_preset.go index 686cc4c..8ff1686 100644 --- a/provider/workspace_preset.go +++ b/provider/workspace_preset.go @@ -64,7 +64,7 @@ func workspacePresetDataSource() *schema.Resource { "parameters": { Type: schema.TypeMap, Description: "Parameters of the workspace preset.", - Required: true, + Optional: true, Elem: &schema.Schema{ Type: schema.TypeString, Required: true, diff --git a/provider/workspace_preset_test.go b/provider/workspace_preset_test.go index 8f0d31e..aa1ca0c 100644 --- a/provider/workspace_preset_test.go +++ b/provider/workspace_preset_test.go @@ -84,7 +84,7 @@ func TestWorkspacePreset(t *testing.T) { }`, // 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"), + ExpectError: nil, }, { Name: "Parameters field is empty", @@ -95,7 +95,7 @@ func TestWorkspacePreset(t *testing.T) { }`, // 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"), + ExpectError: nil, }, { Name: "Parameters field is not a map", From 9f26791c99a88ac3e7a3ea15d8aace2b774701ce Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Fri, 4 Apr 2025 15:24:16 +0000 Subject: [PATCH 7/7] additional documentation for prebuilds --- docs/data-sources/workspace.md | 4 ++-- docs/data-sources/workspace_preset.md | 14 +++++++------- provider/workspace.go | 4 ++-- provider/workspace_preset.go | 11 ++++++----- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/docs/data-sources/workspace.md b/docs/data-sources/workspace.md index 30deee2..29fd917 100644 --- a/docs/data-sources/workspace.md +++ b/docs/data-sources/workspace.md @@ -69,9 +69,9 @@ resource "docker_container" "workspace" { - `access_port` (Number) The access port of the Coder deployment provisioning this workspace. - `access_url` (String) The access URL of the Coder deployment provisioning this workspace. - `id` (String) UUID of the workspace. -- `is_prebuild` (Boolean) Whether the workspace is a prebuild. +- `is_prebuild` (Boolean) Similar to `prebuild_count`, but a boolean value instead of a count. This is set to true if the workspace is a currently unassigned prebuild. Once the workspace is assigned, this value will be false. - `name` (String) Name of the workspace. -- `prebuild_count` (Number) A computed count, equal to 1 if the workspace was prebuilt. +- `prebuild_count` (Number) A computed count, equal to 1 if the workspace is a currently unassigned prebuild. Use this to conditionally act on the status of a prebuild. Actions that do not require user identity can be taken when this value is set to 1. Actions that should only be taken once the workspace has been assigned to a user may be taken when this value is set to 0. - `start_count` (Number) A computed count based on `transition` state. If `start`, count will equal 1. - `template_id` (String) ID of the workspace's template. - `template_name` (String) Name of the workspace's template. diff --git a/docs/data-sources/workspace_preset.md b/docs/data-sources/workspace_preset.md index 9c393fa..edd61f1 100644 --- a/docs/data-sources/workspace_preset.md +++ b/docs/data-sources/workspace_preset.md @@ -3,12 +3,12 @@ page_title: "coder_workspace_preset Data Source - terraform-provider-coder" subcategory: "" description: |- - Use this data source to predefine common configurations for workspaces. + Use this data source to predefine common configurations for coder workspaces. Users will have the option to select a defined preset, which will automatically apply the selected configuration. Any parameters defined in the preset will be applied to the workspace. Parameters that are not defined by the preset will still be configurable when creating a workspace. --- # coder_workspace_preset (Data Source) -Use this data source to predefine common configurations for workspaces. +Use this data source to predefine common configurations for coder workspaces. Users will have the option to select a defined preset, which will automatically apply the selected configuration. Any parameters defined in the preset will be applied to the workspace. Parameters that are not defined by the preset will still be configurable when creating a workspace. ## Example Usage @@ -34,20 +34,20 @@ data "coder_workspace_preset" "example" { ### Required -- `name` (String) Name of the workspace preset. -- `parameters` (Map of String) Parameters of the workspace preset. +- `name` (String) The name of the workspace preset. ### Optional -- `prebuilds` (Block Set, Max: 1) Prebuilds of the workspace preset. (see [below for nested schema](#nestedblock--prebuilds)) +- `parameters` (Map of String) Workspace parameters that will be set by the workspace preset. For simple templates that only need prebuilds, you may define a preset with zero parameters. Because workspace parameters may change between Coder template versions, preset parameters are allowed to define values for parameters that do not exist in the current template version. +- `prebuilds` (Block Set, Max: 1) Prebuilt workspace configuration related to this workspace preset. Coder will build and maintain workspaces in reserve based on this configuration. When a user creates a new workspace using a preset, they will be assigned a prebuilt workspace, instead of waiting for a new workspace to build. (see [below for nested schema](#nestedblock--prebuilds)) ### Read-Only -- `id` (String) ID of the workspace preset. +- `id` (String) The preset ID is automatically generated and may change between runs. It is recommended to use the `name` attribute to identify the preset. ### Nested Schema for `prebuilds` Required: -- `instances` (Number) +- `instances` (Number) The number of workspaces to keep in reserve for this preset. diff --git a/provider/workspace.go b/provider/workspace.go index 30e7ad8..5ddd3ee 100644 --- a/provider/workspace.go +++ b/provider/workspace.go @@ -94,7 +94,7 @@ func workspaceDataSource() *schema.Resource { "prebuild_count": { Type: schema.TypeInt, Computed: true, - Description: "A computed count, equal to 1 if the workspace was prebuilt.", + Description: "A computed count, equal to 1 if the workspace is a currently unassigned prebuild. Use this to conditionally act on the status of a prebuild. Actions that do not require user identity can be taken when this value is set to 1. Actions that should only be taken once the workspace has been assigned to a user may be taken when this value is set to 0.", }, "start_count": { Type: schema.TypeInt, @@ -114,7 +114,7 @@ func workspaceDataSource() *schema.Resource { "is_prebuild": { Type: schema.TypeBool, Computed: true, - Description: "Whether the workspace is a prebuild.", + Description: "Similar to `prebuild_count`, but a boolean value instead of a count. This is set to true if the workspace is a currently unassigned prebuild. Once the workspace is assigned, this value will be false.", }, "name": { Type: schema.TypeString, diff --git a/provider/workspace_preset.go b/provider/workspace_preset.go index 8ff1686..004489e 100644 --- a/provider/workspace_preset.go +++ b/provider/workspace_preset.go @@ -23,7 +23,7 @@ func workspacePresetDataSource() *schema.Resource { return &schema.Resource{ SchemaVersion: 1, - Description: "Use this data source to predefine common configurations for workspaces.", + Description: "Use this data source to predefine common configurations for coder workspaces. Users will have the option to select a defined preset, which will automatically apply the selected configuration. Any parameters defined in the preset will be applied to the workspace. Parameters that are not defined by the preset will still be configurable when creating a workspace.", ReadContext: func(ctx context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics { var preset WorkspacePreset err := mapstructure.Decode(struct { @@ -52,18 +52,18 @@ func workspacePresetDataSource() *schema.Resource { Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeString, - Description: "ID of the workspace preset.", + Description: "The preset ID is automatically generated and may change between runs. It is recommended to use the `name` attribute to identify the preset.", Computed: true, }, "name": { Type: schema.TypeString, - Description: "Name of the workspace preset.", + Description: "The name of the workspace preset.", Required: true, ValidateFunc: validation.StringIsNotEmpty, }, "parameters": { Type: schema.TypeMap, - Description: "Parameters of the workspace preset.", + Description: "Workspace parameters that will be set by the workspace preset. For simple templates that only need prebuilds, you may define a preset with zero parameters. Because workspace parameters may change between Coder template versions, preset parameters are allowed to define values for parameters that do not exist in the current template version.", Optional: true, Elem: &schema.Schema{ Type: schema.TypeString, @@ -73,13 +73,14 @@ func workspacePresetDataSource() *schema.Resource { }, "prebuilds": { Type: schema.TypeSet, - Description: "Prebuilds of the workspace preset.", + Description: "Prebuilt workspace configuration related to this workspace preset. Coder will build and maintain workspaces in reserve based on this configuration. When a user creates a new workspace using a preset, they will be assigned a prebuilt workspace, instead of waiting for a new workspace to build.", Optional: true, MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "instances": { Type: schema.TypeInt, + Description: "The number of workspaces to keep in reserve for this preset.", Required: true, ForceNew: true, ValidateFunc: validation.IntAtLeast(0),