Skip to content

Commit ebc70e4

Browse files
committed
feat(job): schedule job at specific timezone
1 parent 949daed commit ebc70e4

23 files changed

+266
-93
lines changed

internal/pkg/cli/validate.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ var (
125125

126126
domainNameRegexp = regexp.MustCompile(`\.`) // Check for at least one dot in domain name.
127127

128-
awsScheduleRegexp = regexp.MustCompile(`(?:rate|cron)\(.*\)`) // Check for strings of the form rate(*) or cron(*).
128+
awsScheduleRegexp = regexp.MustCompile(`(?:rate|cron|at)\(.*\)`) // Check for strings of the form rate(*) or cron(*).
129129
)
130130

131131
// RDS Aurora Serverless validation expressions.
@@ -493,7 +493,7 @@ func basicNameValidation(val interface{}) error {
493493
}
494494

495495
func validateCron(sched string) error {
496-
// If the schedule is wrapped in aws terms `rate()` or `cron()`, don't validate it--
496+
// If the schedule is wrapped in aws terms `rate()` or `cron()` or `at`, don't validate it--
497497
// instead, pass it in as-is for serverside validation. AWS cron is weird (year field, nonstandard wildcards)
498498
// so for edge cases we need to support it
499499
awsSchedMatch := awsScheduleRegexp.FindStringSubmatch(sched)

internal/pkg/cli/validate_test.go

+4
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,10 @@ func TestValidateCron(t *testing.T) {
701701
input: "cron(0 9 3W * ? *)",
702702
shouldPass: true,
703703
},
704+
"bypass with at()": {
705+
input: "at(2022-11-20T13:00:00)",
706+
shouldPass: true,
707+
},
704708
}
705709
for name, tc := range testCases {
706710
t.Run(name, func(t *testing.T) {

internal/pkg/deploy/cloudformation/stack/scheduled_job.go

+6
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
// Parameter logical IDs for a scheduled job
2626
const (
2727
ScheduledJobScheduleParamKey = "Schedule"
28+
ScheduledJobScheduleTimezoneParamKey = "ScheduleTimezone"
2829
)
2930

3031
// ScheduledJob represents the configuration needed to create a Cloudformation stack from a
@@ -184,6 +185,7 @@ func (j *ScheduledJob) Template() (string, error) {
184185
AddonsExtraParams: addonsParams,
185186
Sidecars: sidecars,
186187
ScheduleExpression: schedule,
188+
ScheduleTimezone: aws.StringValue(j.manifest.On.Timezone),
187189
StateMachine: stateMachine,
188190
HealthCheck: convertContainerHealthCheck(j.manifest.ImageConfig.HealthCheck),
189191
LogConfig: convertLogging(j.manifest.Logging),
@@ -228,6 +230,10 @@ func (j *ScheduledJob) Parameters() ([]*cloudformation.Parameter, error) {
228230
ParameterKey: aws.String(ScheduledJobScheduleParamKey),
229231
ParameterValue: aws.String(schedule),
230232
},
233+
{
234+
ParameterKey: aws.String(ScheduledJobScheduleTimezoneParamKey),
235+
ParameterValue: j.manifest.On.Timezone,
236+
},
231237
}...), nil
232238
}
233239

internal/pkg/deploy/cloudformation/stack/scheduled_job_test.go

+8
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ func TestScheduledJob_Template(t *testing.T) {
6666
require.Equal(t, template.WorkloadOpts{
6767
WorkloadType: manifestinfo.ScheduledJobType,
6868
ScheduleExpression: "cron(0 0 * * ? *)",
69+
ScheduleTimezone: "UTC",
6970
StateMachine: &template.StateMachineOpts{
7071
Timeout: aws.Int(5400),
7172
Retries: aws.Int(3),
@@ -102,6 +103,7 @@ func TestScheduledJob_Template(t *testing.T) {
102103
AddonsExtraParams: `ServiceName: !GetAtt Service.Name
103104
DiscoveryServiceArn: !GetAtt DiscoveryService.Arn`,
104105
ScheduleExpression: "cron(0 0 * * ? *)",
106+
ScheduleTimezone: "UTC",
105107
StateMachine: &template.StateMachineOpts{
106108
Timeout: aws.Int(5400),
107109
Retries: aws.Int(3),
@@ -450,6 +452,7 @@ func TestScheduledJob_Parameters(t *testing.T) {
450452
Dockerfile: "frontend/Dockerfile",
451453
},
452454
Schedule: "@daily",
455+
Timezone: "GMT",
453456
}
454457
testScheduledJobManifest := manifest.NewScheduledJob(baseProps)
455458
testScheduledJobManifest.Count = manifest.Count{
@@ -504,6 +507,10 @@ func TestScheduledJob_Parameters(t *testing.T) {
504507
ParameterKey: aws.String(ScheduledJobScheduleParamKey),
505508
ParameterValue: aws.String("cron(0 0 * * ? *)"),
506509
},
510+
{
511+
ParameterKey: aws.String(ScheduledJobScheduleTimezoneParamKey),
512+
ParameterValue: aws.String("GMT"),
513+
},
507514
}
508515
testCases := map[string]struct {
509516
httpsEnabled bool
@@ -600,6 +607,7 @@ func TestScheduledJob_SerializedParameters(t *testing.T) {
600607
"EnvName": "test",
601608
"LogRetention": "30",
602609
"Schedule": "cron(0 0 * * ? *)",
610+
"ScheduleTimezone": "UTC",
603611
"TaskCPU": "256",
604612
"TaskCount": "1",
605613
"TaskMemory": "512",

internal/pkg/deploy/cloudformation/stack/testdata/workloads/job-test.params.json

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"EnvName": "test",
1010
"LogRetention": "30",
1111
"Schedule": "cron(0 12 ? * MON *)",
12+
"ScheduleTimezone": "UTC",
1213
"TaskCPU": "256",
1314
"TaskCount": "1",
1415
"TaskMemory": "512",

internal/pkg/deploy/cloudformation/stack/testdata/workloads/job-test.stack.yml

+15-11
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ Parameters:
1313
Type: String
1414
Schedule:
1515
Type: String
16+
ScheduleTimezone:
17+
Type: String
1618
ContainerImage:
1719
Type: String
1820
TaskCPU:
@@ -392,29 +394,31 @@ Resources:
392394
Resource:
393395
- !Ref mytopicfifoSNSTopic
394396

395-
Rule:
397+
Schedule:
396398
Metadata:
397-
'aws:copilot:description': "A CloudWatch event rule to trigger the job's state machine"
398-
Type: AWS::Events::Rule
399+
'aws:copilot:description': "An EventBridge Schedule to trigger the job's state machine"
400+
Type: AWS::Scheduler::Schedule
399401
Properties:
400402
ScheduleExpression: !Ref Schedule
401403
State: ENABLED
402-
Targets:
403-
- Arn: !Ref StateMachine
404-
Id: statemachine
405-
RoleArn: !GetAtt RuleRole.Arn
406-
RuleRole:
404+
FlexibleTimeWindow:
405+
Mode: "OFF"
406+
ScheduleExpressionTimezone: !Ref ScheduleTimezone
407+
Target:
408+
Arn: !Ref StateMachine
409+
RoleArn: !GetAtt ScheduleRole.Arn
410+
ScheduleRole:
407411
Type: AWS::IAM::Role
408412
Properties:
409413
AssumeRolePolicyDocument:
410414
Version: '2012-10-17'
411415
Statement:
412416
- Effect: Allow
413417
Principal:
414-
Service: events.amazonaws.com
418+
Service: scheduler.amazonaws.com
415419
Action: sts:AssumeRole
416420
Policies:
417-
- PolicyName: EventRulePolicy
421+
- PolicyName: SchedulePolicy
418422
PolicyDocument:
419423
Version: '2012-10-17'
420424
Statement:
@@ -617,4 +621,4 @@ Resources:
617621
Resource: !Ref mytopicfifoSNSTopic
618622
Condition:
619623
StringEquals:
620-
"sns:Protocol": "sqs"
624+
"sns:Protocol": "sqs"

internal/pkg/initialize/workload.go

+2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ type WorkloadProps struct {
7272
type JobProps struct {
7373
WorkloadProps
7474
Schedule string
75+
Timezone string
7576
HealthCheck manifest.ContainerHealthCheck
7677
Timeout string
7778
Retries int
@@ -299,6 +300,7 @@ func newJobManifest(i *JobProps) (encoding.BinaryMarshaler, error) {
299300
HealthCheck: i.HealthCheck,
300301
Platform: i.Platform,
301302
Schedule: i.Schedule,
303+
Timezone: i.Timezone,
302304
Timeout: i.Timeout,
303305
Retries: i.Retries,
304306
}), nil

0 commit comments

Comments
 (0)