Skip to content

Commit ef92eea

Browse files
authored
feat: allow presets to define prebuilds (#373)
* feat: allow presets to define prebuilds * document prebuild parameters * remove todo * make gen * Add integration testing * Allow presets to define 0 parameters * additional documentation for prebuilds
1 parent 9a74558 commit ef92eea

File tree

8 files changed

+124
-24
lines changed

8 files changed

+124
-24
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ to setup your local Terraform to use your local version rather than the registry
4747
}
4848
```
4949
2. Run `terraform init` and observe a warning like `Warning: Provider development overrides are in effect`
50-
4. Run `go build -o terraform-provider-coder` to build the provider binary, which Terraform will try locate and execute
50+
4. Run `make build` to build the provider binary, which Terraform will try locate and execute
5151
5. All local Terraform runs will now use your local provider!
5252
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._
5353

docs/data-sources/workspace.md

+2
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ resource "docker_container" "workspace" {
6969
- `access_port` (Number) The access port of the Coder deployment provisioning this workspace.
7070
- `access_url` (String) The access URL of the Coder deployment provisioning this workspace.
7171
- `id` (String) UUID of the workspace.
72+
- `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.
7273
- `name` (String) Name of the workspace.
74+
- `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.
7375
- `start_count` (Number) A computed count based on `transition` state. If `start`, count will equal 1.
7476
- `template_id` (String) ID of the workspace's template.
7577
- `template_name` (String) Name of the workspace's template.

docs/data-sources/workspace_preset.md

+16-5
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
page_title: "coder_workspace_preset Data Source - terraform-provider-coder"
44
subcategory: ""
55
description: |-
6-
Use this data source to predefine common configurations for workspaces.
6+
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.
77
---
88

99
# coder_workspace_preset (Data Source)
1010

11-
Use this data source to predefine common configurations for workspaces.
11+
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.
1212

1313
## Example Usage
1414

@@ -34,9 +34,20 @@ data "coder_workspace_preset" "example" {
3434

3535
### Required
3636

37-
- `name` (String) Name of the workspace preset.
38-
- `parameters` (Map of String) Parameters of the workspace preset.
37+
- `name` (String) The name of the workspace preset.
38+
39+
### Optional
40+
41+
- `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.
42+
- `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))
3943

4044
### Read-Only
4145

42-
- `id` (String) ID of the workspace preset.
46+
- `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.
47+
48+
<a id="nestedblock--prebuilds"></a>
49+
### Nested Schema for `prebuilds`
50+
51+
Required:
52+
53+
- `instances` (Number) The number of workspaces to keep in reserve for this preset.

