Skip to content

Commit

Permalink
feat(job): schedule job at specific timezone
Browse files Browse the repository at this point in the history
  • Loading branch information
alquerci committed Feb 17, 2025
1 parent 949daed commit ebc70e4
Show file tree
Hide file tree
Showing 23 changed files with 266 additions and 93 deletions.
4 changes: 2 additions & 2 deletions internal/pkg/cli/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ var (

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

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

// RDS Aurora Serverless validation expressions.
Expand Down Expand Up @@ -493,7 +493,7 @@ func basicNameValidation(val interface{}) error {
}

func validateCron(sched string) error {
// If the schedule is wrapped in aws terms `rate()` or `cron()`, don't validate it--
// If the schedule is wrapped in aws terms `rate()` or `cron()` or `at`, don't validate it--
// instead, pass it in as-is for serverside validation. AWS cron is weird (year field, nonstandard wildcards)
// so for edge cases we need to support it
awsSchedMatch := awsScheduleRegexp.FindStringSubmatch(sched)
Expand Down
4 changes: 4 additions & 0 deletions internal/pkg/cli/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,10 @@ func TestValidateCron(t *testing.T) {
input: "cron(0 9 3W * ? *)",
shouldPass: true,
},
"bypass with at()": {
input: "at(2022-11-20T13:00:00)",
shouldPass: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
Expand Down
6 changes: 6 additions & 0 deletions internal/pkg/deploy/cloudformation/stack/scheduled_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
// Parameter logical IDs for a scheduled job
const (
ScheduledJobScheduleParamKey = "Schedule"
ScheduledJobScheduleTimezoneParamKey = "ScheduleTimezone"
)

// ScheduledJob represents the configuration needed to create a Cloudformation stack from a
Expand Down Expand Up @@ -184,6 +185,7 @@ func (j *ScheduledJob) Template() (string, error) {
AddonsExtraParams: addonsParams,
Sidecars: sidecars,
ScheduleExpression: schedule,
ScheduleTimezone: aws.StringValue(j.manifest.On.Timezone),
StateMachine: stateMachine,
HealthCheck: convertContainerHealthCheck(j.manifest.ImageConfig.HealthCheck),
LogConfig: convertLogging(j.manifest.Logging),
Expand Down Expand Up @@ -228,6 +230,10 @@ func (j *ScheduledJob) Parameters() ([]*cloudformation.Parameter, error) {
ParameterKey: aws.String(ScheduledJobScheduleParamKey),
ParameterValue: aws.String(schedule),
},
{
ParameterKey: aws.String(ScheduledJobScheduleTimezoneParamKey),
ParameterValue: j.manifest.On.Timezone,
},
}...), nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ func TestScheduledJob_Template(t *testing.T) {
require.Equal(t, template.WorkloadOpts{
WorkloadType: manifestinfo.ScheduledJobType,
ScheduleExpression: "cron(0 0 * * ? *)",
ScheduleTimezone: "UTC",
StateMachine: &template.StateMachineOpts{
Timeout: aws.Int(5400),
Retries: aws.Int(3),
Expand Down Expand Up @@ -102,6 +103,7 @@ func TestScheduledJob_Template(t *testing.T) {
AddonsExtraParams: `ServiceName: !GetAtt Service.Name
DiscoveryServiceArn: !GetAtt DiscoveryService.Arn`,
ScheduleExpression: "cron(0 0 * * ? *)",
ScheduleTimezone: "UTC",
StateMachine: &template.StateMachineOpts{
Timeout: aws.Int(5400),
Retries: aws.Int(3),
Expand Down Expand Up @@ -450,6 +452,7 @@ func TestScheduledJob_Parameters(t *testing.T) {
Dockerfile: "frontend/Dockerfile",
},
Schedule: "@daily",
Timezone: "GMT",
}
testScheduledJobManifest := manifest.NewScheduledJob(baseProps)
testScheduledJobManifest.Count = manifest.Count{
Expand Down Expand Up @@ -504,6 +507,10 @@ func TestScheduledJob_Parameters(t *testing.T) {
ParameterKey: aws.String(ScheduledJobScheduleParamKey),
ParameterValue: aws.String("cron(0 0 * * ? *)"),
},
{
ParameterKey: aws.String(ScheduledJobScheduleTimezoneParamKey),
ParameterValue: aws.String("GMT"),
},
}
testCases := map[string]struct {
httpsEnabled bool
Expand Down Expand Up @@ -600,6 +607,7 @@ func TestScheduledJob_SerializedParameters(t *testing.T) {
"EnvName": "test",
"LogRetention": "30",
"Schedule": "cron(0 0 * * ? *)",
"ScheduleTimezone": "UTC",
"TaskCPU": "256",
"TaskCount": "1",
"TaskMemory": "512",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"EnvName": "test",
"LogRetention": "30",
"Schedule": "cron(0 12 ? * MON *)",
"ScheduleTimezone": "UTC",
"TaskCPU": "256",
"TaskCount": "1",
"TaskMemory": "512",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Parameters:
Type: String
Schedule:
Type: String
ScheduleTimezone:
Type: String
ContainerImage:
Type: String
TaskCPU:
Expand Down Expand Up @@ -392,29 +394,31 @@ Resources:
Resource:
- !Ref mytopicfifoSNSTopic

Rule:
Schedule:
Metadata:
'aws:copilot:description': "A CloudWatch event rule to trigger the job's state machine"
Type: AWS::Events::Rule
'aws:copilot:description': "An EventBridge Schedule to trigger the job's state machine"
Type: AWS::Scheduler::Schedule
Properties:
ScheduleExpression: !Ref Schedule
State: ENABLED
Targets:
- Arn: !Ref StateMachine
Id: statemachine
RoleArn: !GetAtt RuleRole.Arn
RuleRole:
FlexibleTimeWindow:
Mode: "OFF"
ScheduleExpressionTimezone: !Ref ScheduleTimezone
Target:
Arn: !Ref StateMachine
RoleArn: !GetAtt ScheduleRole.Arn
ScheduleRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: events.amazonaws.com
Service: scheduler.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: EventRulePolicy
- PolicyName: SchedulePolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
Expand Down Expand Up @@ -617,4 +621,4 @@ Resources:
Resource: !Ref mytopicfifoSNSTopic
Condition:
StringEquals:
"sns:Protocol": "sqs"
"sns:Protocol": "sqs"
2 changes: 2 additions & 0 deletions internal/pkg/initialize/workload.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ type WorkloadProps struct {
type JobProps struct {
WorkloadProps
Schedule string
Timezone string
HealthCheck manifest.ContainerHealthCheck
Timeout string
Retries int
Expand Down Expand Up @@ -299,6 +300,7 @@ func newJobManifest(i *JobProps) (encoding.BinaryMarshaler, error) {
HealthCheck: i.HealthCheck,
Platform: i.Platform,
Schedule: i.Schedule,
Timezone: i.Timezone,
Timeout: i.Timeout,
Retries: i.Retries,
}), nil
Expand Down
Loading

0 comments on commit ebc70e4

Please sign in to comment.