diff --git a/docs/docs/04-standard-library/cloud/schedule.md b/docs/docs/04-standard-library/cloud/schedule.md index 766c745eb03..c10249df724 100644 --- a/docs/docs/04-standard-library/cloud/schedule.md +++ b/docs/docs/04-standard-library/cloud/schedule.md @@ -301,13 +301,21 @@ Trigger events according to a cron schedule using the UNIX cron format. Timezone is UTC. [minute] [hour] [day of month] [month] [day of week] +'*' means all possible values. +'-' means a range of values. +',' means a list of values. +[minute] allows 0-59. +[hour] allows 0-23. +[day of month] allows 1-31. +[month] allows 1-12 or JAN-DEC. +[day of week] allows 0-6 or SUN-SAT. --- *Example* ```wing -"0/1 * ? * *" +"* * * * *" ``` diff --git a/examples/tests/sdk_tests/schedule/init.test.w b/examples/tests/sdk_tests/schedule/init.test.w index e803edff0bb..dc8c3bd27a0 100644 --- a/examples/tests/sdk_tests/schedule/init.test.w +++ b/examples/tests/sdk_tests/schedule/init.test.w @@ -39,12 +39,4 @@ if (util.env("WING_TARGET") != "sim") { error = e; } assert(error == "cron string must be UNIX cron format [minute] [hour] [day of month] [month] [day of week]"); - - - try { - new cloud.Schedule( cron: "* * * * *" ) as "s5"; - } catch e { - error = e; - } - assert(error == "cannot use * in both the Day-of-month and Day-of-week fields. If you use it in one, you must use ? in the other"); } \ No newline at end of file diff --git a/libs/awscdk/src/schedule.ts b/libs/awscdk/src/schedule.ts index 6bdb8471ccb..b30978181d0 100644 --- a/libs/awscdk/src/schedule.ts +++ b/libs/awscdk/src/schedule.ts @@ -9,8 +9,10 @@ import { Construct } from "constructs"; import { App } from "./app"; import { cloud, core, std } from "@winglang/sdk"; import { convertBetweenHandlers } from "@winglang/sdk/lib/shared/convert"; +import { convertUnixCronToAWSCron } from "@winglang/sdk/lib/shared-aws/schedule"; import { isAwsCdkFunction } from "./function"; + /** * AWS implementation of `cloud.Schedule`. * @@ -25,27 +27,15 @@ export class Schedule extends cloud.Schedule { const { rate, cron } = props; - /* - * The schedule cron string is Unix cron format: [minute] [hour] [day of month] [month] [day of week] - * AWS EventBridge Schedule uses a 6 field format which includes year: [minute] [hour] [day of month] [month] [day of week] [year] - * https://docs.aws.amazon.com/scheduler/latest/UserGuide/schedule-types.html#cron-based - * - * We append * to the cron string for year field. - */ if (cron) { - const cronArr = cron.split(" "); - let cronOpt: { [k: string]: string } = { - minute: cronArr[0], - hour: cronArr[1], - month: cronArr[3], - year: "*", - }; - if (cronArr[2] !== "?") { - cronOpt.day = cronArr[2]; - } - if (cronArr[4] !== "?") { - cronOpt.weekDay = cronArr[4]; - } + let cronOpt: { [k: string]: string } = {}; + const awsCron = convertUnixCronToAWSCron(cron); + const cronArr = awsCron.split(" "); + if (cronArr[0] !== "*" && cronArr[0] !== "?") { cronOpt.minute = cronArr[0]; } + if (cronArr[1] !== "*" && cronArr[1] !== "?") { cronOpt.hour = cronArr[1]; } + if (cronArr[2] !== "*" && cronArr[2] !== "?") { cronOpt.day = cronArr[2]; } + if (cronArr[3] !== "*" && cronArr[3] !== "?") { cronOpt.month = cronArr[3]; } + if (cronArr[4] !== "*" && cronArr[4] !== "?") { cronOpt.weekDay = cronArr[4]; } this.scheduleExpression = EventSchedule.cron(cronOpt); } else { diff --git a/libs/awscdk/test/__snapshots__/schedule.test.ts.snap b/libs/awscdk/test/__snapshots__/schedule.test.ts.snap index 535a6820c2d..63bb0e132ba 100644 --- a/libs/awscdk/test/__snapshots__/schedule.test.ts.snap +++ b/libs/awscdk/test/__snapshots__/schedule.test.ts.snap @@ -1,5 +1,467 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`convert single dayOfWeek from Unix to AWS 1`] = ` +{ + "Parameters": { + "BootstrapVersion": { + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", + "Type": "AWS::SSM::Parameter::Value", + }, + }, + "Resources": { + "Schedule251B1F83": { + "Properties": { + "ScheduleExpression": "cron(* * ? * 0 *)", + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "ScheduleOnTick059D62C99", + "Arn", + ], + }, + "Id": "Target0", + }, + ], + }, + "Type": "AWS::Events::Rule", + }, + "ScheduleAllowEventRulemyprojectScheduleOnTick0FF908B3EE22FC7ED": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "ScheduleOnTick059D62C99", + "Arn", + ], + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "Schedule251B1F83", + "Arn", + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "ScheduleOnTick059D62C99": { + "DependsOn": [ + "ScheduleOnTick0ServiceRole37EF1AE1", + ], + "Properties": { + "Architectures": [ + "arm64", + ], + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", + }, + "S3Key": "", + }, + "Environment": { + "Variables": { + "NODE_OPTIONS": "--enable-source-maps", + }, + }, + "Handler": "index.handler", + "LoggingConfig": { + "LogGroup": { + "Ref": "ScheduleOnTick0LogGroup389684B1", + }, + }, + "MemorySize": 1024, + "Role": { + "Fn::GetAtt": [ + "ScheduleOnTick0ServiceRole37EF1AE1", + "Arn", + ], + }, + "Runtime": "nodejs20.x", + "Timeout": 60, + }, + "Type": "AWS::Lambda::Function", + }, + "ScheduleOnTick0LogGroup389684B1": { + "DeletionPolicy": "Retain", + "Properties": { + "RetentionInDays": 30, + }, + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "ScheduleOnTick0ServiceRole37EF1AE1": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5", + ], + { + "Ref": "BootstrapVersion", + }, + ], + }, + ], + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", + }, + ], + }, + }, +} +`; + +exports[`convert the list of dayOfWeek from Unix to AWS 1`] = ` +{ + "Parameters": { + "BootstrapVersion": { + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", + "Type": "AWS::SSM::Parameter::Value", + }, + }, + "Resources": { + "Schedule251B1F83": { + "Properties": { + "ScheduleExpression": "cron(* * ? * 0,2,4,6 *)", + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "ScheduleOnTick059D62C99", + "Arn", + ], + }, + "Id": "Target0", + }, + ], + }, + "Type": "AWS::Events::Rule", + }, + "ScheduleAllowEventRulemyprojectScheduleOnTick0FF908B3EE22FC7ED": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "ScheduleOnTick059D62C99", + "Arn", + ], + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "Schedule251B1F83", + "Arn", + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "ScheduleOnTick059D62C99": { + "DependsOn": [ + "ScheduleOnTick0ServiceRole37EF1AE1", + ], + "Properties": { + "Architectures": [ + "arm64", + ], + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", + }, + "S3Key": "", + }, + "Environment": { + "Variables": { + "NODE_OPTIONS": "--enable-source-maps", + }, + }, + "Handler": "index.handler", + "LoggingConfig": { + "LogGroup": { + "Ref": "ScheduleOnTick0LogGroup389684B1", + }, + }, + "MemorySize": 1024, + "Role": { + "Fn::GetAtt": [ + "ScheduleOnTick0ServiceRole37EF1AE1", + "Arn", + ], + }, + "Runtime": "nodejs20.x", + "Timeout": 60, + }, + "Type": "AWS::Lambda::Function", + }, + "ScheduleOnTick0LogGroup389684B1": { + "DeletionPolicy": "Retain", + "Properties": { + "RetentionInDays": 30, + }, + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "ScheduleOnTick0ServiceRole37EF1AE1": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5", + ], + { + "Ref": "BootstrapVersion", + }, + ], + }, + ], + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", + }, + ], + }, + }, +} +`; + +exports[`convert the range of dayOfWeek from Unix to AWS 1`] = ` +{ + "Parameters": { + "BootstrapVersion": { + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", + "Type": "AWS::SSM::Parameter::Value", + }, + }, + "Resources": { + "Schedule251B1F83": { + "Properties": { + "ScheduleExpression": "cron(* * ? * 0-6 *)", + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "ScheduleOnTick059D62C99", + "Arn", + ], + }, + "Id": "Target0", + }, + ], + }, + "Type": "AWS::Events::Rule", + }, + "ScheduleAllowEventRulemyprojectScheduleOnTick0FF908B3EE22FC7ED": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "ScheduleOnTick059D62C99", + "Arn", + ], + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "Schedule251B1F83", + "Arn", + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "ScheduleOnTick059D62C99": { + "DependsOn": [ + "ScheduleOnTick0ServiceRole37EF1AE1", + ], + "Properties": { + "Architectures": [ + "arm64", + ], + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", + }, + "S3Key": "", + }, + "Environment": { + "Variables": { + "NODE_OPTIONS": "--enable-source-maps", + }, + }, + "Handler": "index.handler", + "LoggingConfig": { + "LogGroup": { + "Ref": "ScheduleOnTick0LogGroup389684B1", + }, + }, + "MemorySize": 1024, + "Role": { + "Fn::GetAtt": [ + "ScheduleOnTick0ServiceRole37EF1AE1", + "Arn", + ], + }, + "Runtime": "nodejs20.x", + "Timeout": 60, + }, + "Type": "AWS::Lambda::Function", + }, + "ScheduleOnTick0LogGroup389684B1": { + "DeletionPolicy": "Retain", + "Properties": { + "RetentionInDays": 30, + }, + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "ScheduleOnTick0ServiceRole37EF1AE1": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5", + ], + { + "Ref": "BootstrapVersion", + }, + ], + }, + ], + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", + }, + ], + }, + }, +} +`; + exports[`schedule behavior with cron 1`] = ` { "Parameters": { @@ -12,7 +474,7 @@ exports[`schedule behavior with cron 1`] = ` "Resources": { "Schedule251B1F83": { "Properties": { - "ScheduleExpression": "cron(0/1 * ? * * *)", + "ScheduleExpression": "cron(0/1 * * * ? *)", "State": "ENABLED", "Targets": [ { @@ -320,7 +782,7 @@ exports[`schedule with two functions 1`] = ` "Resources": { "Schedule251B1F83": { "Properties": { - "ScheduleExpression": "cron(0/1 * ? * * *)", + "ScheduleExpression": "cron(0/1 * * * ? *)", "State": "ENABLED", "Targets": [ { diff --git a/libs/awscdk/test/schedule.test.ts b/libs/awscdk/test/schedule.test.ts index 5092f3c4f30..453ae1712cd 100644 --- a/libs/awscdk/test/schedule.test.ts +++ b/libs/awscdk/test/schedule.test.ts @@ -33,7 +33,7 @@ test("schedule behavior with cron", () => { `async handle(event) { console.log("Received: ", event); }` ); const schedule = new cloud.Schedule(app, "Schedule", { - cron: "0/1 * ? * *", + cron: "0/1 * * * *", }); schedule.onTick(fn); const output = app.synth(); @@ -42,7 +42,70 @@ test("schedule behavior with cron", () => { const template = Template.fromJSON(JSON.parse(output)); template.resourceCountIs("AWS::Events::Rule", 1); template.hasResourceProperties("AWS::Events::Rule", { - ScheduleExpression: "cron(0/1 * ? * * *)", + ScheduleExpression: "cron(0/1 * * * ? *)", + }); + expect(awscdkSanitize(template)).toMatchSnapshot(); +}); + +test("convert single dayOfWeek from Unix to AWS", () => { + // GIVEN + const app = new awscdk.App({ outdir: mkdtemp(), ...CDK_APP_OPTS }); + const fn = simulator.Testing.makeHandler( + `async handle(event) { console.log("Received: ", event); }` + ); + const schedule = new cloud.Schedule(app, "Schedule", { + cron: "* * * * 1", + }); + schedule.onTick(fn); + const output = app.synth(); + + // THEN + const template = Template.fromJSON(JSON.parse(output)); + template.resourceCountIs("AWS::Events::Rule", 1); + template.hasResourceProperties("AWS::Events::Rule", { + ScheduleExpression: "cron(* * ? * 0 *)", + }); + expect(awscdkSanitize(template)).toMatchSnapshot(); +}); + +test("convert the range of dayOfWeek from Unix to AWS", () => { + // GIVEN + const app = new awscdk.App({ outdir: mkdtemp(), ...CDK_APP_OPTS }); + const fn = simulator.Testing.makeHandler( + `async handle(event) { console.log("Received: ", event); }` + ); + const schedule = new cloud.Schedule(app, "Schedule", { + cron: "* * * * 1-7", + }); + schedule.onTick(fn); + const output = app.synth(); + + // THEN + const template = Template.fromJSON(JSON.parse(output)); + template.resourceCountIs("AWS::Events::Rule", 1); + template.hasResourceProperties("AWS::Events::Rule", { + ScheduleExpression: "cron(* * ? * 0-6 *)", + }); + expect(awscdkSanitize(template)).toMatchSnapshot(); +}); + +test("convert the list of dayOfWeek from Unix to AWS", () => { + // GIVEN + const app = new awscdk.App({ outdir: mkdtemp(), ...CDK_APP_OPTS }); + const fn = simulator.Testing.makeHandler( + `async handle(event) { console.log("Received: ", event); }` + ); + const schedule = new cloud.Schedule(app, "Schedule", { + cron: "* * * * 1,3,5,7", + }); + schedule.onTick(fn); + const output = app.synth(); + + // THEN + const template = Template.fromJSON(JSON.parse(output)); + template.resourceCountIs("AWS::Events::Rule", 1); + template.hasResourceProperties("AWS::Events::Rule", { + ScheduleExpression: "cron(* * ? * 0,2,4,6 *)", }); expect(awscdkSanitize(template)).toMatchSnapshot(); }); @@ -54,7 +117,7 @@ test("schedule with two functions", () => { `async handle(event) { console.log("Received: ", event); }` ); const schedule = new cloud.Schedule(app, "Schedule", { - cron: "0/1 * ? * *", + cron: "0/1 * * * *", }); schedule.onTick(fn); const output = app.synth(); @@ -123,7 +186,7 @@ test("schedule with rate less than 1 minute", () => { ).toThrow("rate can not be set to less than 1 minute."); }); -test("cron with Day-of-month and Day-of-week setting with *", () => { +test("cron with day of month and day of week configured at the same time", () => { // GIVEN const app = new awscdk.App({ outdir: mkdtemp(), ...CDK_APP_OPTS }); @@ -131,9 +194,7 @@ test("cron with Day-of-month and Day-of-week setting with *", () => { expect( () => new cloud.Schedule(app, "Schedule", { - cron: "0/1 * * * *", + cron: "* * 1 * 1", }) - ).toThrow( - "cannot use * in both the Day-of-month and Day-of-week fields. If you use it in one, you must use ? in the other" - ); + ).toThrow("Cannot restrict both 'day-of-month' and 'day-of-week' in a cron expression, at least one must be '*'"); }); diff --git a/libs/wingsdk/src/cloud/schedule.ts b/libs/wingsdk/src/cloud/schedule.ts index 3f1007be7c6..a452793402e 100644 --- a/libs/wingsdk/src/cloud/schedule.ts +++ b/libs/wingsdk/src/cloud/schedule.ts @@ -24,7 +24,15 @@ export interface ScheduleProps { /** * Trigger events according to a cron schedule using the UNIX cron format. Timezone is UTC. * [minute] [hour] [day of month] [month] [day of week] - * @example "0/1 * ? * *" + * '*' means all possible values. + * '-' means a range of values. + * ',' means a list of values. + * [minute] allows 0-59. + * [hour] allows 0-23. + * [day of month] allows 1-31. + * [month] allows 1-12 or JAN-DEC. + * [day of week] allows 0-6 or SUN-SAT. + * @example "* * * * *" * @default undefined */ readonly cron?: string; @@ -67,11 +75,6 @@ export class Schedule extends Resource { "cron string must be UNIX cron format [minute] [hour] [day of month] [month] [day of week]" ); } - if (cron && cron.split(" ")[2] == "*" && cron.split(" ")[4] == "*") { - throw new Error( - "cannot use * in both the Day-of-month and Day-of-week fields. If you use it in one, you must use ? in the other" - ); - } } /** diff --git a/libs/wingsdk/src/shared-aws/schedule.ts b/libs/wingsdk/src/shared-aws/schedule.ts new file mode 100644 index 00000000000..424fc6f4cd7 --- /dev/null +++ b/libs/wingsdk/src/shared-aws/schedule.ts @@ -0,0 +1,64 @@ +/** + * Convert Unix cron to AWS cron + */ +export const convertUnixCronToAWSCron = (cron: string) => { + const minute = cron.split(" ")[0]; + const hour = cron.split(" ")[1]; + let dayOfMonth = cron.split(" ")[2]; + const month = cron.split(" ")[3]; + let dayOfWeek = cron.split(" ")[4]; + + /* + * The implementation of cron on AWS does not allow [day of month] and [day of week] + * to have the character '*' at the same time. + * Therefore, [day of week] will be replaced by '?'. + */ + if (cron && dayOfMonth == "*" && dayOfWeek == "*") { + dayOfWeek = "?"; + } + + if (cron && dayOfMonth !== "*" && dayOfWeek !== "*") { + throw new Error( + "Cannot restrict both 'day-of-month' and 'day-of-week' in a cron expression, at least one must be '*'" + ); + } + + if (dayOfWeek !== "*" && dayOfWeek !== "?") { + dayOfMonth = "?"; + if (/\d/.test(dayOfWeek)) { + dayOfWeek = convertDayOfWeekFromUnixToAWS(dayOfWeek); + } + } + + /* + * The schedule cron string is Unix cron format: [minute] [hour] [day of month] [month] [day of week] + * AWS EventBridge Schedule uses a 6 field format which includes year: [minute] [hour] [day of month] [month] [day of week] [year] + * https://docs.aws.amazon.com/scheduler/latest/UserGuide/schedule-types.html#cron-based + * + * We append * to the cron string for year field. + */ + return ( + minute + + " " + + hour + + " " + + dayOfMonth + + " " + + month + + " " + + dayOfWeek + + " *" + ); +}; + +const convertDayOfWeekFromUnixToAWS = (dayOfWeek: string): string => { + const numbers = dayOfWeek.match(/\d+/g); + + if (numbers) { + for (const number of numbers) { + dayOfWeek = dayOfWeek.replace(number, (parseInt(number) - 1).toString()); + } + } + + return dayOfWeek; +}; diff --git a/libs/wingsdk/src/target-sim/util.ts b/libs/wingsdk/src/target-sim/util.ts index 71a78a6cbc4..17074dec81b 100644 --- a/libs/wingsdk/src/target-sim/util.ts +++ b/libs/wingsdk/src/target-sim/util.ts @@ -75,9 +75,7 @@ export function convertDurationToCronExpression(dur: Duration): string { // for now we just use * for day, month, and year const dayInMonth = "*"; const month = "*"; - // if day of month is "*", day of week should be "?" - // https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html - const dayOfWeek = "?"; + const dayOfWeek = "*"; // Generate cron string based on the duration const cronString = `${minute} ${hour} ${dayInMonth} ${month} ${dayOfWeek}`; diff --git a/libs/wingsdk/src/target-tf-aws/schedule.ts b/libs/wingsdk/src/target-tf-aws/schedule.ts index c660b26b61d..9d0d515cce6 100644 --- a/libs/wingsdk/src/target-tf-aws/schedule.ts +++ b/libs/wingsdk/src/target-tf-aws/schedule.ts @@ -7,6 +7,7 @@ import { CloudwatchEventTarget } from "../.gen/providers/aws/cloudwatch-event-ta import * as cloud from "../cloud"; import * as core from "../core"; import { convertBetweenHandlers } from "../shared/convert"; +import { convertUnixCronToAWSCron } from "../shared-aws/schedule"; import { Node } from "../std"; /** @@ -24,21 +25,13 @@ export class Schedule extends cloud.Schedule { const { rate, cron } = props; - /* - * The schedule cron string is Unix cron format: [minute] [hour] [day of month] [month] [day of week] - * AWS EventBridge Schedule uses a 6 field format which includes year: [minute] [hour] [day of month] [month] [day of week] [year] - * https://docs.aws.amazon.com/scheduler/latest/UserGuide/schedule-types.html#cron-based - * - * We append * to the cron string for year field. - */ this.scheduleExpression = rate ? rate.minutes === 1 ? `rate(${rate.minutes} minute)` : `rate(${rate.minutes} minutes)` - : `cron(${cron} *)`; + : `cron(${convertUnixCronToAWSCron(cron!)})`; this.rule = new CloudwatchEventRule(this, "Schedule", { - isEnabled: true, scheduleExpression: this.scheduleExpression, }); } diff --git a/libs/wingsdk/test/target-sim/__snapshots__/schedule.test.ts.snap b/libs/wingsdk/test/target-sim/__snapshots__/schedule.test.ts.snap index 00dcc05c893..c255a568385 100644 --- a/libs/wingsdk/test/target-sim/__snapshots__/schedule.test.ts.snap +++ b/libs/wingsdk/test/target-sim/__snapshots__/schedule.test.ts.snap @@ -13,7 +13,7 @@ exports[`create a schedule 1`] = ` "attrs": {}, "path": "root/my_schedule", "props": { - "cronExpression": "*/1 * * * ?", + "cronExpression": "*/1 * * * *", }, "type": "@winglang/sdk.cloud.Schedule", }, @@ -182,7 +182,7 @@ console.log("Hello from schedule!"); "attrs": {}, "path": "root/my_schedule", "props": { - "cronExpression": "* */3 * * ?", + "cronExpression": "* */3 * * *", }, "type": "@winglang/sdk.cloud.Schedule", }, @@ -392,7 +392,7 @@ console.log("Hello from schedule!"); "attrs": {}, "path": "root/my_schedule", "props": { - "cronExpression": "*/10 * * * ?", + "cronExpression": "*/10 * * * *", }, "type": "@winglang/sdk.cloud.Schedule", }, @@ -602,7 +602,7 @@ console.log("Hello from schedule!"); "attrs": {}, "path": "root/my_schedule", "props": { - "cronExpression": "* * * * ?", + "cronExpression": "* * * * *", }, "type": "@winglang/sdk.cloud.Schedule", }, diff --git a/libs/wingsdk/test/target-sim/schedule.test.ts b/libs/wingsdk/test/target-sim/schedule.test.ts index 1a6624e4d0f..8bc8d691bb4 100644 --- a/libs/wingsdk/test/target-sim/schedule.test.ts +++ b/libs/wingsdk/test/target-sim/schedule.test.ts @@ -12,7 +12,7 @@ console.log("Hello from schedule!"); test("create a schedule", async () => { // GIVEN const app = new SimApp(); - const cron = "*/1 * * * ?"; + const cron = "*/1 * * * *"; new cloud.Schedule(app, "my_schedule", { cron }); const s = await app.startSimulator(); @@ -39,7 +39,7 @@ test("schedule with one task with cron", async () => { const app = new SimApp(); const handler = Testing.makeHandler(INFLIGHT_CODE); const schedule = new cloud.Schedule(app, "my_schedule", { - cron: "* * * * ?", + cron: "* * * * *", }); schedule.onTick(handler); @@ -59,7 +59,7 @@ test("schedule with one task using rate of 10m", async () => { const schedule = new cloud.Schedule(app, "my_schedule", { rate: Duration.fromMinutes(10), }); - const expectedCron = "*/10 * * * ?"; // every 10 minutes cron expression + const expectedCron = "*/10 * * * *"; // every 10 minutes cron expression schedule.onTick(handler); const s = await app.startSimulator(); @@ -87,7 +87,7 @@ test("schedule with one task using rate of 3h", async () => { const schedule = new cloud.Schedule(app, "my_schedule", { rate: Duration.fromHours(3), }); - const expectedCron = "* */3 * * ?"; // every 3 hours cron expression + const expectedCron = "* */3 * * *"; // every 3 hours cron expression schedule.onTick(handler); const s = await app.startSimulator(); diff --git a/libs/wingsdk/test/target-sim/utils.test.ts b/libs/wingsdk/test/target-sim/utils.test.ts index cd5824ed67a..0ea5c3ea185 100644 --- a/libs/wingsdk/test/target-sim/utils.test.ts +++ b/libs/wingsdk/test/target-sim/utils.test.ts @@ -6,7 +6,7 @@ describe("convertDurationToCronExpression", () => { test("converts a duration from minutes", () => { // GIVEN const dur = Duration.fromMinutes(10); - const expectedCron = "*/10 * * * ?"; + const expectedCron = "*/10 * * * *"; // WHEN const cron = convertDurationToCronExpression(dur); @@ -17,7 +17,7 @@ describe("convertDurationToCronExpression", () => { test("converts a duration from hours", () => { const dur = Duration.fromHours(2); - const expectedCron = "* */2 * * ?"; + const expectedCron = "* */2 * * *"; // WHEN const cron = convertDurationToCronExpression(dur); @@ -29,7 +29,7 @@ describe("convertDurationToCronExpression", () => { test("converts durations with fractional hours", () => { // GIVEN const dur = Duration.fromHours(2.5); - const expectedCron = "*/30 */2 * * ?"; + const expectedCron = "*/30 */2 * * *"; // WHEN const cron = convertDurationToCronExpression(dur); diff --git a/libs/wingsdk/test/target-tf-aws/__snapshots__/schedule.test.ts.snap b/libs/wingsdk/test/target-tf-aws/__snapshots__/schedule.test.ts.snap index 1665082908f..5f054a6624f 100644 --- a/libs/wingsdk/test/target-tf-aws/__snapshots__/schedule.test.ts.snap +++ b/libs/wingsdk/test/target-tf-aws/__snapshots__/schedule.test.ts.snap @@ -1,12 +1,803 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`convert single dayOfWeek from Unix to AWS 1`] = ` +{ + "resource": { + "aws_cloudwatch_event_rule": { + "Schedule_15669BF1": { + "schedule_expression": "cron(* * ? * 0 *)", + }, + }, + "aws_cloudwatch_event_target": { + "Schedule_ScheduleTarget0_12D341DB": { + "arn": "\${aws_lambda_function.Schedule_OnTick0_958638E3.qualified_arn}", + "rule": "\${aws_cloudwatch_event_rule.Schedule_15669BF1.name}", + }, + }, + "aws_cloudwatch_log_group": { + "Schedule_OnTick0_CloudwatchLogGroup_A06DC96E": { + "name": "/aws/lambda/OnTick0-c8e1d4a8", + "retention_in_days": 30, + }, + }, + "aws_iam_role": { + "Schedule_OnTick0_IamRole_478B0576": { + "assume_role_policy": "{"Version":"2012-10-17","Statement":[{"Action":"sts:AssumeRole","Principal":{"Service":"lambda.amazonaws.com"},"Effect":"Allow"}]}", + }, + }, + "aws_iam_role_policy": { + "Schedule_OnTick0_IamRolePolicy_708CFC38": { + "policy": "{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"none:null","Resource":"*"}]}", + "role": "\${aws_iam_role.Schedule_OnTick0_IamRole_478B0576.name}", + }, + }, + "aws_iam_role_policy_attachment": { + "Schedule_OnTick0_IamRolePolicyAttachment_5885D6B3": { + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "role": "\${aws_iam_role.Schedule_OnTick0_IamRole_478B0576.name}", + }, + }, + "aws_lambda_function": { + "Schedule_OnTick0_958638E3": { + "architectures": [ + "arm64", + ], + "environment": { + "variables": { + "NODE_OPTIONS": "--enable-source-maps", + "WING_FUNCTION_NAME": "OnTick0-c8e1d4a8", + }, + }, + "function_name": "OnTick0-c8e1d4a8", + "handler": "index.handler", + "memory_size": 1024, + "publish": true, + "role": "\${aws_iam_role.Schedule_OnTick0_IamRole_478B0576.arn}", + "runtime": "nodejs20.x", + "s3_bucket": "\${aws_s3_bucket.Code.bucket}", + "s3_key": "\${aws_s3_object.Schedule_OnTick0_S3Object_95D0AF10.key}", + "timeout": 60, + "vpc_config": { + "security_group_ids": [], + "subnet_ids": [], + }, + }, + }, + "aws_lambda_permission": { + "Schedule_OnTick0_InvokePermission-c8b3fc394731d07e61c00e422c6b234372c09bc3b3_17682171": { + "action": "lambda:InvokeFunction", + "function_name": "\${aws_lambda_function.Schedule_OnTick0_958638E3.function_name}", + "principal": "events.amazonaws.com", + "qualifier": "\${aws_lambda_function.Schedule_OnTick0_958638E3.version}", + "source_arn": "\${aws_cloudwatch_event_rule.Schedule_15669BF1.arn}", + }, + }, + "aws_s3_bucket": { + "Code": { + "bucket_prefix": "code-c84a50b1-", + }, + }, + "aws_s3_object": { + "Schedule_OnTick0_S3Object_95D0AF10": { + "bucket": "\${aws_s3_bucket.Code.bucket}", + "key": "", + "source": "", + }, + }, + }, +} +`; + +exports[`convert single dayOfWeek from Unix to AWS 2`] = ` +{ + "tree": { + "children": { + "root": { + "children": { + "Default": { + "children": { + "Code": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "Code", + "path": "root/Default/Code", + }, + "ParameterRegistrar": { + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "id": "ParameterRegistrar", + "path": "root/Default/ParameterRegistrar", + }, + "Schedule": { + "children": { + "OnTick0": { + "children": { + "Asset": { + "constructInfo": { + "fqn": "cdktf.TerraformAsset", + "version": "0.20.3", + }, + "id": "Asset", + "path": "root/Default/Schedule/OnTick0/Asset", + }, + "CloudwatchLogGroup": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "CloudwatchLogGroup", + "path": "root/Default/Schedule/OnTick0/CloudwatchLogGroup", + }, + "Default": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "Default", + "path": "root/Default/Schedule/OnTick0/Default", + }, + "IamRole": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRole", + "path": "root/Default/Schedule/OnTick0/IamRole", + }, + "IamRolePolicy": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRolePolicy", + "path": "root/Default/Schedule/OnTick0/IamRolePolicy", + }, + "IamRolePolicyAttachment": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRolePolicyAttachment", + "path": "root/Default/Schedule/OnTick0/IamRolePolicyAttachment", + }, + "InvokePermission-c8b3fc394731d07e61c00e422c6b234372c09bc3b3": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "InvokePermission-c8b3fc394731d07e61c00e422c6b234372c09bc3b3", + "path": "root/Default/Schedule/OnTick0/InvokePermission-c8b3fc394731d07e61c00e422c6b234372c09bc3b3", + }, + "S3Object": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "S3Object", + "path": "root/Default/Schedule/OnTick0/S3Object", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "display": { + "description": "A cloud function (FaaS)", + "title": "Function", + }, + "id": "OnTick0", + "path": "root/Default/Schedule/OnTick0", + }, + "Schedule": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "Schedule", + "path": "root/Default/Schedule/Schedule", + }, + "ScheduleTarget0": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "ScheduleTarget0", + "path": "root/Default/Schedule/ScheduleTarget0", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "display": { + "description": "A cloud schedule to trigger events at regular intervals", + "title": "Schedule", + }, + "id": "Schedule", + "path": "root/Default/Schedule", + }, + "aws": { + "constructInfo": { + "fqn": "cdktf.TerraformProvider", + "version": "0.20.3", + }, + "id": "aws", + "path": "root/Default/aws", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "id": "Default", + "path": "root/Default", + }, + "backend": { + "constructInfo": { + "fqn": "cdktf.LocalBackend", + "version": "0.20.3", + }, + "id": "backend", + "path": "root/backend", + }, + }, + "constructInfo": { + "fqn": "cdktf.TerraformStack", + "version": "0.20.3", + }, + "id": "root", + "path": "root", + }, + }, + "constructInfo": { + "fqn": "cdktf.App", + "version": "0.20.3", + }, + "id": "App", + "path": "", + }, + "version": "tree-0.1", +} +`; + +exports[`convert the list of dayOfWeek from Unix to AWS 1`] = ` +{ + "resource": { + "aws_cloudwatch_event_rule": { + "Schedule_15669BF1": { + "schedule_expression": "cron(* * ? * 0,2,4,6 *)", + }, + }, + "aws_cloudwatch_event_target": { + "Schedule_ScheduleTarget0_12D341DB": { + "arn": "\${aws_lambda_function.Schedule_OnTick0_958638E3.qualified_arn}", + "rule": "\${aws_cloudwatch_event_rule.Schedule_15669BF1.name}", + }, + }, + "aws_cloudwatch_log_group": { + "Schedule_OnTick0_CloudwatchLogGroup_A06DC96E": { + "name": "/aws/lambda/OnTick0-c8e1d4a8", + "retention_in_days": 30, + }, + }, + "aws_iam_role": { + "Schedule_OnTick0_IamRole_478B0576": { + "assume_role_policy": "{"Version":"2012-10-17","Statement":[{"Action":"sts:AssumeRole","Principal":{"Service":"lambda.amazonaws.com"},"Effect":"Allow"}]}", + }, + }, + "aws_iam_role_policy": { + "Schedule_OnTick0_IamRolePolicy_708CFC38": { + "policy": "{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"none:null","Resource":"*"}]}", + "role": "\${aws_iam_role.Schedule_OnTick0_IamRole_478B0576.name}", + }, + }, + "aws_iam_role_policy_attachment": { + "Schedule_OnTick0_IamRolePolicyAttachment_5885D6B3": { + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "role": "\${aws_iam_role.Schedule_OnTick0_IamRole_478B0576.name}", + }, + }, + "aws_lambda_function": { + "Schedule_OnTick0_958638E3": { + "architectures": [ + "arm64", + ], + "environment": { + "variables": { + "NODE_OPTIONS": "--enable-source-maps", + "WING_FUNCTION_NAME": "OnTick0-c8e1d4a8", + }, + }, + "function_name": "OnTick0-c8e1d4a8", + "handler": "index.handler", + "memory_size": 1024, + "publish": true, + "role": "\${aws_iam_role.Schedule_OnTick0_IamRole_478B0576.arn}", + "runtime": "nodejs20.x", + "s3_bucket": "\${aws_s3_bucket.Code.bucket}", + "s3_key": "\${aws_s3_object.Schedule_OnTick0_S3Object_95D0AF10.key}", + "timeout": 60, + "vpc_config": { + "security_group_ids": [], + "subnet_ids": [], + }, + }, + }, + "aws_lambda_permission": { + "Schedule_OnTick0_InvokePermission-c8b3fc394731d07e61c00e422c6b234372c09bc3b3_17682171": { + "action": "lambda:InvokeFunction", + "function_name": "\${aws_lambda_function.Schedule_OnTick0_958638E3.function_name}", + "principal": "events.amazonaws.com", + "qualifier": "\${aws_lambda_function.Schedule_OnTick0_958638E3.version}", + "source_arn": "\${aws_cloudwatch_event_rule.Schedule_15669BF1.arn}", + }, + }, + "aws_s3_bucket": { + "Code": { + "bucket_prefix": "code-c84a50b1-", + }, + }, + "aws_s3_object": { + "Schedule_OnTick0_S3Object_95D0AF10": { + "bucket": "\${aws_s3_bucket.Code.bucket}", + "key": "", + "source": "", + }, + }, + }, +} +`; + +exports[`convert the list of dayOfWeek from Unix to AWS 2`] = ` +{ + "tree": { + "children": { + "root": { + "children": { + "Default": { + "children": { + "Code": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "Code", + "path": "root/Default/Code", + }, + "ParameterRegistrar": { + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "id": "ParameterRegistrar", + "path": "root/Default/ParameterRegistrar", + }, + "Schedule": { + "children": { + "OnTick0": { + "children": { + "Asset": { + "constructInfo": { + "fqn": "cdktf.TerraformAsset", + "version": "0.20.3", + }, + "id": "Asset", + "path": "root/Default/Schedule/OnTick0/Asset", + }, + "CloudwatchLogGroup": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "CloudwatchLogGroup", + "path": "root/Default/Schedule/OnTick0/CloudwatchLogGroup", + }, + "Default": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "Default", + "path": "root/Default/Schedule/OnTick0/Default", + }, + "IamRole": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRole", + "path": "root/Default/Schedule/OnTick0/IamRole", + }, + "IamRolePolicy": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRolePolicy", + "path": "root/Default/Schedule/OnTick0/IamRolePolicy", + }, + "IamRolePolicyAttachment": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRolePolicyAttachment", + "path": "root/Default/Schedule/OnTick0/IamRolePolicyAttachment", + }, + "InvokePermission-c8b3fc394731d07e61c00e422c6b234372c09bc3b3": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "InvokePermission-c8b3fc394731d07e61c00e422c6b234372c09bc3b3", + "path": "root/Default/Schedule/OnTick0/InvokePermission-c8b3fc394731d07e61c00e422c6b234372c09bc3b3", + }, + "S3Object": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "S3Object", + "path": "root/Default/Schedule/OnTick0/S3Object", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "display": { + "description": "A cloud function (FaaS)", + "title": "Function", + }, + "id": "OnTick0", + "path": "root/Default/Schedule/OnTick0", + }, + "Schedule": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "Schedule", + "path": "root/Default/Schedule/Schedule", + }, + "ScheduleTarget0": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "ScheduleTarget0", + "path": "root/Default/Schedule/ScheduleTarget0", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "display": { + "description": "A cloud schedule to trigger events at regular intervals", + "title": "Schedule", + }, + "id": "Schedule", + "path": "root/Default/Schedule", + }, + "aws": { + "constructInfo": { + "fqn": "cdktf.TerraformProvider", + "version": "0.20.3", + }, + "id": "aws", + "path": "root/Default/aws", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "id": "Default", + "path": "root/Default", + }, + "backend": { + "constructInfo": { + "fqn": "cdktf.LocalBackend", + "version": "0.20.3", + }, + "id": "backend", + "path": "root/backend", + }, + }, + "constructInfo": { + "fqn": "cdktf.TerraformStack", + "version": "0.20.3", + }, + "id": "root", + "path": "root", + }, + }, + "constructInfo": { + "fqn": "cdktf.App", + "version": "0.20.3", + }, + "id": "App", + "path": "", + }, + "version": "tree-0.1", +} +`; + +exports[`convert the range of dayOfWeek from Unix to AWS 1`] = ` +{ + "resource": { + "aws_cloudwatch_event_rule": { + "Schedule_15669BF1": { + "schedule_expression": "cron(* * ? * 0-6 *)", + }, + }, + "aws_cloudwatch_event_target": { + "Schedule_ScheduleTarget0_12D341DB": { + "arn": "\${aws_lambda_function.Schedule_OnTick0_958638E3.qualified_arn}", + "rule": "\${aws_cloudwatch_event_rule.Schedule_15669BF1.name}", + }, + }, + "aws_cloudwatch_log_group": { + "Schedule_OnTick0_CloudwatchLogGroup_A06DC96E": { + "name": "/aws/lambda/OnTick0-c8e1d4a8", + "retention_in_days": 30, + }, + }, + "aws_iam_role": { + "Schedule_OnTick0_IamRole_478B0576": { + "assume_role_policy": "{"Version":"2012-10-17","Statement":[{"Action":"sts:AssumeRole","Principal":{"Service":"lambda.amazonaws.com"},"Effect":"Allow"}]}", + }, + }, + "aws_iam_role_policy": { + "Schedule_OnTick0_IamRolePolicy_708CFC38": { + "policy": "{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"none:null","Resource":"*"}]}", + "role": "\${aws_iam_role.Schedule_OnTick0_IamRole_478B0576.name}", + }, + }, + "aws_iam_role_policy_attachment": { + "Schedule_OnTick0_IamRolePolicyAttachment_5885D6B3": { + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "role": "\${aws_iam_role.Schedule_OnTick0_IamRole_478B0576.name}", + }, + }, + "aws_lambda_function": { + "Schedule_OnTick0_958638E3": { + "architectures": [ + "arm64", + ], + "environment": { + "variables": { + "NODE_OPTIONS": "--enable-source-maps", + "WING_FUNCTION_NAME": "OnTick0-c8e1d4a8", + }, + }, + "function_name": "OnTick0-c8e1d4a8", + "handler": "index.handler", + "memory_size": 1024, + "publish": true, + "role": "\${aws_iam_role.Schedule_OnTick0_IamRole_478B0576.arn}", + "runtime": "nodejs20.x", + "s3_bucket": "\${aws_s3_bucket.Code.bucket}", + "s3_key": "\${aws_s3_object.Schedule_OnTick0_S3Object_95D0AF10.key}", + "timeout": 60, + "vpc_config": { + "security_group_ids": [], + "subnet_ids": [], + }, + }, + }, + "aws_lambda_permission": { + "Schedule_OnTick0_InvokePermission-c8b3fc394731d07e61c00e422c6b234372c09bc3b3_17682171": { + "action": "lambda:InvokeFunction", + "function_name": "\${aws_lambda_function.Schedule_OnTick0_958638E3.function_name}", + "principal": "events.amazonaws.com", + "qualifier": "\${aws_lambda_function.Schedule_OnTick0_958638E3.version}", + "source_arn": "\${aws_cloudwatch_event_rule.Schedule_15669BF1.arn}", + }, + }, + "aws_s3_bucket": { + "Code": { + "bucket_prefix": "code-c84a50b1-", + }, + }, + "aws_s3_object": { + "Schedule_OnTick0_S3Object_95D0AF10": { + "bucket": "\${aws_s3_bucket.Code.bucket}", + "key": "", + "source": "", + }, + }, + }, +} +`; + +exports[`convert the range of dayOfWeek from Unix to AWS 2`] = ` +{ + "tree": { + "children": { + "root": { + "children": { + "Default": { + "children": { + "Code": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "Code", + "path": "root/Default/Code", + }, + "ParameterRegistrar": { + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "id": "ParameterRegistrar", + "path": "root/Default/ParameterRegistrar", + }, + "Schedule": { + "children": { + "OnTick0": { + "children": { + "Asset": { + "constructInfo": { + "fqn": "cdktf.TerraformAsset", + "version": "0.20.3", + }, + "id": "Asset", + "path": "root/Default/Schedule/OnTick0/Asset", + }, + "CloudwatchLogGroup": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "CloudwatchLogGroup", + "path": "root/Default/Schedule/OnTick0/CloudwatchLogGroup", + }, + "Default": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "Default", + "path": "root/Default/Schedule/OnTick0/Default", + }, + "IamRole": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRole", + "path": "root/Default/Schedule/OnTick0/IamRole", + }, + "IamRolePolicy": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRolePolicy", + "path": "root/Default/Schedule/OnTick0/IamRolePolicy", + }, + "IamRolePolicyAttachment": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRolePolicyAttachment", + "path": "root/Default/Schedule/OnTick0/IamRolePolicyAttachment", + }, + "InvokePermission-c8b3fc394731d07e61c00e422c6b234372c09bc3b3": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "InvokePermission-c8b3fc394731d07e61c00e422c6b234372c09bc3b3", + "path": "root/Default/Schedule/OnTick0/InvokePermission-c8b3fc394731d07e61c00e422c6b234372c09bc3b3", + }, + "S3Object": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "S3Object", + "path": "root/Default/Schedule/OnTick0/S3Object", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "display": { + "description": "A cloud function (FaaS)", + "title": "Function", + }, + "id": "OnTick0", + "path": "root/Default/Schedule/OnTick0", + }, + "Schedule": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "Schedule", + "path": "root/Default/Schedule/Schedule", + }, + "ScheduleTarget0": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "ScheduleTarget0", + "path": "root/Default/Schedule/ScheduleTarget0", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "display": { + "description": "A cloud schedule to trigger events at regular intervals", + "title": "Schedule", + }, + "id": "Schedule", + "path": "root/Default/Schedule", + }, + "aws": { + "constructInfo": { + "fqn": "cdktf.TerraformProvider", + "version": "0.20.3", + }, + "id": "aws", + "path": "root/Default/aws", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "id": "Default", + "path": "root/Default", + }, + "backend": { + "constructInfo": { + "fqn": "cdktf.LocalBackend", + "version": "0.20.3", + }, + "id": "backend", + "path": "root/backend", + }, + }, + "constructInfo": { + "fqn": "cdktf.TerraformStack", + "version": "0.20.3", + }, + "id": "root", + "path": "root", + }, + }, + "constructInfo": { + "fqn": "cdktf.App", + "version": "0.20.3", + }, + "id": "App", + "path": "", + }, + "version": "tree-0.1", +} +`; + exports[`schedule behavior with cron 1`] = ` { "resource": { "aws_cloudwatch_event_rule": { "Schedule_15669BF1": { - "is_enabled": true, - "schedule_expression": "cron(0/1 * ? * * *)", + "schedule_expression": "cron(0/1 * * * ? *)", }, }, "aws_cloudwatch_event_target": { @@ -270,7 +1061,6 @@ exports[`schedule behavior with rate 1`] = ` "resource": { "aws_cloudwatch_event_rule": { "Schedule_15669BF1": { - "is_enabled": true, "schedule_expression": "rate(2 minutes)", }, }, @@ -535,8 +1325,7 @@ exports[`schedule with two functions 1`] = ` "resource": { "aws_cloudwatch_event_rule": { "Schedule_15669BF1": { - "is_enabled": true, - "schedule_expression": "cron(0/1 * ? * * *)", + "schedule_expression": "cron(0/1 * * * ? *)", }, }, "aws_cloudwatch_event_target": { diff --git a/libs/wingsdk/test/target-tf-aws/schedule.test.ts b/libs/wingsdk/test/target-tf-aws/schedule.test.ts index f3c53b36bb2..00e867c7534 100644 --- a/libs/wingsdk/test/target-tf-aws/schedule.test.ts +++ b/libs/wingsdk/test/target-tf-aws/schedule.test.ts @@ -49,7 +49,7 @@ test("schedule behavior with cron", () => { const app = new tfaws.App({ outdir: mkdtemp(), entrypointDir: __dirname }); const fn = Testing.makeHandler(CODE_LOG_EVENT); const schedule = new cloud.Schedule(app, "Schedule", { - cron: "0/1 * ? * *", + cron: "0/1 * * * *", }); schedule.onTick(fn); const output = app.synth(); @@ -72,7 +72,115 @@ test("schedule behavior with cron", () => { output, "aws_cloudwatch_event_rule", { - schedule_expression: "cron(0/1 * ? * * *)", + schedule_expression: "cron(0/1 * * * ? *)", + } + ) + ).toEqual(true); + expect(tfSanitize(output)).toMatchSnapshot(); + expect(treeJsonOf(app.outdir)).toMatchSnapshot(); +}); + +test("convert single dayOfWeek from Unix to AWS", () => { + // GIVEN + const app = new tfaws.App({ outdir: mkdtemp(), entrypointDir: __dirname }); + const fn = Testing.makeHandler(CODE_LOG_EVENT); + const schedule = new cloud.Schedule(app, "Schedule", { + cron: "* * * * 1", + }); + schedule.onTick(fn); + const output = app.synth(); + + // THEN + expect(tfResourcesOf(output)).toEqual([ + "aws_cloudwatch_event_rule", // main schedule event + "aws_cloudwatch_event_target", // schedule target + "aws_cloudwatch_log_group", // log group for function + "aws_iam_role", // role for function + "aws_iam_role_policy", // policy for role + "aws_iam_role_policy_attachment", // execution policy for role + "aws_lambda_function", // processor function + "aws_lambda_permission", // function permission + "aws_s3_bucket", // S3 bucket for code + "aws_s3_object", // S3 object for code + ]); + expect( + cdktf.Testing.toHaveResourceWithProperties( + output, + "aws_cloudwatch_event_rule", + { + schedule_expression: "cron(* * ? * 0 *)", + } + ) + ).toEqual(true); + expect(tfSanitize(output)).toMatchSnapshot(); + expect(treeJsonOf(app.outdir)).toMatchSnapshot(); +}); + +test("convert the range of dayOfWeek from Unix to AWS", () => { + // GIVEN + const app = new tfaws.App({ outdir: mkdtemp(), entrypointDir: __dirname }); + const fn = Testing.makeHandler(CODE_LOG_EVENT); + const schedule = new cloud.Schedule(app, "Schedule", { + cron: "* * * * 1-7", + }); + schedule.onTick(fn); + const output = app.synth(); + + // THEN + expect(tfResourcesOf(output)).toEqual([ + "aws_cloudwatch_event_rule", // main schedule event + "aws_cloudwatch_event_target", // schedule target + "aws_cloudwatch_log_group", // log group for function + "aws_iam_role", // role for function + "aws_iam_role_policy", // policy for role + "aws_iam_role_policy_attachment", // execution policy for role + "aws_lambda_function", // processor function + "aws_lambda_permission", // function permission + "aws_s3_bucket", // S3 bucket for code + "aws_s3_object", // S3 object for code + ]); + expect( + cdktf.Testing.toHaveResourceWithProperties( + output, + "aws_cloudwatch_event_rule", + { + schedule_expression: "cron(* * ? * 0-6 *)", + } + ) + ).toEqual(true); + expect(tfSanitize(output)).toMatchSnapshot(); + expect(treeJsonOf(app.outdir)).toMatchSnapshot(); +}); + +test("convert the list of dayOfWeek from Unix to AWS", () => { + // GIVEN + const app = new tfaws.App({ outdir: mkdtemp(), entrypointDir: __dirname }); + const fn = Testing.makeHandler(CODE_LOG_EVENT); + const schedule = new cloud.Schedule(app, "Schedule", { + cron: "* * * * 1,3,5,7", + }); + schedule.onTick(fn); + const output = app.synth(); + + // THEN + expect(tfResourcesOf(output)).toEqual([ + "aws_cloudwatch_event_rule", // main schedule event + "aws_cloudwatch_event_target", // schedule target + "aws_cloudwatch_log_group", // log group for function + "aws_iam_role", // role for function + "aws_iam_role_policy", // policy for role + "aws_iam_role_policy_attachment", // execution policy for role + "aws_lambda_function", // processor function + "aws_lambda_permission", // function permission + "aws_s3_bucket", // S3 bucket for code + "aws_s3_object", // S3 object for code + ]); + expect( + cdktf.Testing.toHaveResourceWithProperties( + output, + "aws_cloudwatch_event_rule", + { + schedule_expression: "cron(* * ? * 0,2,4,6 *)", } ) ).toEqual(true); @@ -86,7 +194,7 @@ test("schedule with two functions", () => { const fn1 = Testing.makeHandler(CODE_LOG_EVENT); const fn2 = Testing.makeHandler(CODE_LOG_EVENT); const schedule = new cloud.Schedule(app, "Schedule", { - cron: "0/1 * ? * *", + cron: "0/1 * * * *", }); schedule.onTick(fn1); schedule.onTick(fn2); @@ -118,7 +226,7 @@ test("schedule with rate and cron simultaneously", () => { () => new cloud.Schedule(app, "Schedule", { rate: std.Duration.fromSeconds(30), - cron: "0/1 * ? * *", + cron: "0/1 * * * ?", }) ).toThrow("rate and cron cannot be configured simultaneously."); }); @@ -131,7 +239,7 @@ test("cron with more than five values", () => { expect( () => new cloud.Schedule(app, "Schedule", { - cron: "0/1 * ? * * *", + cron: "0/1 * * * * *", }) ).toThrow( "cron string must be UNIX cron format [minute] [hour] [day of month] [month] [day of week]" @@ -160,3 +268,18 @@ test("schedule with rate less than 1 minute", () => { }) ).toThrow("rate can not be set to less than 1 minute."); }); + +test("cron with day of month and day of week configured at the same time", () => { + // GIVEN + const app = new tfaws.App({ outdir: mkdtemp(), entrypointDir: __dirname }); + + // THEN + expect( + () => + new cloud.Schedule(app, "Schedule", { + cron: "* * 1 * 1", + }) + ).toThrow( + "Cannot restrict both 'day-of-month' and 'day-of-week' in a cron expression, at least one must be '*'" + ); +}); diff --git a/tools/hangar/__snapshots__/test_corpus/sdk_tests/schedule/on_tick.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/sdk_tests/schedule/on_tick.test.w_compile_tf-aws.md index ef9c211713d..c71573ca178 100644 --- a/tools/hangar/__snapshots__/test_corpus/sdk_tests/schedule/on_tick.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/sdk_tests/schedule/on_tick.test.w_compile_tf-aws.md @@ -25,7 +25,6 @@ "uniqueId": "from_cron_Schedule_6C1613E8" } }, - "is_enabled": true, "schedule_expression": "cron(* * * * ? *)" }, "from_rate_Schedule_5B82E706": { @@ -35,7 +34,6 @@ "uniqueId": "from_rate_Schedule_5B82E706" } }, - "is_enabled": true, "schedule_expression": "rate(1 minute)" } },