Skip to content

feat: add scheduling configuration for prebuilds #408

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Jun 18, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/data-sources/workspace_preset.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,27 @@ Required:

Optional:

- `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))
- `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))

<a id="nestedblock--prebuilds--autoscaling"></a>
### Nested Schema for `prebuilds.autoscaling`

Required:

- `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))
- `timezone` (String) The timezone to use for the autoscaling schedule (e.g., "UTC", "America/New_York").

<a id="nestedblock--prebuilds--autoscaling--schedule"></a>
### Nested Schema for `prebuilds.autoscaling.schedule`

Required:

- `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 "*".
- `instances` (Number) The number of prebuild instances to maintain during this schedule period.



<a id="nestedblock--prebuilds--expiration_policy"></a>
### Nested Schema for `prebuilds.expiration_policy`

Expand Down
17 changes: 11 additions & 6 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,17 @@ 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_preset.prebuilds.instances": `1`,
"workspace_preset.prebuilds.expiration_policy.ttl": `86400`,
"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`,
"workspace_preset.prebuilds.expiration_policy.ttl": `86400`,
"workspace_preset.prebuilds.autoscaling.timezone": `UTC`,
"workspace_preset.prebuilds.autoscaling.schedule0.cron": `\* 8-18 \* \* 1-5`,
"workspace_preset.prebuilds.autoscaling.schedule0.instances": `3`,
"workspace_preset.prebuilds.autoscaling.schedule1.cron": `\* 8-14 \* \* 6`,
"workspace_preset.prebuilds.autoscaling.schedule1.instances": `1`,
},
},
{
Expand Down
16 changes: 16 additions & 0 deletions integration/test-data-source/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ data "coder_workspace_preset" "preset" {
expiration_policy {
ttl = 86400
}
autoscaling {
timezone = "UTC"
schedule {
cron = "* 8-18 * * 1-5"
instances = 3
}
schedule {
cron = "* 8-14 * * 6"
instances = 1
}
}
}
}

Expand All @@ -56,6 +67,11 @@ locals {
"workspace_preset.parameters.param" : data.coder_workspace_preset.preset.parameters.param,
"workspace_preset.prebuilds.instances" : tostring(one(data.coder_workspace_preset.preset.prebuilds).instances),
"workspace_preset.prebuilds.expiration_policy.ttl" : tostring(one(one(data.coder_workspace_preset.preset.prebuilds).expiration_policy).ttl),
"workspace_preset.prebuilds.autoscaling.timezone" : tostring(one(one(data.coder_workspace_preset.preset.prebuilds).autoscaling).timezone),
"workspace_preset.prebuilds.autoscaling.schedule0.cron" : tostring(one(one(data.coder_workspace_preset.preset.prebuilds).autoscaling).schedule[0].cron),
"workspace_preset.prebuilds.autoscaling.schedule0.instances" : tostring(one(one(data.coder_workspace_preset.preset.prebuilds).autoscaling).schedule[0].instances),
"workspace_preset.prebuilds.autoscaling.schedule1.cron" : tostring(one(one(data.coder_workspace_preset.preset.prebuilds).autoscaling).schedule[1].cron),
"workspace_preset.prebuilds.autoscaling.schedule1.instances" : tostring(one(one(data.coder_workspace_preset.preset.prebuilds).autoscaling).schedule[1].instances),
}
}

Expand Down
89 changes: 89 additions & 0 deletions provider/workspace_preset.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@ package provider
import (
"context"
"fmt"
"strings"
"time"

"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"
rbcron "github.com/robfig/cron/v3"
)

var PrebuildsCRONParser = rbcron.NewParser(rbcron.Minute | rbcron.Hour | rbcron.Dom | rbcron.Month | rbcron.Dow)

type WorkspacePreset struct {
Name string `mapstructure:"name"`
Parameters map[string]string `mapstructure:"parameters"`
Expand All @@ -29,12 +34,23 @@ type WorkspacePrebuild struct {
// for utilities that parse our terraform output using this type. To remain compatible
// with those cases, we use a slice here.
ExpirationPolicy []ExpirationPolicy `mapstructure:"expiration_policy"`
Autoscaling []Autoscaling `mapstructure:"autoscaling"`
}

type ExpirationPolicy struct {
TTL int `mapstructure:"ttl"`
}

type Autoscaling struct {
Timezone string `mapstructure:"timezone"`
Schedule []Schedule `mapstructure:"schedule"`
}

type Schedule struct {
Cron string `mapstructure:"cron"`
Instances int `mapstructure:"instances"`
}

func workspacePresetDataSource() *schema.Resource {
return &schema.Resource{
SchemaVersion: 1,
Expand Down Expand Up @@ -119,9 +135,82 @@ func workspacePresetDataSource() *schema.Resource {
},
},
},
"autoscaling": {
Type: schema.TypeList,
Description: "Configuration block that defines autoscaling behavior for prebuilds. Use this to automatically adjust the number of prebuild instances based on a schedule.",
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"timezone": {
Type: schema.TypeString,
Description: "The timezone to use for the autoscaling schedule (e.g., \"UTC\", \"America/New_York\").",
Required: true,
ValidateFunc: func(val interface{}, key string) ([]string, []error) {
timezone := val.(string)

_, err := time.LoadLocation(timezone)
if err != nil {
return nil, []error{fmt.Errorf("failed to load location: %w", err)}
}

return nil, nil
},
},
"schedule": {
Type: schema.TypeList,
Description: "One or more schedule blocks that define when to scale the number of prebuild instances.",
Required: true,
MinItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"cron": {
Type: schema.TypeString,
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 \"*\".",
Required: true,
ValidateFunc: func(val interface{}, key string) ([]string, []error) {
cronSpec := val.(string)

err := validatePrebuildsCronSpec(cronSpec)
if err != nil {
return nil, []error{fmt.Errorf("cron spec failed validation: %w", err)}
}

_, err = PrebuildsCRONParser.Parse(cronSpec)
if err != nil {
return nil, []error{fmt.Errorf("failed to parse cron spec: %w", err)}
}

return nil, nil
},
},
"instances": {
Type: schema.TypeInt,
Description: "The number of prebuild instances to maintain during this schedule period.",
Required: true,
},
},
},
},
},
},
},
},
},
},
},
}
}

// validatePrebuildsCronSpec ensures that the minute, day-of-month and month options of spec are all set to *
func validatePrebuildsCronSpec(spec string) error {
parts := strings.Fields(spec)
if len(parts) != 5 {
return fmt.Errorf("cron specification should consist of 5 fields")
}
if parts[0] != "*" || parts[2] != "*" || parts[3] != "*" {
return fmt.Errorf("minute, day-of-month and month should be *")
}

return nil
}
Loading
Loading