-
Notifications
You must be signed in to change notification settings - Fork 22
feat: add autoscaling 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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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"` | ||||||
|
@@ -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, | ||||||
|
@@ -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\").", | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: can you include a link to an acceptable list of TZs here? I think https://en.wikipedia.org/wiki/List_of_tz_database_time_zones suffices but I'm not sure. https://pkg.go.dev/time#LoadLocation says it references the "IANA Time Zone database". |
||||||
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)} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
|
||||||
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 \"*\".", | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why must day-of-month and month 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 * | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It'd be good to add the why here, not just the what. |
||||||
func validatePrebuildsCronSpec(spec string) error { | ||||||
parts := strings.Fields(spec) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Howcome we don't use |
||||||
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 | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love that we explicitly don't have a default value here 👍 nice.