integration/integration_test.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,11 @@ func TestIntegration(t *testing.T) {
9090
// TODO (sasswart): the cli doesn't support presets yet.
9191
// once it does, the value for workspace_parameter.value
9292
// will be the preset value.
93-
"workspace_parameter.value": `param value`,
94-
"workspace_parameter.icon": `param icon`,
95-
"workspace_preset.name": `preset`,
96-
"workspace_preset.parameters.param": `preset param value`,
93+
"workspace_parameter.value": `param value`,
94+
"workspace_parameter.icon": `param icon`,
95+
"workspace_preset.name": `preset`,
96+
"workspace_preset.parameters.param": `preset param value`,
97+
"workspace_preset.prebuilds.instances": `1`,
9798
},
9899
},
99100
{

integration/test-data-source/main.tf

+5
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ data "coder_workspace_preset" "preset" {
2424
parameters = {
2525
(data.coder_parameter.param.name) = "preset param value"
2626
}
27+
28+
prebuilds {
29+
instances = 1
30+
}
2731
}
2832

2933
locals {
@@ -47,6 +51,7 @@ locals {
4751
"workspace_parameter.icon" : data.coder_parameter.param.icon,
4852
"workspace_preset.name" : data.coder_workspace_preset.preset.name,
4953
"workspace_preset.parameters.param" : data.coder_workspace_preset.preset.parameters.param,
54+
"workspace_preset.prebuilds.instances" : tostring(one(data.coder_workspace_preset.preset.prebuilds).instances),
5055
}
5156
}
5257

provider/workspace.go

+22
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ func workspaceDataSource() *schema.Resource {
2727
}
2828
_ = rd.Set("start_count", count)
2929

30+
prebuild := helpers.OptionalEnv(IsPrebuildEnvironmentVariable())
31+
prebuildCount := 0
32+
if prebuild == "true" {
33+
prebuildCount = 1
34+
_ = rd.Set("is_prebuild", true)
35+
}
36+
_ = rd.Set("prebuild_count", prebuildCount)
37+
3038
name := helpers.OptionalEnvOrDefault("CODER_WORKSPACE_NAME", "default")
3139
rd.Set("name", name)
3240

@@ -83,6 +91,11 @@ func workspaceDataSource() *schema.Resource {
8391
Computed: true,
8492
Description: "The access port of the Coder deployment provisioning this workspace.",
8593
},
94+
"prebuild_count": {
95+
Type: schema.TypeInt,
96+
Computed: true,
97+
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.",
98+
},
8699
"start_count": {
87100
Type: schema.TypeInt,
88101
Computed: true,
@@ -98,6 +111,11 @@ func workspaceDataSource() *schema.Resource {
98111
Computed: true,
99112
Description: "UUID of the workspace.",
100113
},
114+
"is_prebuild": {
115+
Type: schema.TypeBool,
116+
Computed: true,
117+
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.",
118+
},
101119
"name": {
102120
Type: schema.TypeString,
103121
Computed: true,
@@ -121,3 +139,7 @@ func workspaceDataSource() *schema.Resource {
121139
},
122140
}
123141
}
142+
143+
func IsPrebuildEnvironmentVariable() string {
144+
return "CODER_WORKSPACE_IS_PREBUILD"
145+
}

provider/workspace_preset.go

+35-12
Original file line numberDiff line numberDiff line change
@@ -12,59 +12,82 @@ import (
1212
type WorkspacePreset struct {
1313
Name string `mapstructure:"name"`
1414
Parameters map[string]string `mapstructure:"parameters"`
15+
Prebuilds WorkspacePrebuild `mapstructure:"prebuilds"`
16+
}
17+
18+
type WorkspacePrebuild struct {
19+
Instances int `mapstructure:"instances"`
1520
}
1621

1722
func workspacePresetDataSource() *schema.Resource {
1823
return &schema.Resource{
1924
SchemaVersion: 1,
2025

21-
Description: "Use this data source to predefine common configurations for workspaces.",
26+
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.",
2227
ReadContext: func(ctx context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics {
2328
var preset WorkspacePreset
2429
err := mapstructure.Decode(struct {
2530
Name interface{}
2631
Parameters interface{}
32+
Prebuilds struct {
33+
Instances interface{}
34+
}
2735
}{
2836
Name: rd.Get("name"),
2937
Parameters: rd.Get("parameters"),
38+
Prebuilds: struct {
39+
Instances interface{}
40+
}{
41+
Instances: rd.Get("prebuilds.0.instances"),
42+
},
3043
}, &preset)
3144
if err != nil {
3245
return diag.Errorf("decode workspace preset: %s", err)
3346
}
3447

35-
// MinItems doesn't work with maps, so we need to check the length
36-
// of the map manually. All other validation is handled by the
37-
// schema.
38-
if len(preset.Parameters) == 0 {
39-
return diag.Errorf("expected \"parameters\" to not be an empty map")
40-
}
41-
4248
rd.SetId(preset.Name)
4349

4450
return nil
4551
},
4652
Schema: map[string]*schema.Schema{
4753
"id": {
4854
Type: schema.TypeString,
49-
Description: "ID of the workspace preset.",
55+
Description: "The preset ID is automatically generated and may change between runs. It is recommended to use the `name` attribute to identify the preset.",
5056
Computed: true,
5157
},
5258
"name": {
5359
Type: schema.TypeString,
54-
Description: "Name of the workspace preset.",
60+
Description: "The name of the workspace preset.",
5561
Required: true,
5662
ValidateFunc: validation.StringIsNotEmpty,
5763
},
5864
"parameters": {
5965
Type: schema.TypeMap,
60-
Description: "Parameters of the workspace preset.",
61-
Required: true,
66+
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.",
67+
Optional: true,
6268
Elem: &schema.Schema{
6369
Type: schema.TypeString,
6470
Required: true,
6571
ValidateFunc: validation.StringIsNotEmpty,
6672
},
6773
},
74+
"prebuilds": {
75+
Type: schema.TypeSet,
76+
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.",
77+
Optional: true,
78+
MaxItems: 1,
79+
Elem: &schema.Resource{
80+
Schema: map[string]*schema.Schema{
81+
"instances": {
82+
Type: schema.TypeInt,
83+
Description: "The number of workspaces to keep in reserve for this preset.",
84+
Required: true,
85+
ForceNew: true,
86+
ValidateFunc: validation.IntAtLeast(0),
87+
},
88+
},
89+
},
90+
},
6891
},
6992
}
7093
}

provider/workspace_preset_test.go

+38-2
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func TestWorkspacePreset(t *testing.T) {
8484
}`,
8585
// This validation is done by Terraform, but it could still break if we misconfigure the schema.
8686
// So we test it here to make sure we don't regress.
87-
ExpectError: regexp.MustCompile("The argument \"parameters\" is required, but no definition was found"),
87+
ExpectError: nil,
8888
},
8989
{
9090
Name: "Parameters field is empty",
@@ -95,7 +95,7 @@ func TestWorkspacePreset(t *testing.T) {
9595
}`,
9696
// This validation is *not* done by Terraform, because MinItems doesn't work with maps.
9797
// We've implemented the validation in ReadContext, so we test it here to make sure we don't regress.
98-
ExpectError: regexp.MustCompile("expected \"parameters\" to not be an empty map"),
98+
ExpectError: nil,
9999
},
100100
{
101101
Name: "Parameters field is not a map",
@@ -108,6 +108,42 @@ func TestWorkspacePreset(t *testing.T) {
108108
// So we test it here to make sure we don't regress.
109109
ExpectError: regexp.MustCompile("Inappropriate value for attribute \"parameters\": map of string required"),
110110
},
111+
{
112+
Name: "Prebuilds is set, but not its required fields",
113+
Config: `
114+
data "coder_workspace_preset" "preset_1" {
115+
name = "preset_1"
116+
parameters = {
117+
"region" = "us-east1-a"
118+
}
119+
prebuilds {}
120+
}`,
121+
ExpectError: regexp.MustCompile("The argument \"instances\" is required, but no definition was found."),
122+
},
123+
{
124+
Name: "Prebuilds is set, and so are its required fields",
125+
Config: `
126+
data "coder_workspace_preset" "preset_1" {
127+
name = "preset_1"
128+
parameters = {
129+
"region" = "us-east1-a"
130+
}
131+
prebuilds {
132+
instances = 1
133+
}
134+
}`,
135+
ExpectError: nil,
136+
Check: func(state *terraform.State) error {
137+
require.Len(t, state.Modules, 1)
138+
require.Len(t, state.Modules[0].Resources, 1)
139+
resource := state.Modules[0].Resources["data.coder_workspace_preset.preset_1"]
140+
require.NotNil(t, resource)
141+
attrs := resource.Primary.Attributes
142+
require.Equal(t, attrs["name"], "preset_1")
143+
require.Equal(t, attrs["prebuilds.0.instances"], "1")
144+
return nil
145+
},
146+
},
111147
}
112148

113149
for _, testcase := range testcases {

0 commit comments

Comments
 (0)