diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/cdk-backup.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/cdk-backup.assets.json index 19ddf51ac30c3..967f653a106aa 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/cdk-backup.assets.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/cdk-backup.assets.json @@ -1,7 +1,7 @@ { - "version": "32.0.0", + "version": "34.0.0", "files": { - "0c52c355c71ac95690274d7987110017ff9cd1a1bc79fa4206fda2f55d6b62d5": { + "7617f116cb014747232d2ebeb42fb3e22242c4b0987526daf7a033aba236d976": { "source": { "path": "cdk-backup.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "0c52c355c71ac95690274d7987110017ff9cd1a1bc79fa4206fda2f55d6b62d5.json", + "objectKey": "7617f116cb014747232d2ebeb42fb3e22242c4b0987526daf7a033aba236d976.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/cdk-backup.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/cdk-backup.template.json index 2722b98da789d..e1d8a605c90e0 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/cdk-backup.template.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/cdk-backup.template.json @@ -83,6 +83,7 @@ }, "RuleName": "Daily", "ScheduleExpression": "cron(0 5 * * ? *)", + "ScheduleExpressionTimezone": "Etc/UTC", "TargetBackupVault": { "Fn::GetAtt": [ "Vault23237E5B", @@ -96,6 +97,7 @@ }, "RuleName": "Weekly", "ScheduleExpression": "cron(0 5 ? * SAT *)", + "ScheduleExpressionTimezone": "Etc/UTC", "TargetBackupVault": { "Fn::GetAtt": [ "Vault23237E5B", @@ -110,6 +112,7 @@ }, "RuleName": "Monthly5Year", "ScheduleExpression": "cron(0 5 1 * ? *)", + "ScheduleExpressionTimezone": "Etc/UTC", "TargetBackupVault": { "Fn::GetAtt": [ "Vault23237E5B", diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/cdk.out index f0b901e7c06e5..2313ab5436501 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/cdk.out +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"32.0.0"} \ No newline at end of file +{"version":"34.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/integ.json index 266124ac58c12..b044eefcaffbb 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/integ.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "33.0.0", + "version": "34.0.0", "testCases": { "integ.backup": { "stacks": [ diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/manifest.json index aa4d73d1faac8..85402b8c54933 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/manifest.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "33.0.0", + "version": "34.0.0", "artifacts": { "cdk-backup.assets": { "type": "cdk:asset-manifest", @@ -14,10 +14,11 @@ "environment": "aws://unknown-account/unknown-region", "properties": { "templateFile": "cdk-backup.template.json", + "terminationProtection": false, "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/0c52c355c71ac95690274d7987110017ff9cd1a1bc79fa4206fda2f55d6b62d5.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/7617f116cb014747232d2ebeb42fb3e22242c4b0987526daf7a033aba236d976.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/tree.json index 3728b496ca8cf..9120c3a2895c6 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/tree.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-backup/test/integ.backup.js.snapshot/tree.json @@ -37,22 +37,22 @@ } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_dynamodb.CfnTable", + "version": "0.0.0" } }, "ScalingRole": { "id": "ScalingRole", "path": "cdk-backup/Table/ScalingRole", "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_dynamodb.Table", + "version": "0.0.0" } }, "FileSystem": { @@ -63,8 +63,8 @@ "aws:cdk:cloudformation:props": {} }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_efs.CfnFileSystem", + "version": "0.0.0" } }, "Vault": { @@ -84,14 +84,14 @@ } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_backup.CfnBackupVault", + "version": "0.0.0" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_backup.BackupVault", + "version": "0.0.0" } }, "SecondaryVault": { @@ -111,22 +111,22 @@ } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_backup.CfnBackupVault", + "version": "0.0.0" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_backup.BackupVault", + "version": "0.0.0" } }, "Env": { "id": "Env", "path": "cdk-backup/Env", "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" } }, "ThirdVault": { @@ -156,14 +156,14 @@ } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_backup.CfnBackupVault", + "version": "0.0.0" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_backup.BackupVault", + "version": "0.0.0" } }, "Plan": { @@ -185,6 +185,7 @@ }, "ruleName": "Daily", "scheduleExpression": "cron(0 5 * * ? *)", + "scheduleExpressionTimezone": "Etc/UTC", "targetBackupVault": { "Fn::GetAtt": [ "Vault23237E5B", @@ -198,6 +199,7 @@ }, "ruleName": "Weekly", "scheduleExpression": "cron(0 5 ? * SAT *)", + "scheduleExpressionTimezone": "Etc/UTC", "targetBackupVault": { "Fn::GetAtt": [ "Vault23237E5B", @@ -212,6 +214,7 @@ }, "ruleName": "Monthly5Year", "scheduleExpression": "cron(0 5 1 * ? *)", + "scheduleExpressionTimezone": "Etc/UTC", "targetBackupVault": { "Fn::GetAtt": [ "Vault23237E5B", @@ -250,8 +253,8 @@ } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_backup.CfnBackupPlan", + "version": "0.0.0" } }, "Selection": { @@ -266,8 +269,8 @@ "id": "ImportRole", "path": "cdk-backup/Plan/Selection/Role/ImportRole", "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" } }, "Resource": { @@ -305,14 +308,14 @@ } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_iam.CfnRole", + "version": "0.0.0" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_iam.Role", + "version": "0.0.0" } }, "Resource": { @@ -394,42 +397,42 @@ } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_backup.CfnBackupSelection", + "version": "0.0.0" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_backup.BackupSelection", + "version": "0.0.0" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.aws_backup.BackupPlan", + "version": "0.0.0" } }, "BootstrapVersion": { "id": "BootstrapVersion", "path": "cdk-backup/BootstrapVersion", "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" } }, "CheckBootstrapVersion": { "id": "CheckBootstrapVersion", "path": "cdk-backup/CheckBootstrapVersion", "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" } }, "Tree": { @@ -437,13 +440,13 @@ "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.2.69" + "version": "10.2.70" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.2.69" + "fqn": "aws-cdk-lib.App", + "version": "0.0.0" } } } \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-dynamodb/test/integ.autoscaling.lit.js.snapshot/aws-cdk-dynamodb.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-dynamodb/test/integ.autoscaling.lit.js.snapshot/aws-cdk-dynamodb.assets.json index 354247134c202..30904fa397fe7 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-dynamodb/test/integ.autoscaling.lit.js.snapshot/aws-cdk-dynamodb.assets.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-dynamodb/test/integ.autoscaling.lit.js.snapshot/aws-cdk-dynamodb.assets.json @@ -1,7 +1,7 @@ { - "version": "20.0.0", + "version": "34.0.0", "files": { - "619c64c0f19a7d78d759641bfba47f06ebbd7ffb42aae71ec695a6777a534d01": { + "865151155140d0ba599d3522f659db77ada497ea1a763cf11c3fb58d017a3082": { "source": { "path": "aws-cdk-dynamodb.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "619c64c0f19a7d78d759641bfba47f06ebbd7ffb42aae71ec695a6777a534d01.json", + "objectKey": "865151155140d0ba599d3522f659db77ada497ea1a763cf11c3fb58d017a3082.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-dynamodb/test/integ.autoscaling.lit.js.snapshot/aws-cdk-dynamodb.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-dynamodb/test/integ.autoscaling.lit.js.snapshot/aws-cdk-dynamodb.template.json index 72046e5747d8d..bd730b9100e98 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-dynamodb/test/integ.autoscaling.lit.js.snapshot/aws-cdk-dynamodb.template.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-dynamodb/test/integ.autoscaling.lit.js.snapshot/aws-cdk-dynamodb.template.json @@ -3,16 +3,16 @@ "TableCD117FA1": { "Type": "AWS::DynamoDB::Table", "Properties": { - "KeySchema": [ + "AttributeDefinitions": [ { "AttributeName": "hashKey", - "KeyType": "HASH" + "AttributeType": "S" } ], - "AttributeDefinitions": [ + "KeySchema": [ { "AttributeName": "hashKey", - "AttributeType": "S" + "KeyType": "HASH" } ], "ProvisionedThroughput": { @@ -56,23 +56,25 @@ ] }, "ScalableDimension": "dynamodb:table:ReadCapacityUnits", - "ServiceNamespace": "dynamodb", "ScheduledActions": [ { "ScalableTargetAction": { "MinCapacity": 20 }, "Schedule": "cron(0 8 * * ? *)", - "ScheduledActionName": "ScaleUpInTheMorning" + "ScheduledActionName": "ScaleUpInTheMorning", + "Timezone": "Etc/UTC" }, { "ScalableTargetAction": { "MaxCapacity": 20 }, "Schedule": "cron(0 20 * * ? *)", - "ScheduledActionName": "ScaleDownAtNight" + "ScheduledActionName": "ScaleDownAtNight", + "Timezone": "Etc/UTC" } - ] + ], + "ServiceNamespace": "dynamodb" } }, "TableReadScalingTargetTracking67DF0596": { diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-dynamodb/test/integ.autoscaling.lit.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-dynamodb/test/integ.autoscaling.lit.js.snapshot/cdk.out index 588d7b269d34f..2313ab5436501 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-dynamodb/test/integ.autoscaling.lit.js.snapshot/cdk.out +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-dynamodb/test/integ.autoscaling.lit.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"20.0.0"} \ No newline at end of file +{"version":"34.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-dynamodb/test/integ.autoscaling.lit.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-dynamodb/test/integ.autoscaling.lit.js.snapshot/integ.json index 8db5a6b10f544..81c9e6f902331 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-dynamodb/test/integ.autoscaling.lit.js.snapshot/integ.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-dynamodb/test/integ.autoscaling.lit.js.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "20.0.0", + "version": "34.0.0", "testCases": { "integ.autoscaling.lit": { "stacks": [ diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-dynamodb/test/integ.autoscaling.lit.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-dynamodb/test/integ.autoscaling.lit.js.snapshot/manifest.json index ceac2d72f9911..d4155ced6dcad 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-dynamodb/test/integ.autoscaling.lit.js.snapshot/manifest.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-dynamodb/test/integ.autoscaling.lit.js.snapshot/manifest.json @@ -1,12 +1,6 @@ { - "version": "20.0.0", + "version": "34.0.0", "artifacts": { - "Tree": { - "type": "cdk:tree", - "properties": { - "file": "tree.json" - } - }, "aws-cdk-dynamodb.assets": { "type": "cdk:asset-manifest", "properties": { @@ -20,10 +14,11 @@ "environment": "aws://unknown-account/unknown-region", "properties": { "templateFile": "aws-cdk-dynamodb.template.json", + "terminationProtection": false, "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/619c64c0f19a7d78d759641bfba47f06ebbd7ffb42aae71ec695a6777a534d01.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/865151155140d0ba599d3522f659db77ada497ea1a763cf11c3fb58d017a3082.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -71,6 +66,12 @@ ] }, "displayName": "aws-cdk-dynamodb" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-dynamodb/test/integ.autoscaling.lit.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-dynamodb/test/integ.autoscaling.lit.js.snapshot/tree.json index a7da46d30ce88..07eb252a889ae 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-dynamodb/test/integ.autoscaling.lit.js.snapshot/tree.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-dynamodb/test/integ.autoscaling.lit.js.snapshot/tree.json @@ -4,14 +4,6 @@ "id": "App", "path": "", "children": { - "Tree": { - "id": "Tree", - "path": "Tree", - "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.1.85" - } - }, "aws-cdk-dynamodb": { "id": "aws-cdk-dynamodb", "path": "aws-cdk-dynamodb", @@ -26,16 +18,16 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::DynamoDB::Table", "aws:cdk:cloudformation:props": { - "keySchema": [ + "attributeDefinitions": [ { "attributeName": "hashKey", - "keyType": "HASH" + "attributeType": "S" } ], - "attributeDefinitions": [ + "keySchema": [ { "attributeName": "hashKey", - "attributeType": "S" + "keyType": "HASH" } ], "provisionedThroughput": { @@ -45,7 +37,7 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-dynamodb.CfnTable", + "fqn": "aws-cdk-lib.aws_dynamodb.CfnTable", "version": "0.0.0" } }, @@ -53,8 +45,8 @@ "id": "ScalingRole", "path": "aws-cdk-dynamodb/Table/ScalingRole", "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.1.85" + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" } }, "ReadScaling": { @@ -101,11 +93,11 @@ ] }, "scalableDimension": "dynamodb:table:ReadCapacityUnits", - "serviceNamespace": "dynamodb", "scheduledActions": [ { "scheduledActionName": "ScaleUpInTheMorning", "schedule": "cron(0 8 * * ? *)", + "timezone": "Etc/UTC", "scalableTargetAction": { "minCapacity": 20 } @@ -113,15 +105,17 @@ { "scheduledActionName": "ScaleDownAtNight", "schedule": "cron(0 20 * * ? *)", + "timezone": "Etc/UTC", "scalableTargetAction": { "maxCapacity": 20 } } - ] + ], + "serviceNamespace": "dynamodb" } }, "constructInfo": { - "fqn": "@aws-cdk/aws-applicationautoscaling.CfnScalableTarget", + "fqn": "aws-cdk-lib.aws_applicationautoscaling.CfnScalableTarget", "version": "0.0.0" } }, @@ -149,44 +143,68 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-applicationautoscaling.CfnScalingPolicy", + "fqn": "aws-cdk-lib.aws_applicationautoscaling.CfnScalingPolicy", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-applicationautoscaling.TargetTrackingScalingPolicy", + "fqn": "aws-cdk-lib.aws_applicationautoscaling.TargetTrackingScalingPolicy", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-applicationautoscaling.ScalableTarget", + "fqn": "aws-cdk-lib.aws_applicationautoscaling.ScalableTarget", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-applicationautoscaling.BaseScalableAttribute", + "fqn": "aws-cdk-lib.aws_applicationautoscaling.BaseScalableAttribute", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-dynamodb.Table", + "fqn": "aws-cdk-lib.aws_dynamodb.Table", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "aws-cdk-dynamodb/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "aws-cdk-dynamodb/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", "version": "0.0.0" } } }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.85" + "version": "10.2.70" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.1.85" + "fqn": "aws-cdk-lib.App", + "version": "0.0.0" } } } \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.autoscaling.lit.js.snapshot/aws-lambda-autoscaling.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.autoscaling.lit.js.snapshot/aws-lambda-autoscaling.assets.json index 784bf713b5a18..adda4ff7aab5e 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.autoscaling.lit.js.snapshot/aws-lambda-autoscaling.assets.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.autoscaling.lit.js.snapshot/aws-lambda-autoscaling.assets.json @@ -1,7 +1,7 @@ { "version": "34.0.0", "files": { - "4e491c14be2b36af5be729715edfad23d2db6e3050125cdd632fce51b0900fb5": { + "54ec09132268008f855bb0ad4863af3ceda54b0640d90c386503d8faf54eeae8": { "source": { "path": "aws-lambda-autoscaling.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "4e491c14be2b36af5be729715edfad23d2db6e3050125cdd632fce51b0900fb5.json", + "objectKey": "54ec09132268008f855bb0ad4863af3ceda54b0640d90c386503d8faf54eeae8.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.autoscaling.lit.js.snapshot/aws-lambda-autoscaling.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.autoscaling.lit.js.snapshot/aws-lambda-autoscaling.template.json index 6ac23d677363c..9da5f1d9d06f2 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.autoscaling.lit.js.snapshot/aws-lambda-autoscaling.template.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.autoscaling.lit.js.snapshot/aws-lambda-autoscaling.template.json @@ -124,14 +124,16 @@ "MinCapacity": 20 }, "Schedule": "cron(0 8 * * ? *)", - "ScheduledActionName": "ScaleUpInTheMorning" + "ScheduledActionName": "ScaleUpInTheMorning", + "Timezone": "Etc/UTC" }, { "ScalableTargetAction": { "MaxCapacity": 20 }, "Schedule": "cron(0 20 * * ? *)", - "ScheduledActionName": "ScaleDownAtNight" + "ScheduledActionName": "ScaleDownAtNight", + "Timezone": "Etc/UTC" } ], "ServiceNamespace": "lambda" diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.autoscaling.lit.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.autoscaling.lit.js.snapshot/manifest.json index 20b77f5b33063..4f657607872f7 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.autoscaling.lit.js.snapshot/manifest.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.autoscaling.lit.js.snapshot/manifest.json @@ -14,10 +14,11 @@ "environment": "aws://unknown-account/unknown-region", "properties": { "templateFile": "aws-lambda-autoscaling.template.json", + "terminationProtection": false, "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/4e491c14be2b36af5be729715edfad23d2db6e3050125cdd632fce51b0900fb5.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/54ec09132268008f855bb0ad4863af3ceda54b0640d90c386503d8faf54eeae8.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -86,15 +87,6 @@ "type": "aws:cdk:logicalId", "data": "CheckBootstrapVersion" } - ], - "MyLambdaCurrentVersionE7A382CC9ef7d0f7e3b3b55a2ac4da5225352f4d": [ - { - "type": "aws:cdk:logicalId", - "data": "MyLambdaCurrentVersionE7A382CC9ef7d0f7e3b3b55a2ac4da5225352f4d", - "trace": [ - "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" - ] - } ] }, "displayName": "aws-lambda-autoscaling" diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.autoscaling.lit.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.autoscaling.lit.js.snapshot/tree.json index cec884fba04b0..7822036ae32e5 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.autoscaling.lit.js.snapshot/tree.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-lambda/test/integ.autoscaling.lit.js.snapshot/tree.json @@ -219,6 +219,7 @@ { "scheduledActionName": "ScaleUpInTheMorning", "schedule": "cron(0 8 * * ? *)", + "timezone": "Etc/UTC", "scalableTargetAction": { "minCapacity": 20 } @@ -226,6 +227,7 @@ { "scheduledActionName": "ScaleDownAtNight", "schedule": "cron(0 20 * * ? *)", + "timezone": "Etc/UTC", "scalableTargetAction": { "maxCapacity": 20 } diff --git a/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule-expression.ts b/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule-expression.ts index 477eee7b20659..9fa2c5165a6e9 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule-expression.ts +++ b/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule-expression.ts @@ -1,5 +1,4 @@ -import * as events from 'aws-cdk-lib/aws-events'; -import { Duration, TimeZone } from 'aws-cdk-lib/core'; +import { Duration, TimeZone, Schedule, CronOptions } from 'aws-cdk-lib/core'; /** * ScheduleExpression for EventBridge Schedule @@ -9,7 +8,7 @@ import { Duration, TimeZone } from 'aws-cdk-lib/core'; * * @see https://docs.aws.amazon.com/scheduler/latest/UserGuide/schedule-types.html */ -export abstract class ScheduleExpression { +export abstract class ScheduleExpression extends Schedule { /** * Construct a one-time schedule from a date. * @@ -17,15 +16,7 @@ export abstract class ScheduleExpression { * @param timeZone The time zone to use for interpreting the date. Default: - UTC */ public static at(date: Date, timeZone?: TimeZone): ScheduleExpression { - try { - const literal = date.toISOString().split('.')[0]; - return new LiteralScheduleExpression(`at(${literal})`, timeZone ?? TimeZone.ETC_UTC); - } catch (e) { - if (e instanceof RangeError) { - throw new Error('Invalid date'); - } - throw e; - } + return super.protectedAt(date, timeZone); } /** @@ -34,7 +25,7 @@ export abstract class ScheduleExpression { * @param timeZone The time zone to use for interpreting the expression. Default: - UTC */ public static expression(expression: string, timeZone?: TimeZone): ScheduleExpression { - return new LiteralScheduleExpression(expression, timeZone ?? TimeZone.ETC_UTC); + return super.protectedExpression(expression, timeZone); } /** @@ -43,53 +34,14 @@ export abstract class ScheduleExpression { * Rates may be defined with any unit of time, but when converted into minutes, the duration must be a positive whole number of minutes. */ public static rate(duration: Duration): ScheduleExpression { - const schedule = events.Schedule.rate(duration); - return new LiteralScheduleExpression(schedule.expressionString); + return super.protectedRate(duration); } /** * Create a recurring schedule from a set of cron fields and time zone. */ - public static cron(options: CronOptionsWithTimezone): ScheduleExpression { - const { timeZone, ...cronOptions } = options; - const schedule = events.Schedule.cron(cronOptions); - return new LiteralScheduleExpression(schedule.expressionString, timeZone); + public static cron(options: CronOptions): ScheduleExpression { + return super.protectedCron(options, 'aws-scheduler'); } - - /** - * Retrieve the expression for this schedule - */ - public abstract readonly expressionString: string; - - /** - * Retrieve the expression for this schedule - */ - public abstract readonly timeZone?: TimeZone; - - protected constructor() {} } -/** - * Options to configure a cron expression - * - * All fields are strings so you can use complex expressions. Absence of - * a field implies '*' or '?', whichever one is appropriate. - * - * @see https://docs.aws.amazon.com/eventbridge/latest/userguide/scheduled-events.html#cron-expressions - */ -export interface CronOptionsWithTimezone extends events.CronOptions { - /** - * The timezone to run the schedule in - * - * @default - TimeZone.ETC_UTC - */ - readonly timeZone?: TimeZone; -} - -const DEFAULT_TIMEZONE = TimeZone.ETC_UTC; - -class LiteralScheduleExpression extends ScheduleExpression { - constructor(public readonly expressionString: string, public readonly timeZone: TimeZone = DEFAULT_TIMEZONE) { - super(); - } -} diff --git a/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule.ts b/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule.ts index 343acc398324f..e349ebb31e2ac 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule.ts +++ b/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule.ts @@ -87,6 +87,7 @@ export class Schedule extends Resource implements ISchedule { this.group = props.group; const targetConfig = props.target.bind(this); + props.schedule._bind(this); const resource = new CfnSchedule(this, 'Resource', { name: this.physicalName, diff --git a/packages/@aws-cdk/aws-scheduler-alpha/lib/target.ts b/packages/@aws-cdk/aws-scheduler-alpha/lib/target.ts index e15b635f7ae08..3850f095ea04e 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/lib/target.ts +++ b/packages/@aws-cdk/aws-scheduler-alpha/lib/target.ts @@ -30,7 +30,7 @@ export interface ScheduleTargetConfig { readonly role: iam.IRole; /** - * What input to pass to the tatget + * What input to pass to the target */ readonly input?: ScheduleTargetInput; diff --git a/packages/@aws-cdk/aws-scheduler-alpha/test/schedule-expression.test.ts b/packages/@aws-cdk/aws-scheduler-alpha/test/schedule-expression.test.ts index d12ca608820d3..6f71558c1a704 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/test/schedule-expression.test.ts +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/schedule-expression.test.ts @@ -28,20 +28,23 @@ describe('schedule expression', () => { }); test('cron expressions saves timezone', () => { - expect(TimeZone.EUROPE_LONDON).toEqual(ScheduleExpression.cron( - { - minute: '0', - hour: '10', - timeZone: TimeZone.EUROPE_LONDON, - }).timeZone); + // GIVEN + const cron = ScheduleExpression.cron({ + minute: '0', + hour: '10', + timeZone: TimeZone.EUROPE_LONDON, + }); + cron._bind(new Stack()); + + // THEN + expect(cron.timeZone).toEqual(TimeZone.EUROPE_LONDON); }); test('cron expressions timezone is UTC if not specified', () => { - expect(TimeZone.ETC_UTC).toEqual(ScheduleExpression.cron( - { - minute: '0', - hour: '10', - }).timeZone); + expect(ScheduleExpression.cron({ + minute: '0', + hour: '10', + }).timeZone).toEqual(TimeZone.ETC_UTC); }); test('rate cannot be 0', () => { diff --git a/packages/@aws-cdk/aws-synthetics-alpha/lib/schedule.ts b/packages/@aws-cdk/aws-synthetics-alpha/lib/schedule.ts index 7be04de89fab3..1d2aa9b88ea67 100644 --- a/packages/@aws-cdk/aws-synthetics-alpha/lib/schedule.ts +++ b/packages/@aws-cdk/aws-synthetics-alpha/lib/schedule.ts @@ -1,9 +1,10 @@ -import { Duration } from 'aws-cdk-lib/core'; +import { Duration, Schedule as CoreSchedule } from 'aws-cdk-lib/core'; +import { Construct } from 'constructs'; /** * Schedule for canary runs */ -export class Schedule { +export class Schedule extends CoreSchedule { /** * The canary will be executed once. @@ -36,39 +37,27 @@ export class Schedule { if (minutes === 0) { return Schedule.once(); } - if (minutes === 1) { - return new Schedule('rate(1 minute)'); - } - return new Schedule(`rate(${minutes} minutes)`); + return super.protectedRate(interval); } /** * Create a schedule from a set of cron fields */ public static cron(options: CronOptions): Schedule { - if (options.weekDay !== undefined && options.day !== undefined) { - throw new Error('Cannot supply both \'day\' and \'weekDay\', use at most one'); - } - - const minute = fallback(options.minute, '*'); - const hour = fallback(options.hour, '*'); - const month = fallback(options.month, '*'); - - // Weekday defaults to '?' if not supplied. If it is supplied, day must become '?' - const day = fallback(options.day, options.weekDay !== undefined ? '?' : '*'); - const weekDay = fallback(options.weekDay, '?'); - - // '*' is only allowed in the year field - const year = '*'; + return super.protectedCron({ + ...options, + year: '*', // '*' is the only allowed value in the year field + }, 'aws-synthetics'); + } - return new Schedule(`cron(${minute} ${hour} ${day} ${month} ${weekDay} ${year})`); + private constructor(public readonly expressionString: string) { + super(); } - private constructor( - /** - * The Schedule expression - */ - public readonly expressionString: string) {} + /** + * @internal + */ + public _bind(_scope: Construct) {} } /** @@ -115,7 +104,3 @@ export interface CronOptions { */ readonly weekDay?: string; } - -function fallback(x: string | undefined, def: string): string { - return x ?? def; -} diff --git a/packages/aws-cdk-lib/aws-applicationautoscaling/README.md b/packages/aws-cdk-lib/aws-applicationautoscaling/README.md index 6bc05ee9882f2..c729bd27b704e 100644 --- a/packages/aws-cdk-lib/aws-applicationautoscaling/README.md +++ b/packages/aws-cdk-lib/aws-applicationautoscaling/README.md @@ -191,7 +191,7 @@ capacity.scaleOnSchedule('PrescaleInTheMorning', { capacity.scaleOnSchedule('AllowDownscalingAtNight', { schedule: appscaling.Schedule.cron({ hour: '20', minute: '0' }), - minCapacity: 1 + minCapacity: 1, }); ``` @@ -220,12 +220,12 @@ const target = new appscaling.ScalableTarget(this, 'ScalableTarget', { minCapacity: 10, resourceId: `function:${handler.functionName}:${fnVer.version}`, scalableDimension: 'lambda:function:ProvisionedConcurrency', -}) +}); target.scaleToTrackMetric('PceTracking', { targetValue: 0.9, predefinedMetric: appscaling.PredefinedMetric.LAMBDA_PROVISIONED_CONCURRENCY_UTILIZATION, -}) +}); ``` ### ElastiCache Redis shards scaling with target value diff --git a/packages/aws-cdk-lib/aws-applicationautoscaling/lib/scalable-target.ts b/packages/aws-cdk-lib/aws-applicationautoscaling/lib/scalable-target.ts index 0e825bce03d19..4f4dee7696fc8 100644 --- a/packages/aws-cdk-lib/aws-applicationautoscaling/lib/scalable-target.ts +++ b/packages/aws-cdk-lib/aws-applicationautoscaling/lib/scalable-target.ts @@ -155,6 +155,7 @@ export class ScalableTarget extends Resource implements IScalableTarget { this.actions.push({ scheduledActionName: id, schedule: action.schedule.expressionString, + timezone: action.schedule.timeZone?.timezoneName, startTime: action.startTime, endTime: action.endTime, scalableTargetAction: { diff --git a/packages/aws-cdk-lib/aws-applicationautoscaling/lib/schedule.ts b/packages/aws-cdk-lib/aws-applicationautoscaling/lib/schedule.ts index 82c73291993ed..3954b0a81efaf 100644 --- a/packages/aws-cdk-lib/aws-applicationautoscaling/lib/schedule.ts +++ b/packages/aws-cdk-lib/aws-applicationautoscaling/lib/schedule.ts @@ -1,87 +1,38 @@ -import { Construct } from 'constructs'; -import { Annotations, Duration } from '../../core'; +import { Duration, TimeZone, CronOptions as CoreCronOptions, Schedule as CoreSchedule } from '../../core'; /** * Schedule for scheduled scaling actions */ -export abstract class Schedule { +export abstract class Schedule extends CoreSchedule { /** * Construct a schedule from a literal schedule expression * * @param expression The expression to use. Must be in a format that Application AutoScaling will recognize */ - public static expression(expression: string): Schedule { - return new LiteralSchedule(expression); + public static expression(expression: string, timeZone?: TimeZone): Schedule { + return super.protectedExpression(expression, timeZone); } /** - * Construct a schedule from an interval and a time unit + * Construct a schedule from an interval and a time unit. Must be a whole number of seconds. */ public static rate(duration: Duration): Schedule { - if (duration.isUnresolved()) { - const validDurationUnit = ['minute', 'minutes', 'hour', 'hours', 'day', 'days']; - if (!validDurationUnit.includes(duration.unitLabel())) { - throw new Error("Allowed units for scheduling are: 'minute', 'minutes', 'hour', 'hours', 'day' or 'days'"); - } - return new LiteralSchedule(`rate(${duration.formatTokenToNumber()})`); - } - if (duration.toSeconds() === 0) { - throw new Error('Duration cannot be 0'); - } - - let rate = maybeRate(duration.toDays({ integral: false }), 'day'); - if (rate === undefined) { rate = maybeRate(duration.toHours({ integral: false }), 'hour'); } - if (rate === undefined) { rate = makeRate(duration.toMinutes({ integral: true }), 'minute'); } - return new LiteralSchedule(rate); + return super.protectedRate(duration); } /** * Construct a Schedule from a moment in time */ - public static at(moment: Date): Schedule { - return new LiteralSchedule(`at(${formatISO(moment)})`); + public static at(moment: Date, timeZone?: TimeZone): Schedule { + return super.protectedAt(moment, timeZone); } /** * Create a schedule from a set of cron fields */ - public static cron(options: CronOptions): Schedule { - if (options.weekDay !== undefined && options.day !== undefined) { - throw new Error('Cannot supply both \'day\' and \'weekDay\', use at most one'); - } - - const minute = fallback(options.minute, '*'); - const hour = fallback(options.hour, '*'); - const month = fallback(options.month, '*'); - const year = fallback(options.year, '*'); - - // Weekday defaults to '?' if not supplied. If it is supplied, day must become '?' - const day = fallback(options.day, options.weekDay !== undefined ? '?' : '*'); - const weekDay = fallback(options.weekDay, '?'); - - return new class extends Schedule { - public readonly expressionString: string = `cron(${minute} ${hour} ${day} ${month} ${weekDay} ${year})`; - public _bind(scope: Construct) { - if (!options.minute) { - Annotations.of(scope).addWarningV2('@aws-cdk/aws-applicationautoscaling:defaultRunEveryMinute', 'cron: If you don\'t pass \'minute\', by default the event runs every minute. Pass \'minute: \'*\'\' if that\'s what you intend, or \'minute: 0\' to run once per hour instead.'); - } - return new LiteralSchedule(this.expressionString); - } - }; + public static cron(options: CoreCronOptions): Schedule { + return super.protectedCron(options, 'aws-applicationautoscaling'); } - - /** - * Retrieve the expression for this schedule - */ - public abstract readonly expressionString: string; - - protected constructor() {} - - /** - * - * @internal - */ - public abstract _bind(scope: Construct): void; } /** @@ -91,92 +42,6 @@ export abstract class Schedule { * a field implies '*' or '?', whichever one is appropriate. * * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html#CronExpressions + * @deprecated use core.CronOptions instead */ -export interface CronOptions { - /** - * The minute to run this rule at - * - * @default - Every minute - */ - readonly minute?: string; - - /** - * The hour to run this rule at - * - * @default - Every hour - */ - readonly hour?: string; - - /** - * The day of the month to run this rule at - * - * @default - Every day of the month - */ - readonly day?: string; - - /** - * The month to run this rule at - * - * @default - Every month - */ - readonly month?: string; - - /** - * The year to run this rule at - * - * @default - Every year - */ - readonly year?: string; - - /** - * The day of the week to run this rule at - * - * @default - Any day of the week - */ - readonly weekDay?: string; -} - -class LiteralSchedule extends Schedule { - constructor(public readonly expressionString: string) { - super(); - } - - public _bind() {} -} - -function fallback(x: T | undefined, def: T): T { - return x === undefined ? def : x; -} - -function formatISO(date?: Date) { - if (!date) { return undefined; } - - return date.getUTCFullYear() + - '-' + pad(date.getUTCMonth() + 1) + - '-' + pad(date.getUTCDate()) + - 'T' + pad(date.getUTCHours()) + - ':' + pad(date.getUTCMinutes()) + - ':' + pad(date.getUTCSeconds()); - - function pad(num: number) { - if (num < 10) { - return '0' + num; - } - return num; - } -} - -/** - * Return the rate if the rate is whole number - */ -function maybeRate(interval: number, singular: string) { - if (interval === 0 || !Number.isInteger(interval)) { return undefined; } - return makeRate(interval, singular); -} - -/** - * Return 'rate(${interval} ${singular}(s))` for the interval - */ -function makeRate(interval: number, singular: string) { - return interval === 1 ? `rate(1 ${singular})` : `rate(${interval} ${singular}s)`; -} +export interface CronOptions extends CoreCronOptions {} diff --git a/packages/aws-cdk-lib/aws-applicationautoscaling/test/schedule.test.ts b/packages/aws-cdk-lib/aws-applicationautoscaling/test/schedule.test.ts index 86d4d06c8cae6..59e597522f007 100644 --- a/packages/aws-cdk-lib/aws-applicationautoscaling/test/schedule.test.ts +++ b/packages/aws-cdk-lib/aws-applicationautoscaling/test/schedule.test.ts @@ -1,15 +1,17 @@ -import { Duration, Stack, Lazy } from '../../core'; +import { Duration, Stack, Lazy, TimeZone } from '../../core'; import * as appscaling from '../lib'; describe('cron', () => { test('test utc cron, hour only', () => { expect(appscaling.Schedule.cron({ hour: '18', minute: '0' }).expressionString).toEqual('cron(0 18 * * ? *)'); - }); test('test utc cron, hour and minute', () => { expect(appscaling.Schedule.cron({ hour: '18', minute: '24' }).expressionString).toEqual('cron(24 18 * * ? *)'); + }); + test('test europe/london cron', () => { + expect(appscaling.Schedule.cron({ hour: '18', minute: '0', timeZone: TimeZone.EUROPE_LONDON }).timeZone?.timezoneName).toEqual('Europe/London'); }); }); @@ -17,28 +19,24 @@ describe('rate', () => { test('rate must be whole number of minutes', () => { expect(() => { appscaling.Schedule.rate(Duration.minutes(0.13456)); - }).toThrow(/'0.13456 minutes' cannot be converted into a whole number of seconds/); - + }).toThrow(/0.13456 must be a whole number of minutes/); }); test('rate can be in seconds', () => { const duration = appscaling.Schedule.rate(Duration.seconds(120)); expect('rate(2 minutes)').toEqual(duration.expressionString); - }); test('rate must not be in seconds when specified as a token', () => { expect(() => { appscaling.Schedule.rate(Duration.seconds(Lazy.number({ produce: () => 5 }))); }).toThrow(/Allowed units for scheduling/); - }); test('rate cannot be 0', () => { expect(() => { appscaling.Schedule.rate(Duration.days(0)); }).toThrow(/Duration cannot be 0/); - }); test('rate can be token', () => { @@ -46,20 +44,17 @@ describe('rate', () => { const lazyDuration = Duration.minutes(Lazy.number({ produce: () => 5 })); const rate = appscaling.Schedule.rate(lazyDuration); expect('rate(5 minutes)').toEqual(stack.resolve(rate).expressionString); - }); test('rate can be in allowed type hours', () => { expect('rate(1 hour)').toEqual(appscaling.Schedule.rate(Duration.hours(1)) .expressionString); - }); }); describe('expression', () => { test('test using a literal schedule expression', () => { expect(appscaling.Schedule.expression('cron(0 18 * * ? *)').expressionString).toEqual('cron(0 18 * * ? *)'); - }); }); diff --git a/packages/aws-cdk-lib/aws-autoscaling/README.md b/packages/aws-cdk-lib/aws-autoscaling/README.md index 9fb6dd8c8d81f..2ff0df4646877 100644 --- a/packages/aws-cdk-lib/aws-autoscaling/README.md +++ b/packages/aws-cdk-lib/aws-autoscaling/README.md @@ -288,7 +288,7 @@ autoScalingGroup.scaleOnSchedule('PrescaleInTheMorning', { autoScalingGroup.scaleOnSchedule('AllowDownscalingAtNight', { schedule: autoscaling.Schedule.cron({ hour: '20', minute: '0' }), - minCapacity: 1 + minCapacity: 1, }); ``` diff --git a/packages/aws-cdk-lib/aws-autoscaling/lib/schedule.ts b/packages/aws-cdk-lib/aws-autoscaling/lib/schedule.ts index 7f532e65ac62a..79cc02545b5d9 100644 --- a/packages/aws-cdk-lib/aws-autoscaling/lib/schedule.ts +++ b/packages/aws-cdk-lib/aws-autoscaling/lib/schedule.ts @@ -1,57 +1,44 @@ import { Construct } from 'constructs'; -import { Annotations } from '../../core'; +import { Annotations, CronOptions as CoreCronOptions, Schedule as CoreSchedule, TimeZone } from '../../core'; /** * Schedule for scheduled scaling actions */ -export abstract class Schedule { +export abstract class Schedule extends CoreSchedule { /** * Construct a schedule from a literal schedule expression * * @param expression The expression to use. Must be in a format that AutoScaling will recognize * @see http://crontab.org/ */ - public static expression(expression: string): Schedule { - return new LiteralSchedule(expression); + public static expression(expression: string, timeZone?: TimeZone): Schedule { + return super.protectedExpression(expression, timeZone); } /** * Create a schedule from a set of cron fields */ - public static cron(options: CronOptions): Schedule { - if (options.weekDay !== undefined && options.day !== undefined) { - throw new Error('Cannot supply both \'day\' and \'weekDay\', use at most one'); - } - - const minute = fallback(options.minute, '*'); - const hour = fallback(options.hour, '*'); - const month = fallback(options.month, '*'); - const day = fallback(options.day, '*'); - const weekDay = fallback(options.weekDay, '*'); + public static cron(options: CoreCronOptions): Schedule { + const cron = super.protectedCron({ + weekDay: '*', // to override core.Schedule's default + day: '*', // to override core.Schedule's default + ...options, + }); + const cronSplit = cron.expressionString.slice(5).split(' '); // remove "cron(" from start + cronSplit.pop(); // remove year, since autoscaling does not accept it + const autoscalingCron = cronSplit.join(' '); return new class extends Schedule { - public readonly expressionString: string = `${minute} ${hour} ${day} ${month} ${weekDay}`; + public readonly expressionString = autoscalingCron; + public readonly timeZone = options.timeZone; public _bind(scope: Construct) { if (!options.minute) { Annotations.of(scope).addWarningV2('@aws-cdk/aws-autoscaling:scheduleDefaultRunsEveryMinute', 'cron: If you don\'t pass \'minute\', by default the event runs every minute. Pass \'minute: \'*\'\' if that\'s what you intend, or \'minute: 0\' to run once per hour instead.'); } - return new LiteralSchedule(this.expressionString); + return Schedule.expression(this.expressionString, this.timeZone); } }; } - - /** - * Retrieve the expression for this schedule - */ - public abstract readonly expressionString: string; - - protected constructor() {} - - /** - * - * @internal - */ - public abstract _bind(scope: Construct): void; } /** @@ -61,52 +48,6 @@ export abstract class Schedule { * a field implies '*' or '?', whichever one is appropriate. * * @see http://crontab.org/ + * @deprecated use core.CronOptions */ -export interface CronOptions { - /** - * The minute to run this rule at - * - * @default - Every minute - */ - readonly minute?: string; - - /** - * The hour to run this rule at - * - * @default - Every hour - */ - readonly hour?: string; - - /** - * The day of the month to run this rule at - * - * @default - Every day of the month - */ - readonly day?: string; - - /** - * The month to run this rule at - * - * @default - Every month - */ - readonly month?: string; - - /** - * The day of the week to run this rule at - * - * @default - Any day of the week - */ - readonly weekDay?: string; -} - -class LiteralSchedule extends Schedule { - constructor(public readonly expressionString: string) { - super(); - } - - public _bind(): void {} -} - -function fallback(x: T | undefined, def: T): T { - return x === undefined ? def : x; -} +export interface CronOptions extends CoreCronOptions {} diff --git a/packages/aws-cdk-lib/aws-autoscaling/lib/scheduled-action.ts b/packages/aws-cdk-lib/aws-autoscaling/lib/scheduled-action.ts index 84b2d55f63b1d..468b4027dcd20 100644 --- a/packages/aws-cdk-lib/aws-autoscaling/lib/scheduled-action.ts +++ b/packages/aws-cdk-lib/aws-autoscaling/lib/scheduled-action.ts @@ -16,9 +16,11 @@ export interface BasicScheduledActionProps { * For more information, see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. * * @default - UTC + * @deprecated use time zones as part of the schedule prop * */ readonly timeZone?: string; + /** * When to perform this action. * @@ -104,6 +106,10 @@ export class ScheduledAction extends Resource { throw new Error('At least one of minCapacity, maxCapacity, or desiredCapacity is required'); } + if (props.timeZone && props.schedule.timeZone) { + throw new Error('Cannot set `timeZone` property and `schedule` property with time zone defined. Please remove the deprecated `timeZone` property.'); + } + // add a warning on synth when minute is not defined in a cron schedule props.schedule._bind(this); @@ -115,7 +121,7 @@ export class ScheduledAction extends Resource { maxSize: props.maxCapacity, desiredCapacity: props.desiredCapacity, recurrence: props.schedule.expressionString, - timeZone: props.timeZone, + timeZone: props.schedule.timeZone?.timezoneName ?? props.timeZone, }); this.scheduledActionName = resource.attrScheduledActionName; diff --git a/packages/aws-cdk-lib/aws-autoscaling/test/scheduled-action.test.ts b/packages/aws-cdk-lib/aws-autoscaling/test/scheduled-action.test.ts index 943939c09febb..33fb769a47d15 100644 --- a/packages/aws-cdk-lib/aws-autoscaling/test/scheduled-action.test.ts +++ b/packages/aws-cdk-lib/aws-autoscaling/test/scheduled-action.test.ts @@ -62,6 +62,42 @@ describeDeprecated('scheduled action', () => { }); }); + test('set timezone as part of schedule', () => { + // GIVEN + const stack = new cdk.Stack(); + const asg = makeAutoScalingGroup(stack); + + // WHEN + asg.scaleOnSchedule('ScaleOutAtMiddaySeoul', { + schedule: autoscaling.Schedule.expression('0 12 * * *', cdk.TimeZone.ASIA_SEOUL), + minCapacity: 12, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScheduledAction', { + MinSize: 12, + Recurrence: '0 12 * * *', + TimeZone: 'Asia/Seoul', + }); + }); + + test('throws when timezone and scheduled timezone set together', () => { + // GIVEN + const stack = new cdk.Stack(); + const asg = makeAutoScalingGroup(stack); + + // THEN + expect(() => asg.scaleOnSchedule('ScaleOutAtMiddaySeoul', { + schedule: autoscaling.Schedule.cron({ + hour: '12', + minute: '0', + timeZone: cdk.TimeZone.ASIA_SEOUL, + }), + minCapacity: 12, + timeZone: 'Asia/Seoul', + })).toThrowError(/Please remove the deprecated `timeZone` property./); + }); + test('autoscaling group has recommended updatepolicy for scheduled actions', () => { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/aws-cdk-lib/aws-backup/README.md b/packages/aws-cdk-lib/aws-backup/README.md index 27b2fcc3667f1..fe2abaae5608d 100644 --- a/packages/aws-cdk-lib/aws-backup/README.md +++ b/packages/aws-cdk-lib/aws-backup/README.md @@ -8,10 +8,10 @@ configure backup policies and monitor backup activity for your AWS resources in ## Backup plan and selection In AWS Backup, a *backup plan* is a policy expression that defines when and how you want to back up - your AWS resources, such as Amazon DynamoDB tables or Amazon Elastic File System (Amazon EFS) file - systems. You can assign resources to backup plans, and AWS Backup automatically backs up and retains - backups for those resources according to the backup plan. You can create multiple backup plans if you - have workloads with different backup requirements. +your AWS resources, such as Amazon DynamoDB tables or Amazon Elastic File System (Amazon EFS) file +systems. You can assign resources to backup plans, and AWS Backup automatically backs up and retains +backups for those resources according to the backup plan. You can create multiple backup plans if you +have workloads with different backup requirements. This module provides ready-made backup plans (similar to the console experience): @@ -52,8 +52,8 @@ plan.addSelection('Selection', { backup.BackupResource.fromRdsServerlessCluster(myServerlessCluster), // An Aurora Serverless cluster backup.BackupResource.fromTag('stage', 'prod'), // All resources that are tagged stage=prod in the region/account backup.BackupResource.fromConstruct(myCoolConstruct), // All backupable resources in `myCoolConstruct` - ] -}) + ], +}); ``` If not specified, a new IAM role with a managed policy for backup will be @@ -66,7 +66,7 @@ declare const plan: backup.BackupPlan; plan.addRule(new backup.BackupPlanRule({ completionWindow: Duration.hours(2), startWindow: Duration.hours(1), - scheduleExpression: events.Schedule.cron({ // Only cron expressions are supported + schedule: backup.Schedule.cron({ // Only cron expressions are supported day: '15', hour: '3', minute: '30', @@ -100,7 +100,7 @@ plan.addRule(new backup.BackupPlanRule({ destinationBackupVault: secondaryVault, moveToColdStorageAfter: Duration.days(30), deleteAfter: Duration.days(120), - }] + }], })); ``` @@ -193,7 +193,7 @@ const vault = new backup.BackupVault(this, 'Vault', { }), ], }), -}) +}); ``` Alternativately statements can be added to the vault policy using `addToAccessPolicy()`. diff --git a/packages/aws-cdk-lib/aws-backup/lib/index.ts b/packages/aws-cdk-lib/aws-backup/lib/index.ts index 7b34df14d75df..2e6094022a9e1 100644 --- a/packages/aws-cdk-lib/aws-backup/lib/index.ts +++ b/packages/aws-cdk-lib/aws-backup/lib/index.ts @@ -3,6 +3,7 @@ export * from './plan'; export * from './rule'; export * from './selection'; export * from './resource'; +export * from './schedule'; // AWS::Backup CloudFormation Resources: export * from './backup.generated'; diff --git a/packages/aws-cdk-lib/aws-backup/lib/plan.ts b/packages/aws-cdk-lib/aws-backup/lib/plan.ts index a7ce167ac92ea..96789462488c5 100644 --- a/packages/aws-cdk-lib/aws-backup/lib/plan.ts +++ b/packages/aws-cdk-lib/aws-backup/lib/plan.ts @@ -187,7 +187,8 @@ export class BackupPlan extends Resource implements IBackupPlan { moveToColdStorageAfterDays: rule.props.moveToColdStorageAfter?.toDays(), }, ruleName: rule.props.ruleName ?? `${this.node.id}Rule${this.rules.length}`, - scheduleExpression: rule.props.scheduleExpression?.expressionString, + scheduleExpression: rule.props.schedule?.expressionString ?? rule.props.scheduleExpression?.expressionString, + scheduleExpressionTimezone: rule.props.schedule?.timeZone?.timezoneName, startWindowMinutes: rule.props.startWindow?.toMinutes(), enableContinuousBackup: rule.props.enableContinuousBackup, targetBackupVault: vault.backupVaultName, diff --git a/packages/aws-cdk-lib/aws-backup/lib/rule.ts b/packages/aws-cdk-lib/aws-backup/lib/rule.ts index 566f1edbde1ac..30591c29735c3 100644 --- a/packages/aws-cdk-lib/aws-backup/lib/rule.ts +++ b/packages/aws-cdk-lib/aws-backup/lib/rule.ts @@ -1,3 +1,4 @@ +import { Schedule } from './schedule'; import { IBackupVault } from './vault'; import * as events from '../../aws-events'; import { Duration, Token } from '../../core'; @@ -41,9 +42,17 @@ export interface BackupPlanRuleProps { * A CRON expression specifying when AWS Backup initiates a backup job. * * @default - no schedule + * @deprecated use schedule prop instead */ readonly scheduleExpression?: events.Schedule; + /** + * A CRON expression specifying when AWS Backup initiates a backup job. + * + * @default - no schedule + */ + readonly schedule?: Schedule; + /** * The duration after a backup is scheduled before a job is canceled if it doesn't start successfully. * @@ -122,7 +131,7 @@ export class BackupPlanRule { return new BackupPlanRule({ backupVault, ruleName: 'Daily', - scheduleExpression: events.Schedule.cron({ + schedule: Schedule.cron({ hour: '5', minute: '0', }), @@ -137,7 +146,7 @@ export class BackupPlanRule { return new BackupPlanRule({ backupVault, ruleName: 'Weekly', - scheduleExpression: events.Schedule.cron({ + schedule: Schedule.cron({ hour: '5', minute: '0', weekDay: 'SAT', @@ -153,7 +162,7 @@ export class BackupPlanRule { return new BackupPlanRule({ backupVault, ruleName: 'Monthly1Year', - scheduleExpression: events.Schedule.cron({ + schedule: Schedule.cron({ day: '1', hour: '5', minute: '0', @@ -170,7 +179,7 @@ export class BackupPlanRule { return new BackupPlanRule({ backupVault, ruleName: 'Monthly5Year', - scheduleExpression: events.Schedule.cron({ + schedule: Schedule.cron({ day: '1', hour: '5', minute: '0', @@ -187,7 +196,7 @@ export class BackupPlanRule { return new BackupPlanRule({ backupVault, ruleName: 'Monthly7Year', - scheduleExpression: events.Schedule.cron({ + schedule: Schedule.cron({ day: '1', hour: '5', minute: '0', @@ -200,7 +209,7 @@ export class BackupPlanRule { /** * Properties of BackupPlanRule */ - public readonly props: BackupPlanRuleProps + public readonly props: BackupPlanRuleProps; /** @param props Rule properties */ constructor(props: BackupPlanRuleProps) { @@ -213,6 +222,10 @@ export class BackupPlanRule { throw new Error('`scheduleExpression` must be of type `cron`'); } + if (props.schedule && props.scheduleExpression) { + throw new Error('Cannot specify `schedule` and `scheduleExpression` together. Please use `schedule` only.'); + } + const deleteAfter = (props.enableContinuousBackup && !props.deleteAfter) ? Duration.days(35) : props.deleteAfter; if (props.enableContinuousBackup && props.moveToColdStorageAfter) { diff --git a/packages/aws-cdk-lib/aws-backup/lib/schedule.ts b/packages/aws-cdk-lib/aws-backup/lib/schedule.ts new file mode 100644 index 0000000000000..25c38df03b52e --- /dev/null +++ b/packages/aws-cdk-lib/aws-backup/lib/schedule.ts @@ -0,0 +1,19 @@ +import { CronOptions, Schedule as CoreSchedule, TimeZone } from '../../core'; + +export abstract class Schedule extends CoreSchedule { + /** + * Construct a schedule from a literal schedule expression + * + * @param expression The expression to use. Must be in a format that AWS Backup will recognize + */ + public static expression(expression: string, timeZone?: TimeZone): Schedule { + return super.protectedExpression(expression, timeZone); + } + + /** + * Construct a schedule from cron options + */ + public static cron(options: CronOptions): Schedule { + return super.protectedCron(options, 'aws-backup'); + } +} diff --git a/packages/aws-cdk-lib/aws-backup/test/plan.test.ts b/packages/aws-cdk-lib/aws-backup/test/plan.test.ts index 4d9c23700d651..3adb1b5e50c2b 100644 --- a/packages/aws-cdk-lib/aws-backup/test/plan.test.ts +++ b/packages/aws-cdk-lib/aws-backup/test/plan.test.ts @@ -1,7 +1,8 @@ +import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import { Template } from '../../assertions'; import * as events from '../../aws-events'; import { App, Duration, Stack } from '../../core'; -import { BackupPlan, BackupPlanRule, BackupVault } from '../lib'; +import { BackupPlan, BackupPlanRule, BackupVault, Schedule } from '../lib'; let stack: Stack; beforeEach(() => { @@ -20,7 +21,7 @@ test('create a plan and add rules', () => { new BackupPlanRule({ completionWindow: Duration.hours(2), startWindow: Duration.hours(1), - scheduleExpression: events.Schedule.cron({ + schedule: Schedule.cron({ day: '15', hour: '3', minute: '30', @@ -157,7 +158,7 @@ test('create a plan and add rules - add BackupPlan.AdvancedBackupSettings.Backup new BackupPlanRule({ completionWindow: Duration.hours(2), startWindow: Duration.hours(1), - scheduleExpression: events.Schedule.cron({ + schedule: Schedule.cron({ day: '15', hour: '3', minute: '30', @@ -380,12 +381,19 @@ test('throws when deleteAfter is not greater than moveToColdStorageAfter', () => })).toThrow(/`deleteAfter` must be greater than `moveToColdStorageAfter`/); }); -test('throws when scheduleExpression is not of type cron', () => { +testDeprecated('throws when scheduleExpression is not of type cron', () => { expect(() => new BackupPlanRule({ scheduleExpression: events.Schedule.rate(Duration.hours(5)), })).toThrow(/`scheduleExpression` must be of type `cron`/); }); +testDeprecated('throws when schedule and scheduleExpression are both set', () => { + expect(() => new BackupPlanRule({ + schedule: Schedule.cron({ day: '* ' }), + scheduleExpression: events.Schedule.cron({ day: '?' }), + })).toThrow(/Please use `schedule` only./); +}); + test('synth fails when plan has no rules', () => { // GIVEN const app = new App(); diff --git a/packages/aws-cdk-lib/aws-dynamodb/test/dynamodb.test.ts b/packages/aws-cdk-lib/aws-dynamodb/test/dynamodb.test.ts index edab404c22f61..a92ccb0473df4 100644 --- a/packages/aws-cdk-lib/aws-dynamodb/test/dynamodb.test.ts +++ b/packages/aws-cdk-lib/aws-dynamodb/test/dynamodb.test.ts @@ -1548,7 +1548,7 @@ test('scheduled scaling shows warning when minute is not defined in cron', () => }); // THEN - Annotations.fromStack(stack).hasWarning('/Default/MyTable/ReadScaling/Target', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead. [ack: @aws-cdk/aws-applicationautoscaling:defaultRunEveryMinute]"); + Annotations.fromStack(stack).hasWarning('/Default/MyTable/ReadScaling/Target', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead. [ack: @aws-cdk/aws-applicationautoscaling:scheduleDefaultRunsEveryMinute]"); }); test('scheduled scaling shows no warning when minute is * in cron', () => { diff --git a/packages/aws-cdk-lib/aws-ecs-patterns/test/ec2/scheduled-ecs-task.test.ts b/packages/aws-cdk-lib/aws-ecs-patterns/test/ec2/scheduled-ecs-task.test.ts index 764d99d4216c0..2d9704e419923 100644 --- a/packages/aws-cdk-lib/aws-ecs-patterns/test/ec2/scheduled-ecs-task.test.ts +++ b/packages/aws-cdk-lib/aws-ecs-patterns/test/ec2/scheduled-ecs-task.test.ts @@ -364,7 +364,7 @@ test('Scheduled Ec2 Task shows warning when minute is not defined in cron', () = }); // THEN - Annotations.fromStack(stack).hasWarning('/Default', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead. [ack: @aws-cdk/aws-events:scheduleWillRunEveryMinute]"); + Annotations.fromStack(stack).hasWarning('/Default', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead. [ack: @aws-cdk/aws-events:scheduleDefaultRunsEveryMinute]"); }); test('Scheduled Ec2 Task shows no warning when minute is * in cron', () => { diff --git a/packages/aws-cdk-lib/aws-ecs-patterns/test/fargate/scheduled-fargate-task.test.ts b/packages/aws-cdk-lib/aws-ecs-patterns/test/fargate/scheduled-fargate-task.test.ts index 8b318fd8b1167..8e425b9230542 100644 --- a/packages/aws-cdk-lib/aws-ecs-patterns/test/fargate/scheduled-fargate-task.test.ts +++ b/packages/aws-cdk-lib/aws-ecs-patterns/test/fargate/scheduled-fargate-task.test.ts @@ -451,7 +451,7 @@ test('Scheduled Fargate Task shows warning when minute is not defined in cron', }); // THEN - Annotations.fromStack(stack).hasWarning('/Default', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead. [ack: @aws-cdk/aws-events:scheduleWillRunEveryMinute]"); + Annotations.fromStack(stack).hasWarning('/Default', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead. [ack: @aws-cdk/aws-events:scheduleDefaultRunsEveryMinute]"); }); test('Scheduled Fargate Task shows no warning when minute is * in cron', () => { diff --git a/packages/aws-cdk-lib/aws-ecs/test/fargate/fargate-service.test.ts b/packages/aws-cdk-lib/aws-ecs/test/fargate/fargate-service.test.ts index aec9f09bff889..d85edf4e940b3 100644 --- a/packages/aws-cdk-lib/aws-ecs/test/fargate/fargate-service.test.ts +++ b/packages/aws-cdk-lib/aws-ecs/test/fargate/fargate-service.test.ts @@ -2493,7 +2493,7 @@ describe('fargate service', () => { }); // THEN - Annotations.fromStack(stack).hasWarning('/Default/Service/TaskCount/Target', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead. [ack: @aws-cdk/aws-applicationautoscaling:defaultRunEveryMinute]"); + Annotations.fromStack(stack).hasWarning('/Default/Service/TaskCount/Target', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead. [ack: @aws-cdk/aws-applicationautoscaling:scheduleDefaultRunsEveryMinute]"); }); test('scheduled scaling shows no warning when minute is * in cron', () => { diff --git a/packages/aws-cdk-lib/aws-events/lib/schedule.ts b/packages/aws-cdk-lib/aws-events/lib/schedule.ts index 94b3ecbf7f1e0..d4d6549e8b611 100644 --- a/packages/aws-cdk-lib/aws-events/lib/schedule.ts +++ b/packages/aws-cdk-lib/aws-events/lib/schedule.ts @@ -1,5 +1,4 @@ -import { Construct } from 'constructs'; -import { Annotations, Duration } from '../../core'; +import { Duration, Schedule as CoreSchedule } from '../../core'; /** * Schedule for scheduled event rules @@ -8,14 +7,14 @@ import { Annotations, Duration } from '../../core'; * * @see https://docs.aws.amazon.com/eventbridge/latest/userguide/scheduled-events.html */ -export abstract class Schedule { +export abstract class Schedule extends CoreSchedule { /** * Construct a schedule from a literal schedule expression * * @param expression The expression to use. Must be in a format that EventBridge will recognize */ public static expression(expression: string): Schedule { - return new LiteralSchedule(expression); + return super.protectedExpression(expression); } /** @@ -24,63 +23,15 @@ export abstract class Schedule { * Rates may be defined with any unit of time, but when converted into minutes, the duration must be a positive whole number of minutes. */ public static rate(duration: Duration): Schedule { - if (duration.isUnresolved()) { - const validDurationUnit = ['minute', 'minutes', 'hour', 'hours', 'day', 'days']; - if (validDurationUnit.indexOf(duration.unitLabel()) === -1) { - throw new Error("Allowed units for scheduling are: 'minute', 'minutes', 'hour', 'hours', 'day', 'days'"); - } - return new LiteralSchedule(`rate(${duration.formatTokenToNumber()})`); - } - if (duration.toMinutes() === 0) { - throw new Error('Duration cannot be 0'); - } - - let rate = maybeRate(duration.toDays({ integral: false }), 'day'); - if (rate === undefined) { rate = maybeRate(duration.toHours({ integral: false }), 'hour'); } - if (rate === undefined) { rate = makeRate(duration.toMinutes({ integral: true }), 'minute'); } - return new LiteralSchedule(rate); + return super.protectedRate(duration); } /** * Create a schedule from a set of cron fields */ public static cron(options: CronOptions): Schedule { - if (options.weekDay !== undefined && options.day !== undefined) { - throw new Error('Cannot supply both \'day\' and \'weekDay\', use at most one'); - } - - const minute = fallback(options.minute, '*'); - const hour = fallback(options.hour, '*'); - const month = fallback(options.month, '*'); - const year = fallback(options.year, '*'); - - // Weekday defaults to '?' if not supplied. If it is supplied, day must become '?' - const day = fallback(options.day, options.weekDay !== undefined ? '?' : '*'); - const weekDay = fallback(options.weekDay, '?'); - - return new class extends Schedule { - public readonly expressionString: string = `cron(${minute} ${hour} ${day} ${month} ${weekDay} ${year})`; - public _bind(scope: Construct) { - if (!options.minute) { - Annotations.of(scope).addWarningV2('@aws-cdk/aws-events:scheduleWillRunEveryMinute', 'cron: If you don\'t pass \'minute\', by default the event runs every minute. Pass \'minute: \'*\'\' if that\'s what you intend, or \'minute: 0\' to run once per hour instead.'); - } - return new LiteralSchedule(this.expressionString); - } - }; + return super.protectedCron(options, 'aws-events'); } - - /** - * Retrieve the expression for this schedule - */ - public abstract readonly expressionString: string; - - protected constructor() {} - - /** - * - * @internal - */ - public abstract _bind(scope: Construct): void; } /** @@ -134,30 +85,3 @@ export interface CronOptions { */ readonly weekDay?: string; } - -class LiteralSchedule extends Schedule { - constructor(public readonly expressionString: string) { - super(); - } - - public _bind() {} -} - -function fallback(x: T | undefined, def: T): T { - return x ?? def; -} - -/** - * Return the rate if the rate is whole number - */ -function maybeRate(interval: number, singular: string) { - if (interval === 0 || !Number.isInteger(interval)) { return undefined; } - return makeRate(interval, singular); -} - -/** - * Return 'rate(${interval} ${singular}(s))` for the interval - */ -function makeRate(interval: number, singular: string) { - return interval === 1 ? `rate(1 ${singular})` : `rate(${interval} ${singular}s)`; -} diff --git a/packages/aws-cdk-lib/aws-events/test/rule.test.ts b/packages/aws-cdk-lib/aws-events/test/rule.test.ts index 6cca0b11ba804..f31f7dd5b7ea8 100644 --- a/packages/aws-cdk-lib/aws-events/test/rule.test.ts +++ b/packages/aws-cdk-lib/aws-events/test/rule.test.ts @@ -38,7 +38,7 @@ describe('rule', () => { }), }); - Annotations.fromStack(stack).hasWarning('/Default/MyRule', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead. [ack: @aws-cdk/aws-events:scheduleWillRunEveryMinute]"); + Annotations.fromStack(stack).hasWarning('/Default/MyRule', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead. [ack: @aws-cdk/aws-events:scheduleDefaultRunsEveryMinute]"); }); test('rule does not display warning when minute is set to * in cron', () => { diff --git a/packages/aws-cdk-lib/aws-lambda/test/alias.test.ts b/packages/aws-cdk-lib/aws-lambda/test/alias.test.ts index 34ee5d33c52bd..a5081ea79593a 100644 --- a/packages/aws-cdk-lib/aws-lambda/test/alias.test.ts +++ b/packages/aws-cdk-lib/aws-lambda/test/alias.test.ts @@ -602,7 +602,7 @@ describe('alias', () => { }); // THEN - Annotations.fromStack(stack).hasWarning('/Default/Alias/AliasScaling/Target', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead. [ack: @aws-cdk/aws-applicationautoscaling:defaultRunEveryMinute]"); + Annotations.fromStack(stack).hasWarning('/Default/Alias/AliasScaling/Target', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead. [ack: @aws-cdk/aws-applicationautoscaling:scheduleDefaultRunsEveryMinute]"); }); test('scheduled scaling shows no warning when minute is * in cron', () => { diff --git a/packages/aws-cdk-lib/core/lib/helpers-internal/index.ts b/packages/aws-cdk-lib/core/lib/helpers-internal/index.ts index 03a39234e8d52..50003af38ab8f 100644 --- a/packages/aws-cdk-lib/core/lib/helpers-internal/index.ts +++ b/packages/aws-cdk-lib/core/lib/helpers-internal/index.ts @@ -3,4 +3,4 @@ export * from './cfn-parse'; export { md5hash } from '../private/md5'; export * from './customize-roles'; export * from './string-specializer'; -export { constructInfoFromConstruct, constructInfoFromStack } from '../private/runtime-info'; \ No newline at end of file +export { constructInfoFromConstruct, constructInfoFromStack } from '../private/runtime-info'; diff --git a/packages/aws-cdk-lib/core/lib/index.ts b/packages/aws-cdk-lib/core/lib/index.ts index b35e89c0e59e4..b84f63ce14592 100644 --- a/packages/aws-cdk-lib/core/lib/index.ts +++ b/packages/aws-cdk-lib/core/lib/index.ts @@ -71,3 +71,4 @@ export * from './validation'; export * from './private/intrinsic'; export * from './names'; export * from './time-zone'; +export * from './schedule'; \ No newline at end of file diff --git a/packages/aws-cdk-lib/core/lib/schedule.ts b/packages/aws-cdk-lib/core/lib/schedule.ts new file mode 100644 index 0000000000000..4c30eb3a86a96 --- /dev/null +++ b/packages/aws-cdk-lib/core/lib/schedule.ts @@ -0,0 +1,205 @@ +import { Construct } from 'constructs'; +import { Annotations } from './annotations'; +import { Duration } from './duration'; +import { TimeZone } from './time-zone'; + +/** + * A Core Schedule. This construct is not meant to be used as is or exposed to consumers in other modules. + * It is meant to be extended by other modules that require some sort of schedule implementation. All + * methods in `core.Schedule` are protected, so that construct authors can decide which APIs to expose. + * + * @see https://docs.aws.amazon.com/eventbridge/latest/userguide/scheduled-events.html + */ +export abstract class Schedule { + /** + * Construct a one-time schedule from a date. + * + * @param date The date and time to use. The millisecond part will be ignored. + * @param timeZone The time zone to use for interpreting the date. Default: - UTC + */ + protected static protectedAt(date: Date, timeZone?: TimeZone): Schedule { + try { + const literal = date.toISOString().split('.')[0]; + return new LiteralSchedule(`at(${literal})`, timeZone ?? DEFAULT_TIMEZONE); + } catch (e) { + if (e instanceof RangeError) { + throw new Error('Invalid date'); + } + throw e; + } + } + + /** + * Construct a schedule from a literal schedule expression + * + * @param expression The expression to use. Must be in a format that EventBridge will recognize + * @param timeZone The time zone, if applicable. This is only valid for 'at' and 'cron' expressions + */ + protected static protectedExpression(expression: string, timeZone?: TimeZone): Schedule { + return new LiteralSchedule(expression, timeZone ?? DEFAULT_TIMEZONE); + } + + /** + * Construct a schedule from an interval and a time unit + * + * Rates may be defined with any unit of time, but when converted into minutes, the duration must be a positive whole number of minutes. + */ + protected static protectedRate(duration: Duration): Schedule { + if (duration.isUnresolved()) { + const validDurationUnit = ['minute', 'minutes', 'hour', 'hours', 'day', 'days']; + if (validDurationUnit.indexOf(duration.unitLabel()) === -1) { + throw new Error("Allowed units for scheduling are: 'minute', 'minutes', 'hour', 'hours', 'day', 'days'"); + } + return new LiteralSchedule(`rate(${duration.formatTokenToNumber()})`); + } + if (duration.toMinutes() === 0) { + throw new Error('Duration cannot be 0'); + } + + let rate = maybeRate(duration.toDays({ integral: false }), 'day'); + if (rate === undefined) { rate = maybeRate(duration.toHours({ integral: false }), 'hour'); } + if (rate === undefined) { rate = makeRate(duration.toMinutes({ integral: true }), 'minute'); } + return new LiteralSchedule(rate); + } + + /** + * Create a schedule from a set of cron fields. + * + * @param module the module calling protectedCron, if you want module-specific warnings (i.e. aws-applicationautoscaling) + */ + protected static protectedCron(options: CronOptions, module?: string): Schedule { + if (options.weekDay !== undefined && options.day !== undefined && + !(options.weekDay === '*' && options.day === '*') // special case for aws-autoscaling + ) { + throw new Error('Cannot supply both \'day\' and \'weekDay\', use at most one'); + } + + const minute = fallback(options.minute, '*'); + const hour = fallback(options.hour, '*'); + const month = fallback(options.month, '*'); + const year = fallback(options.year, '*'); + + // Weekday defaults to '?' if not supplied. If it is supplied, day must become '?' + const day = fallback(options.day, options.weekDay !== undefined ? '?' : '*'); + const weekDay = fallback(options.weekDay, '?'); + + return new class extends Schedule { + public readonly expressionString = `cron(${minute} ${hour} ${day} ${month} ${weekDay} ${year})`; + public readonly timeZone = options.timeZone ?? DEFAULT_TIMEZONE; + public _bind(scope: Construct) { + if (!options.minute) { + Annotations.of(scope).addWarningV2(`@aws-cdk/${module ?? 'core'}:scheduleDefaultRunsEveryMinute`, 'cron: If you don\'t pass \'minute\', by default the event runs every minute. Pass \'minute: \'*\'\' if that\'s what you intend, or \'minute: 0\' to run once per hour instead.'); + } + return new LiteralSchedule(this.expressionString, this.timeZone); + } + }; + } + + /** + * Retrieve the expression for this schedule. + */ + public abstract readonly expressionString: string; + + /** + * The timezone of the expression, if applicable. + */ + public readonly timeZone?: TimeZone = undefined; + + protected constructor() {} + + /** + * @internal + */ + public abstract _bind(scope: Construct): void; +} + +/** + * Options to configure a cron expression + * + * All fields are strings so you can use complex expressions. Absence of + * a field implies '*' or '?', whichever one is appropriate. + * + * @see https://docs.aws.amazon.com/eventbridge/latest/userguide/scheduled-events.html#cron-expressions + */ +export interface CronOptions { + /** + * The minute to run this rule at + * + * @default - Every minute + */ + readonly minute?: string; + + /** + * The hour to run this rule at + * + * @default - Every hour + */ + readonly hour?: string; + + /** + * The day of the month to run this rule at + * + * @default - Every day of the month + */ + readonly day?: string; + + /** + * The month to run this rule at + * + * @default - Every month + */ + readonly month?: string; + + /** + * The year to run this rule at + * + * @default - Every year + */ + readonly year?: string; + + /** + * The day of the week to run this rule at + * + * @default - Any day of the week + */ + readonly weekDay?: string; + + /** + * Retrieve the expression for this schedule + * + * @default TimeZone.ETC_UTC + */ + readonly timeZone?: TimeZone; +} + +const DEFAULT_TIMEZONE = TimeZone.ETC_UTC; + +class LiteralSchedule extends Schedule { + constructor( + public readonly expressionString: string, + public readonly timeZone?: TimeZone, + ) { + super(); + } + + public _bind() {} +} + +function fallback(x: T | undefined, def: T): T { + return x ?? def; +} + +/** + * Return the rate if the rate is whole number + */ +function maybeRate(interval: number, singular: string) { + if (interval === 0 || !Number.isInteger(interval)) { return undefined; } + return makeRate(interval, singular); +} + +/** + * Return 'rate(${interval} ${singular}(s))` for the interval + */ +function makeRate(interval: number, singular: string) { + return interval === 1 ? `rate(1 ${singular})` : `rate(${interval} ${singular}s)`; +} diff --git a/packages/aws-cdk-lib/core/test/schedule.test.ts b/packages/aws-cdk-lib/core/test/schedule.test.ts new file mode 100644 index 0000000000000..34723010a9592 --- /dev/null +++ b/packages/aws-cdk-lib/core/test/schedule.test.ts @@ -0,0 +1,84 @@ +import { Annotations } from '../../assertions'; +import { Duration } from '../lib/duration'; +import { CronOptions, Schedule } from '../lib/schedule'; +import { Stack } from '../lib/stack'; +import { TimeZone } from '../lib/time-zone'; + +/** + * Basic example of extending core.Schedule for testing purposes. + */ +abstract class TestSchedule extends Schedule { + public static at(date: Date, timeZone?: TimeZone): Schedule { + return Schedule.protectedAt(date, timeZone); + } + + public static rate(duration: Duration): Schedule { + return Schedule.protectedRate(duration); + } + + public static expression(expression: string, timeZone?: TimeZone): Schedule { + return Schedule.protectedExpression(expression, timeZone); + } + + public static cron(options: CronOptions): Schedule { + return Schedule.protectedCron(options); + } +} + +describe('schedules', () => { + test('at - with default timezone', () => { + const schedule = TestSchedule.at(new Date(Date.UTC(1969, 10, 20, 0, 0, 0))); + expectSchedule(schedule, 'at(1969-11-20T00:00:00)', TimeZone.ETC_UTC); + }); + + test('at - with timezone', () => { + const schedule = TestSchedule.at(new Date(Date.UTC(1969, 10, 20, 0, 0, 0)), TimeZone.ASIA_TOKYO ); + expectSchedule(schedule, 'at(1969-11-20T00:00:00)', TimeZone.ASIA_TOKYO); + }); + + test('rate', () => { + const schedule = TestSchedule.rate(Duration.days(1)); + expectSchedule(schedule, 'rate(1 day)', undefined); + }); + + test('expression - with default timezone', () => { + const schedule = TestSchedule.expression('at(1969-11-20T00:00:00)'); + expectSchedule(schedule, 'at(1969-11-20T00:00:00)', TimeZone.ETC_UTC); + }); + + test('expression - with timezone', () => { + const schedule = TestSchedule.expression('at(1969-11-20T00:00:00)', TimeZone.ASIA_SEOUL); + expectSchedule(schedule, 'at(1969-11-20T00:00:00)', TimeZone.ASIA_SEOUL); + }); + + test('cron - with default timezone', () => { + const schedule = TestSchedule.cron({ + minute: '0/10', + weekDay: 'MON-FRI', + }); + expectSchedule(schedule, 'cron(0/10 * ? * MON-FRI *)', TimeZone.ETC_UTC); + }); + + test('cron - with timezone', () => { + const schedule = TestSchedule.cron({ + minute: '0/10', + weekDay: 'MON-FRI', + timeZone: TimeZone.ANTARCTICA_TROLL, + }); + expectSchedule(schedule, 'cron(0/10 * ? * MON-FRI *)', TimeZone.ANTARCTICA_TROLL); + }); + + test('cron warning when minute not supplied', () => { + const stack = new Stack(); + const schedule = TestSchedule.cron({ + weekDay: 'MON-FRI', + }); + schedule._bind(stack); + Annotations.fromStack(stack).hasWarning('/Default', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead. [ack: @aws-cdk/core:scheduleDefaultRunsEveryMinute]"); + }); +}); + +function expectSchedule(schedule: Schedule, expectedExpression: string, expectedTimeZone?: TimeZone) { + expect(schedule.expressionString).toEqual(expectedExpression); + expect(schedule.timeZone).toEqual(expectedTimeZone); +} diff --git a/packages/aws-cdk-lib/rosetta/aws_backup/default.ts-fixture b/packages/aws-cdk-lib/rosetta/aws_backup/default.ts-fixture index 997a397e1ad70..012bd4f3fdbbd 100644 --- a/packages/aws-cdk-lib/rosetta/aws_backup/default.ts-fixture +++ b/packages/aws-cdk-lib/rosetta/aws_backup/default.ts-fixture @@ -4,7 +4,6 @@ import { Construct } from 'constructs'; import * as backup from 'aws-cdk-lib/aws-backup'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; -import * as events from 'aws-cdk-lib/aws-events'; import * as kms from 'aws-cdk-lib/aws-kms'; import * as sns from 'aws-cdk-lib/aws-sns'; import * as rds from 'aws-cdk-lib/aws-rds';