8000 docs: clarify cron attribute format for coder_script resource (#409) · coder/terraform-provider-coder@0ce611a · GitHub
[go: up one dir, main page]

Skip to content

Commit 0ce611a

Browse files
docs: clarify cron attribute format for coder_script resource (#409)
Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
1 parent e6bbd8c commit 0ce611a

File tree

4 files changed

+130
-13
lines changed

4 files changed

+130
-13
lines changed

docs/resources/script.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,26 @@ resource "coder_script" "code-server" {
4343
})
4444
}
4545
46-
resource "coder_script" "nightly_sleep_reminder" {
46+
resource "coder_script" "nightly_update" {
4747
agent_id = coder_agent.dev.agent_id
4848
display_name = "Nightly update"
4949
icon = "/icon/database.svg"
50-
cron = "0 22 * * *"
50+
cron = "0 0 22 * * *" # Run at 22:00 (10 PM) every day
5151
script = <<EOF
5252
#!/bin/sh
5353
echo "Running nightly update"
54-
sudo apt-get install
54+
sudo apt-get update
55+
EOF
56+
}
57+
58+
resource "coder_script" "every_5_minutes" {
59+
agent_id = coder_agent.dev.agent_id
60+
display_name = "Health check"
61+
icon = "/icon/heart.svg"
62+
cron = "0 */5 * * * *" # Run every 5 minutes
63+
script = <<EOF
64+
#!/bin/sh
65+
echo "Health check at $(date)"
5566
EOF
5667
}
5768
@@ -78,7 +89,7 @@ resource "coder_script" "shutdown" {
7889

7990
### Optional
8091

81-
- `cron` (String) The cron schedule to run the script on. This is a cron expression.
92+
- `cron` (String) The cron schedule to run the script on. This uses a 6-field cron expression format: `seconds minutes hours day-of-month month day-of-week`. Note that this differs from the standard Unix 5-field format by including seconds as the first field. Examples: `"0 0 22 * * *"` (daily at 10 PM), `"0 */5 * * * *"` (every 5 minutes), `"30 0 9 * * 1-5"` (weekdays at 9:30 AM).
8293
- `icon` (String) A URL to an icon that will display in the dashboard. View built-in icons [here](https://github.com/coder/coder/tree/main/site/static/icon). Use a built-in icon with `"${data.coder_workspace.me.access_url}/icon/<path>"`.
8394
- `log_path` (String) The path of a file to write the logs to. If relative, it will be appended to tmp.
8495
- `run_on_start` (Boolean) This option defines whether or not the script should run when the agent starts. The script should exit when it is done to signal that the agent is ready.

examples/resources/coder_script/resource.tf

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,26 @@ resource "coder_script" "code-server" {
2828
})
2929
}
3030

31-
resource "coder_script" "nightly_sleep_reminder" {
31+
resource "coder_script" "nightly_update" {
3232
agent_id = coder_agent.dev.agent_id
3333
display_name = "Nightly update"
3434
icon = "/icon/database.svg"
35-
cron = "0 22 * * *"
35+
cron = "0 0 22 * * *" # Run at 22:00 (10 PM) every day
3636
script = <<EOF
3737
#!/bin/sh
3838
echo "Running nightly update"
39-
sudo apt-get install
39+
sudo apt-get update
40+
EOF
41+
}
42+
43+
resource "coder_script" "every_5_minutes" {
44+
agent_id = coder_agent.dev.agent_id
45+
display_name = "Health check"
46+
icon = "/icon/heart.svg"
47+
cron = "0 */5 * * * *" # Run every 5 minutes
48+
script = <<EOF
49+
#!/bin/sh
50+
echo "Health check at $(date)"
4051
EOF
4152
}
4253

provider/script.go

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

78
"github.com/google/uuid"
89
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
@@ -13,6 +14,32 @@ import (
1314

1415
var ScriptCRONParser = cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.DowOptional | cron.Descriptor)
1516

17+
// ValidateCronExpression validates a cron expression and provides helpful warnings for common mistakes
18+
func ValidateCronExpression(cronExpr string) (warnings []string, errors []error) {
19+
// Check if it looks like a 5-field Unix cron expression
20+
fields := strings.Fields(cronExpr)
21+
if len(fields) == 5 {
22+
// Try to parse as standard Unix cron (without seconds)
23+
unixParser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.DowOptional | cron.Descriptor)
24+
if _, err := unixParser.Parse(cronExpr); err == nil {
25+
// It's a valid 5-field expression, provide a helpful warning
26+
warnings = append(warnings, fmt.Sprintf(
27+
"The cron expression '%s' appears to be in Unix 5-field format. "+
28+
"Coder uses 6-field format (seconds minutes hours day month day-of-week). "+
29+
"Consider prefixing with '0 ' to run at the start of each minute: '0 %s'",
30+
cronExpr, cronExpr))
31+
}
32+
}
33+
34+
// Validate with the actual 6-field parser
35+
_, err := ScriptCRONParser.Parse(cronExpr)
36+
if err != nil {
37+
errors = append(errors, fmt.Errorf("%s is not a valid cron expression: %w", cronExpr, err))
38+
}
39+
40+
return warnings, errors
41+
}
42+
1643
func scriptResource() *schema.Resource {
1744
return &schema.Resource{
1845
SchemaVersion: 1,
@@ -72,17 +99,13 @@ func scriptResource() *schema.Resource {
7299
ForceNew: true,
73100
Type: schema.TypeString,
74101
Optional: true,
75-
Description: "The cron schedule to run the script on. This is a cron expression.",
102+
Description: "The cron schedule to run the script on. This uses a 6-field cron expression format: `seconds minutes hours day-of-month month day-of-week`. Note that this differs from the standard Unix 5-field format by including seconds as the first field. Examples: `\"0 0 22 * * *\"` (daily at 10 PM), `\"0 */5 * * * *\"` (every 5 minutes), `\"30 0 9 * * 1-5\"` (weekdays at 9:30 AM).",
76103
ValidateFunc: func(i interface{}, _ string) ([]string, []error) {
77104
v, ok := i.(string)
78105
if !ok {
79106
return []string{}, []error{fmt.Errorf("got type %T instead of string", i)}
80107
}
81-
_, err := ScriptCRONParser.Parse(v)
82-
if err != nil {
83-
return []string{}, []error{fmt.Errorf("%s is not a valid cron expression: %w", v, err)}
84-
}
85-
return nil, nil
108+
return ValidateCronExpression(v)
86109
},
87110
},
88111
"start_blocks_login": {

provider/script_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88

99
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
1010
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
11+
12+
"github.com/coder/terraform-provider-coder/v2/provider"
1113
)
1214

1315
func TestScript(t *testing.T) {
@@ -124,3 +126,73 @@ func TestScriptStartBlocksLoginRequiresRunOnStart(t *testing.T) {
124126
}},
125127
})
126128
}
129+
130+
func TestValidateCronExpression(t *testing.T) {
131+
t.Parallel()
132+
133+
tests := []struct {
134+
name string
135+
cronExpr string
136+
expectWarnings bool
137+
expectErrors bool
138+
warningContains string
139+
}{
140+
{
141+
name: "valid 6-field expression",
142+
cronExpr: "0 0 22 * * *",
143+
expectWarnings: false,
144+
expectErrors: false,
145+
},
146+
{
147+
name: "valid 6-field expression with seconds",
148+
cronExpr: "30 0 9 * * 1-5",
149+
expectWarnings: false,
150+
expectErrors: false,
151+
},
152+
{
153+
name: "5-field Unix format - should warn",
154+
cronExpr: "0 22 * * *",
155+
expectWarnings: true,
156+
expectErrors: false,
157+
warningContains: "appears to be in Unix 5-field format",
158+
},
159+
{
160+
name: "5-field every 5 minutes - should warn",
161+
cronExpr: "*/5 * * * *",
162+
expectWarnings: true,
163+
expectErrors: false,
164+
warningContains: "Consider prefixing with '0 '",
165+
},
166+
{
167+
name: "invalid expression",
168+
cronExpr: "invalid",
169+
expectErrors: true,
170+
},
171+
{
172+
name: "too many fields",
173+
cronExpr: "0 0 0 0 0 0 0",
174+
expectErrors: true,
175+
},
176+
}
177+
178+
for _, tt := range tests {
179+
t.Run(tt.name, func(t *testing.T) {
180+
warnings, errors := provider.ValidateCronExpression(tt.cronExpr)
181+
182+
if tt.expectWarnings {
183+
require.NotEmpty(t, warnings, "Expected warnings but got none")
184+
if tt.warningContains != "" {
185+
require.Contains(t, warnings[0], tt.warningContains)
186+
}
187+
} else {
188+
require.Empty(t, warnings, "Expected no warnings but got: %v", warnings)
189+
}
190+
191+
if tt.expectErrors {
192+
require.NotEmpty(t, errors, "Expected errors but got none")
193+
} else {
194+
require.Empty(t, errors, "Expected no errors but got: %v", errors)
195+
}
196+
})
197+
}
198+
}

0 commit comments

Comments
 (0)
0