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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy