Skip to content

Commit d61894d

Browse files
feat: add autoscaling configuration for prebuilds
1 parent cfa101d commit d61894d

File tree

5 files changed

+378
-6
lines changed

5 files changed

+378
-6
lines changed

docs/data-sources/workspace_preset.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,27 @@ Required:
5454

5555
Optional:
5656

57+
- `autoscaling` (Block List, Max: 1) Configuration block that defines autoscaling behavior for prebuilds. Use this to automatically adjust the number of prebuild instances based on a schedule. (see [below for nested schema](#nestedblock--prebuilds--autoscaling))
5758
- `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))
5859

60+
<a id="nestedblock--prebuilds--autoscaling"></a>
61+
### Nested Schema for `prebuilds.autoscaling`
62+
63+
Required:
64+
65+
- `schedule` (Block List, Min: 1) One or more schedule blocks that define when to scale the number of prebuild instances. (see [below for nested schema](#nestedblock--prebuilds--autoscaling--schedule))
66+
- `timezone` (String) The timezone to use for the autoscaling schedule (e.g., "UTC", "America/New_York").
67+
68+
<a id="nestedblock--prebuilds--autoscaling--schedule"></a>
69+
### Nested Schema for `prebuilds.autoscaling.schedule`
70+
71+
Required:
72+
73+
- `cron` (String) A cron expression that defines when this schedule should be active. The cron expression must be in the format "* HOUR * * DAY-OF-WEEK" where HOUR is 0-23 and DAY-OF-WEEK is 0-6 (Sunday-Saturday). The minute, day-of-month, and month fields must be "*".
74+
- `instances` (Number) The number of prebuild instances to maintain during this schedule period.
75+
76+
77+
5978
<a id="nestedblock--prebuilds--expiration_policy"></a>
6079
### Nested Schema for `prebuilds.expiration_policy`
6180

integration/integration_test.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,17 @@ 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`,
98-
"workspace_preset.prebuilds.expiration_policy.ttl": `86400`,
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`,
99+
"workspace_preset.prebuilds.autoscaling.timezone": `UTC`,
100+
"workspace_preset.prebuilds.autoscaling.schedule0.cron": `\* 8-18 \* \* 1-5`,
101+
"workspace_preset.prebuilds.autoscaling.schedule0.instances": `3`,
102+
"workspace_preset.prebuilds.autoscaling.schedule1.cron": `\* 8-14 \* \* 6`,
103+
"workspace_preset.prebuilds.autoscaling.schedule1.instances": `1`,
99104
},
100105
},
101106
{

integration/test-data-source/main.tf

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ data "coder_workspace_preset" "preset" {
3030
expiration_policy {
3131
ttl = 86400
3232
}
33+
autoscaling {
34+
timezone = "UTC"
35+
schedule {
36+
cron = "* 8-18 * * 1-5"
37+
instances = 3
38+
}
39+
schedule {
40+
cron = "* 8-14 * * 6"
41+
instances = 1
42+
}
43+
}
3344
}
3445
}
3546

@@ -56,6 +67,11 @@ locals {
5667
"workspace_preset.parameters.param" : data.coder_workspace_preset.preset.parameters.param,
5768
"workspace_preset.prebuilds.instances" : tostring(one(data.coder_workspace_preset.preset.prebuilds).instances),
5869
"workspace_preset.prebuilds.expiration_policy.ttl" : tostring(one(one(data.coder_workspace_preset.preset.prebuilds).expiration_policy).ttl),
70+
"workspace_preset.prebuilds.autoscaling.timezone" : tostring(one(one(data.coder_workspace_preset.preset.prebuilds).autoscaling).timezone),
71+
"workspace_preset.prebuilds.autoscaling.schedule0.cron" : tostring(one(one(data.coder_workspace_preset.preset.prebuilds).autoscaling).schedule[0].cron),
72+
"workspace_preset.prebuilds.autoscaling.schedule0.instances" : tostring(one(one(data.coder_workspace_preset.preset.prebuilds).autoscaling).schedule[0].instances),
73+
"workspace_preset.prebuilds.autoscaling.schedule1.cron" : tostring(one(one(data.coder_workspace_preset.preset.prebuilds).autoscaling).schedule[1].cron),
74+
"workspace_preset.prebuilds.autoscaling.schedule1.instances" : tostring(one(one(data.coder_workspace_preset.preset.prebuilds).autoscaling).schedule[1].instances),
5975
}
6076
}
6177

provider/workspace_preset.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,18 @@ package provider
33
import (
44
"context"
55
"fmt"
6+
"strings"
7+
"time"
68

79
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
810
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
911
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
1012
"github.com/mitchellh/mapstructure"
13+
rbcron "github.com/robfig/cron/v3"
1114
)
1215

16+
var PrebuildsCRONParser = rbcron.NewParser(rbcron.Minute | rbcron.Hour | rbcron.Dom | rbcron.Month | rbcron.Dow)
17+
1318
type WorkspacePreset struct {
1419
Name string `mapstructure:"name"`
1520
Parameters map[string]string `mapstructure:"parameters"`
@@ -29,12 +34,23 @@ type WorkspacePrebuild struct {
2934
// for utilities that parse our terraform output using this type. To remain compatible
3035
// with those cases, we use a slice here.
3136
ExpirationPolicy []ExpirationPolicy `mapstructure:"expiration_policy"`
37+
Autoscaling []Autoscaling `mapstructure:"autoscaling"`
3238
}
3339

3440
type ExpirationPolicy struct {
3541
TTL int `mapstructure:"ttl"`
3642
}
3743

44+
type Autoscaling struct {
45+
Timezone string `mapstructure:"timezone"`
46+
Schedule []Schedule `mapstructure:"schedule"`
47+
}
48+
49+
type Schedule struct {
50+
Cron string `mapstructure:"cron"`
51+
Instances int `mapstructure:"instances"`
52+
}
53+
3854
func workspacePresetDataSource() *schema.Resource {
3955
return &schema.Resource{
4056
SchemaVersion: 1,
@@ -119,9 +135,82 @@ func workspacePresetDataSource() *schema.Resource {
119135
},
120136
},
121137
},
138+
"autoscaling": {
139+
Type: schema.TypeList,
140+
Description: "Configuration block that defines autoscaling behavior for prebuilds. Use this to automatically adjust the number of prebuild instances based on a schedule.",
141+
Optional: true,
142+
MaxItems: 1,
143+
Elem: &schema.Resource{
144+
Schema: map[string]*schema.Schema{
145+
"timezone": {
146+
Type: schema.TypeString,
147+
Description: "The timezone to use for the autoscaling schedule (e.g., \"UTC\", \"America/New_York\").",
148+
Required: true,
149+
ValidateFunc: func(val interface{}, key string) ([]string, []error) {
150+
timezone := val.(string)
151+
152+
_, err := time.LoadLocation(timezone)
153+
if err != nil {
154+
return nil, []error{fmt.Errorf("failed to load location: %w", err)}
155+
}
156+
157+
return nil, nil
158+
},
159+
},
160+
"schedule": {
161+
Type: schema.TypeList,
162+
Description: "One or more schedule blocks that define when to scale the number of prebuild instances.",
163+
Required: true,
164+
MinItems: 1,
165+
Elem: &schema.Resource{
166+
Schema: map[string]*schema.Schema{
167+
"cron": {
168+
Type: schema.TypeString,
169+
Description: "A cron expression that defines when this schedule should be active. The cron expression must be in the format \"* HOUR * * DAY-OF-WEEK\" where HOUR is 0-23 and DAY-OF-WEEK is 0-6 (Sunday-Saturday). The minute, day-of-month, and month fields must be \"*\".",
170+
Required: true,
171+
ValidateFunc: func(val interface{}, key string) ([]string, []error) {
172+
cronSpec := val.(string)
173+
174+
err := validatePrebuildsCronSpec(cronSpec)
175+
if err != nil {
176+
return nil, []error{fmt.Errorf("cron spec failed validation: %w", err)}
177+
}
178+
179+
_, err = PrebuildsCRONParser.Parse(cronSpec)
180+
if err != nil {
181+
return nil, []error{fmt.Errorf("failed to parse cron spec: %w", err)}
182+
}
183+
184+
return nil, nil
185+
},
186+
},
187+
"instances": {
188+
Type: schema.TypeInt,
189+
Description: "The number of prebuild instances to maintain during this schedule period.",
190+
Required: true,
191+
},
192+
},
193+
},
194+
},
195+
},
196+
},
197+
},
122198
},
123199
},
124200
},
125201
},
126202
}
127203
}
204+
205+
// validatePrebuildsCronSpec ensures that the minute, day-of-month and month options of spec are all set to *
206+
func validatePrebuildsCronSpec(spec string) error {
207+
parts := strings.Fields(spec)
208+
if len(parts) != 5 {
209+
return fmt.Errorf("cron specification should consist of 5 fields")
210+
}
211+
if parts[0] != "*" || parts[2] != "*" || parts[3] != "*" {
212+
return fmt.Errorf("minute, day-of-month and month should be *")
213+
}
214+
215+
return nil
216+
}

0 commit comments

Comments
 (0)