Skip to content

Commit deeea0e

Browse files
feat: add validation for cron spec
1 parent 9370443 commit deeea0e

File tree

2 files changed

+68
-0
lines changed

2 files changed

+68
-0
lines changed

provider/workspace_preset.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ package provider
33
import (
44
"context"
55
"fmt"
6+
"strings"
67

78
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
89
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
910
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
1011
"github.com/mitchellh/mapstructure"
12+
rbcron "github.com/robfig/cron/v3"
1113
)
1214

15+
var PrebuildsCRONParser = rbcron.NewParser(rbcron.Minute | rbcron.Hour | rbcron.Dom | rbcron.Month | rbcron.Dow)
16+
1317
type WorkspacePreset struct {
1418
Name string `mapstructure:"name"`
1519
Parameters map[string]string `mapstructure:"parameters"`
@@ -149,6 +153,21 @@ func workspacePresetDataSource() *schema.Resource {
149153
"cron": {
150154
Type: schema.TypeString,
151155
Required: true,
156+
ValidateFunc: func(val interface{}, key string) ([]string, []error) {
157+
cronSpec := val.(string)
158+
159+
err := validatePrebuildsCronSpec(cronSpec)
160+
if err != nil {
161+
return nil, []error{fmt.Errorf("cron spec failed validation: %w", err)}
162+
}
163+
164+
_, err = PrebuildsCRONParser.Parse(cronSpec)
165+
if err != nil {
166+
return nil, []error{fmt.Errorf("failed to parse cron spec: %w", err)}
167+
}
168+
169+
return nil, nil
170+
},
152171
},
153172
"instances": {
154173
Type: schema.TypeInt,
@@ -166,3 +185,16 @@ func workspacePresetDataSource() *schema.Resource {
166185
},
167186
}
168187
}
188+
189+
// validatePrebuildsCronSpec ensures that the minute, day-of-month and month options of spec are all set to *
190+
func validatePrebuildsCronSpec(spec string) error {
191+
parts := strings.Fields(spec)
192+
if len(parts) != 5 {
193+
return fmt.Errorf("cron specification should consist of 5 fields")
194+
}
195+
if parts[0] != "*" || parts[2] != "*" || parts[3] != "*" {
196+
return fmt.Errorf("minute, day-of-month and month should be *")
197+
}
198+
199+
return nil
200+
}

provider/workspace_preset_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,42 @@ func TestWorkspacePreset(t *testing.T) {
426426
return nil
427427
},
428428
},
429+
{
430+
Name: "Prebuilds is set with an autoscaling.schedule field, but the cron includes a disallowed minute field",
431+
Config: `
432+
data "coder_workspace_preset" "preset_1" {
433+
name = "preset_1"
434+
prebuilds {
435+
instances = 1
436+
autoscaling {
437+
timezone = "UTC"
438+
schedule {
439+
cron = "30 8-18 * * 1-5"
440+
instances = "1"
441+
}
442+
}
443+
}
444+
}`,
445+
ExpectError: regexp.MustCompile(`cron spec failed validation: minute, day-of-month and month should be *`),
446+
},
447+
{
448+
Name: "Prebuilds is set with an autoscaling.schedule field, but the cron hour field is invalid",
449+
Config: `
450+
data "coder_workspace_preset" "preset_1" {
451+
name = "preset_1"
452+
prebuilds {
453+
instances = 1
454+
autoscaling {
455+
timezone = "UTC"
456+
schedule {
457+
cron = "* 25-26 * * 1-5"
458+
instances = "1"
459+
}
460+
}
461+
}
462+
}`,
463+
ExpectError: regexp.MustCompile(`failed to parse cron spec: end of range \(26\) above maximum \(23\): 25-26`),
464+
},
429465
}
430466

431467
for _, testcase := range testcases {

0 commit comments

Comments
 (0)