Skip to content

Commit 28dae7f

Browse files
authored
feat: add expiration_policy parameter to prebuild resource (#404)
* feat: add cache_invalidation parameter to prebuild resource * fix: formatting * fix: remove TODO comment * fix: cache_invalidation block in integration tests * fix: main.tf integration test * fix: test Prebuilds_is_set_with_a_cache_invalidation_field_without_its_required_fields * fix: remove printf * chore: add comment about partial match in preset test * chore: add comment about cache_invalidation struct as slice * chore: add max validation of 604800 seconds (7 days) for invalidate_after_secs to prevent stale prebuilds * chores: update terraform schema to expiration_policy.ttl & update max validation ttl value to 1 year * fix: comment
1 parent 7489953 commit 28dae7f

File tree

5 files changed

+126
-5
lines changed

5 files changed

+126
-5
lines changed

docs/data-sources/workspace_preset.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,14 @@ data "coder_workspace_preset" "example" {
5151
Required:
5252

5353
- `instances` (Number) The number of workspaces to keep in reserve for this preset.
54+
55+
Optional:
56+
57+
- `expiration_policy` (Block Set, Max: 1) Configuration block that defines TTL (time-to-live) behavior for prebuilds. Use this to automatically invalidate and delete prebuilds after a certain period, ensuring they stay up-to-date. (see [below for nested schema](#nestedblock--prebuilds--expiration_policy))
58+
59+
<a id="nestedblock--prebuilds--expiration_policy"></a>
60+
### Nested Schema for `prebuilds.expiration_policy`
61+
62+
Required:
63+
64+
- `ttl` (Number) Time in seconds after which an unclaimed prebuild is considered expired and eligible for cleanup.

integration/integration_test.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,12 @@ 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`,
97-
"workspace_preset.prebuilds.instances": `1`,
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`,
98+
"workspace_preset.prebuilds.expiration_policy.ttl": `86400`,
9899
},
99100
},
100101
{

integration/test-data-source/main.tf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ data "coder_workspace_preset" "preset" {
2727

2828
prebuilds {
2929
instances = 1
30+
expiration_policy {
31+
ttl = 86400
32+
}
3033
}
3134
}
3235

@@ -52,6 +55,7 @@ locals {
5255
"workspace_preset.name" : data.coder_workspace_preset.preset.name,
5356
"workspace_preset.parameters.param" : data.coder_workspace_preset.preset.parameters.param,
5457
"workspace_preset.prebuilds.instances" : tostring(one(data.coder_workspace_preset.preset.prebuilds).instances),
58+
"workspace_preset.prebuilds.expiration_policy.ttl" : tostring(one(one(data.coder_workspace_preset.preset.prebuilds).expiration_policy).ttl),
5559
}
5660
}
5761

provider/workspace_preset.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ type WorkspacePreset struct {
2222

2323
type WorkspacePrebuild struct {
2424
Instances int `mapstructure:"instances"`
25+
// There should always be only one expiration_policy block, but Terraform's type system
26+
// still parses them as a slice, so we need to handle it as such. We could use
27+
// an anonymous type and rd.Get to avoid a slice here, but that would not be possible
28+
// for utilities that parse our terraform output using this type. To remain compatible
29+
// with those cases, we use a slice here.
30+
ExpirationPolicy []ExpirationPolicy `mapstructure:"expiration_policy"`
31+
}
32+
33+
type ExpirationPolicy struct {
34+
TTL int `mapstructure:"ttl"`
2535
}
2636

2737
func workspacePresetDataSource() *schema.Resource {
@@ -81,6 +91,24 @@ func workspacePresetDataSource() *schema.Resource {
8191
ForceNew: true,
8292
ValidateFunc: validation.IntAtLeast(0),
8393
},
94+
"expiration_policy": {
95+
Type: schema.TypeSet,
96+
Description: "Configuration block that defines TTL (time-to-live) behavior for prebuilds. Use this to automatically invalidate and delete prebuilds after a certain period, ensuring they stay up-to-date.",
97+
Optional: true,
98+
MaxItems: 1,
99+
Elem: &schema.Resource{
100+
Schema: map[string]*schema.Schema{
101+
"ttl": {
102+
Type: schema.TypeInt,
103+
Description: "Time in seconds after which an unclaimed prebuild is considered expired and eligible for cleanup.",
104+
Required: true,
105+
ForceNew: true,
106+
// Ensure TTL is between 0 and 31536000 seconds (1 year) to prevent stale prebuilds
107+
ValidateFunc: validation.IntBetween(0, 31536000),
108+
},
109+
},
110+
},
111+
},
84112
},
85113
},
86114
},

provider/workspace_preset_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,83 @@ func TestWorkspacePreset(t *testing.T) {
144144
return nil
145145
},
146146
},
147+
{
148+
Name: "Prebuilds is set with a expiration_policy field without its required fields",
149+
Config: `
150+
data "coder_workspace_preset" "preset_1" {
151+
name = "preset_1"
152+
parameters = {
153+
"region" = "us-east1-a"
154+
}
155+
prebuilds {
156+
instances = 1
157+
expiration_policy {}
158+
}
159+
}`,
160+
ExpectError: regexp.MustCompile("The argument \"ttl\" is required, but no definition was found."),
161+
},
162+
{
163+
Name: "Prebuilds is set with a expiration_policy field with its required fields",
164+
Config: `
165+
data "coder_workspace_preset" "preset_1" {
166+
name = "preset_1"
167+
parameters = {
168+
"region" = "us-east1-a"
169+
}
170+
prebuilds {
171+
instances = 1
172+
expiration_policy {
173+
ttl = 86400
174+
}
175+
}
176+
}`,
177+
ExpectError: nil,
178+
Check: func(state *terraform.State) error {
179+
require.Len(t, state.Modules, 1)
180+
require.Len(t, state.Modules[0].Resources, 1)
181+
resource := state.Modules[0].Resources["data.coder_workspace_preset.preset_1"]
182+
require.NotNil(t, resource)
183+
attrs := resource.Primary.Attributes
184+
require.Equal(t, attrs["name"], "preset_1")
185+
require.Equal(t, attrs["prebuilds.0.expiration_policy.0.ttl"], "86400")
186+
return nil
187+
},
188+
},
189+
{
190+
Name: "Prebuilds block with expiration_policy.ttl set to 2 years (exceeds 1 year limit)",
191+
Config: `
192+
data "coder_workspace_preset" "preset_1" {
193+
name = "preset_1"
194+
parameters = {
195+
"region" = "us-east1-a"
196+
}
197+
prebuilds {
198+
instances = 1
199+
expiration_policy {
200+
ttl = 63072000
201+
}
202+
}
203+
}`,
204+
ExpectError: regexp.MustCompile(`expected prebuilds.0.expiration_policy.0.ttl to be in the range \(0 - 31536000\), got 63072000`),
205+
},
206+
{
207+
Name: "Prebuilds is set with a expiration_policy field with its required fields and an unexpected argument",
208+
Config: `
209+
data "coder_workspace_preset" "preset_1" {
210+
name = "preset_1"
211+
parameters = {
212+
"region" = "us-east1-a"
213+
}
214+
prebuilds {
215+
instances = 1
216+
expiration_policy {
217+
ttl = 86400
218+
invalid_argument = "test"
219+
}
220+
}
221+
}`,
222+
ExpectError: regexp.MustCompile("An argument named \"invalid_argument\" is not expected here."),
223+
},
147224
}
148225

149226
for _, testcase := range testcases {

0 commit comments

Comments
 (0)