diff --git a/packages/@aws-cdk/aws-scheduler-alpha/README.md b/packages/@aws-cdk/aws-scheduler-alpha/README.md index 549a9f241530d..c8c78d21514b4 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/README.md +++ b/packages/@aws-cdk/aws-scheduler-alpha/README.md @@ -232,8 +232,22 @@ const target = new targets.LambdaInvoke(fn, { ## Overriding Target Properties -TODO: Not yet implemented. See section in [L2 Event Bridge Scheduler RFC](https://github.com/aws/aws-cdk-rfcs/blob/master/text/0474-event-bridge-scheduler-l2.md) +If you wish to reuse the same target in multiple schedules, you can override target properties like `input`, +`maximumRetryAttempts` and `maximumEventAge` when creating a Schedule using the `targetOverrides` parameter: + +```ts +declare const target: targets.LambdaInvoke; +const oneTimeSchedule = new Schedule(this, 'Schedule', { + schedule: ScheduleExpression.rate(cdk.Duration.hours(12)), + target, + targetOverrides: { + input: ScheduleTargetInput.fromText("Overriding Target Input"), + maximumEventAge: Duration.seconds(180), + maximumRetryAttempts: 5, + }, +}); +``` ## Monitoring diff --git a/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule.ts b/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule.ts index e6d65c8c0b2fb..4342bcf8aaa37 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule.ts +++ b/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule.ts @@ -1,7 +1,8 @@ -import { IResource, Resource } from 'aws-cdk-lib'; +import { Duration, IResource, Resource } from 'aws-cdk-lib'; import { CfnSchedule } from 'aws-cdk-lib/aws-scheduler'; import { Construct } from 'constructs'; import { IGroup } from './group'; +import { ScheduleTargetInput } from './input'; import { ScheduleExpression } from './schedule-expression'; import { IScheduleTarget } from './target'; @@ -23,6 +24,30 @@ export interface ISchedule extends IResource { readonly scheduleArn: string; } +export interface ScheduleTargetProps { + /** + * The text, or well-formed JSON, passed to the target. + * + * If you are configuring a templated Lambda, AWS Step Functions, or Amazon EventBridge target, + * the input must be a well-formed JSON. For all other target types, a JSON is not required. + * + * @default - The target's input is used. + */ + readonly input?: ScheduleTargetInput; + /** + * The maximum amount of time, in seconds, to continue to make retry attempts. + * + * @default - The target's maximumEventAgeInSeconds is used. + */ + readonly maximumEventAge?: Duration; + /** + * The maximum number of retry attempts to make before the request fails. + * + * @default - The target's maximumRetryAttempts is used. + */ + readonly maximumRetryAttempts?: number; +} + /** * Construction properties for `Schedule`. */ @@ -38,6 +63,11 @@ export interface ScheduleProps { */ readonly target: IScheduleTarget; + /** + * Allows to override target properties when creating a new schedule. + */ + readonly targetOverrides?: ScheduleTargetProps; + /** * The name of the schedule. * @@ -94,6 +124,13 @@ export class Schedule extends Resource implements ISchedule { const targetConfig = props.target.bind(this); + const retryPolicy = { + maximumEventAgeInSeconds: props.targetOverrides?.maximumEventAge?.toSeconds() ?? targetConfig.retryPolicy?.maximumEventAgeInSeconds, + maximumRetryAttempts: props.targetOverrides?.maximumRetryAttempts ?? targetConfig.retryPolicy?.maximumRetryAttempts, + }; + + this.validateRetryPolicy(retryPolicy.maximumEventAgeInSeconds, retryPolicy.maximumRetryAttempts); + const resource = new CfnSchedule(this, 'Resource', { name: this.physicalName, flexibleTimeWindow: { mode: 'OFF' }, @@ -104,9 +141,11 @@ export class Schedule extends Resource implements ISchedule { target: { arn: targetConfig.arn, roleArn: targetConfig.role.roleArn, - input: targetConfig.input?.bind(this), + input: props.targetOverrides?.input ? + props.targetOverrides?.input?.bind(this) : + targetConfig.input?.bind(this), deadLetterConfig: targetConfig.deadLetterConfig, - retryPolicy: targetConfig.retryPolicy, + retryPolicy: retryPolicy.maximumEventAgeInSeconds || retryPolicy.maximumRetryAttempts ? retryPolicy : undefined, ecsParameters: targetConfig.ecsParameters, kinesisParameters: targetConfig.kinesisParameters, eventBridgeParameters: targetConfig.eventBridgeParameters, @@ -122,4 +161,13 @@ export class Schedule extends Resource implements ISchedule { resourceName: `${this.group?.groupName ?? 'default'}/${this.physicalName}`, }); } + + private validateRetryPolicy(maximumEventAgeInSeconds: number | undefined, maximumRetryAttempts: number | undefined) { + if (maximumEventAgeInSeconds && (maximumEventAgeInSeconds < 60 || maximumEventAgeInSeconds > 900)) { + throw new Error(`maximumEventAgeInSeconds must be between 60 and 900, got ${maximumEventAgeInSeconds}`); + } + if (maximumRetryAttempts && (maximumRetryAttempts < 0 || maximumRetryAttempts > 185)) { + throw new Error(`maximumRetryAttempts must be between 0 and 185, got ${maximumRetryAttempts}`); + } + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-scheduler-alpha/test/input.test.ts b/packages/@aws-cdk/aws-scheduler-alpha/test/input.test.ts index afb890b7957ec..07aa43853e182 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/test/input.test.ts +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/input.test.ts @@ -146,4 +146,24 @@ describe('schedule target input', () => { }, }); }); + + test('can override target input', () => { + // WHEN + const input = ScheduleTargetInput.fromText('Original Input'); + new Schedule(stack, 'TestSchedule', { + schedule: expr, + target: new SomeLambdaTarget(func, input), + targetOverrides: { + input: ScheduleTargetInput.fromText('Overridden Input'), + }, + enabled: false, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Scheduler::Schedule', { + Target: { + Input: '"Overridden Input"', + }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/aws-cdk-scheduler-schedule.assets.json b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/aws-cdk-scheduler-schedule.assets.json index 2a26ac17ddebd..9c12eb7d9b585 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/aws-cdk-scheduler-schedule.assets.json +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/aws-cdk-scheduler-schedule.assets.json @@ -1,7 +1,7 @@ { - "version": "34.0.0", + "version": "33.0.0", "files": { - "8e518d2f0d7b4fcf41623a37d5a30fa0a49f5185d9301205cad6e72cc59b84a8": { + "70a4ff6207a6b7ce2e7a4354be513e0143bb5f5c671d6826cfb30c010875e4bd": { "source": { "path": "aws-cdk-scheduler-schedule.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "8e518d2f0d7b4fcf41623a37d5a30fa0a49f5185d9301205cad6e72cc59b84a8.json", + "objectKey": "70a4ff6207a6b7ce2e7a4354be513e0143bb5f5c671d6826cfb30c010875e4bd.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/aws-cdk-scheduler-schedule.template.json b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/aws-cdk-scheduler-schedule.template.json index 59e00cc6402db..468105f46d1c9 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/aws-cdk-scheduler-schedule.template.json +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/aws-cdk-scheduler-schedule.template.json @@ -83,6 +83,11 @@ "Arn" ] }, + "Input": "\"Input Text\"", + "RetryPolicy": { + "MaximumEventAgeInSeconds": 180, + "MaximumRetryAttempts": 3 + }, "RoleArn": { "Fn::GetAtt": [ "Role1ABCC5F0", @@ -108,6 +113,41 @@ "Arn" ] }, + "Input": "\"Input Text\"", + "RetryPolicy": { + "MaximumEventAgeInSeconds": 180, + "MaximumRetryAttempts": 3 + }, + "RoleArn": { + "Fn::GetAtt": [ + "Role1ABCC5F0", + "Arn" + ] + } + } + } + }, + "TargetOverrideScheduleFF8CB184": { + "Type": "AWS::Scheduler::Schedule", + "Properties": { + "FlexibleTimeWindow": { + "Mode": "OFF" + }, + "ScheduleExpression": "rate(12 hours)", + "ScheduleExpressionTimezone": "Etc/UTC", + "State": "ENABLED", + "Target": { + "Arn": { + "Fn::GetAtt": [ + "Function76856677", + "Arn" + ] + }, + "Input": "\"Changed Text\"", + "RetryPolicy": { + "MaximumEventAgeInSeconds": 360, + "MaximumRetryAttempts": 5 + }, "RoleArn": { "Fn::GetAtt": [ "Role1ABCC5F0", diff --git a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/cdk.out b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/cdk.out index 2313ab5436501..560dae10d018f 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/cdk.out +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"34.0.0"} \ No newline at end of file +{"version":"33.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/integ.json b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/integ.json index c1aec1a40f53f..8bd3af4b50e53 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/integ.json +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "34.0.0", + "version": "33.0.0", "testCases": { "integtest-schedule/DefaultTest": { "stacks": [ diff --git a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/integtestscheduleDefaultTestDeployAssert24CB3896.assets.json b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/integtestscheduleDefaultTestDeployAssert24CB3896.assets.json index 8f8a003c1b5ba..0ec5b6018b44a 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/integtestscheduleDefaultTestDeployAssert24CB3896.assets.json +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/integtestscheduleDefaultTestDeployAssert24CB3896.assets.json @@ -1,5 +1,5 @@ { - "version": "34.0.0", + "version": "33.0.0", "files": { "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { "source": { diff --git a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/manifest.json b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/manifest.json index 0bbde393e24c3..7abe8574bc711 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "34.0.0", + "version": "33.0.0", "artifacts": { "aws-cdk-scheduler-schedule.assets": { "type": "cdk:asset-manifest", @@ -18,7 +18,7 @@ "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}/8e518d2f0d7b4fcf41623a37d5a30fa0a49f5185d9301205cad6e72cc59b84a8.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/70a4ff6207a6b7ce2e7a4354be513e0143bb5f5c671d6826cfb30c010875e4bd.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -64,6 +64,12 @@ "data": "DisabledScheduleA1DF7F0F" } ], + "/aws-cdk-scheduler-schedule/TargetOverrideSchedule/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "TargetOverrideScheduleFF8CB184" + } + ], "/aws-cdk-scheduler-schedule/BootstrapVersion": [ { "type": "aws:cdk:logicalId", diff --git a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/tree.json b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/tree.json index db604f9e73761..d31f862d39c77 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/tree.json +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/tree.json @@ -170,6 +170,11 @@ "Role1ABCC5F0", "Arn" ] + }, + "input": "\"Input Text\"", + "retryPolicy": { + "maximumEventAgeInSeconds": 180, + "maximumRetryAttempts": 3 } } } @@ -213,6 +218,59 @@ "Role1ABCC5F0", "Arn" ] + }, + "input": "\"Input Text\"", + "retryPolicy": { + "maximumEventAgeInSeconds": 180, + "maximumRetryAttempts": 3 + } + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_scheduler.CfnSchedule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-scheduler-alpha.Schedule", + "version": "0.0.0" + } + }, + "TargetOverrideSchedule": { + "id": "TargetOverrideSchedule", + "path": "aws-cdk-scheduler-schedule/TargetOverrideSchedule", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-scheduler-schedule/TargetOverrideSchedule/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Scheduler::Schedule", + "aws:cdk:cloudformation:props": { + "flexibleTimeWindow": { + "mode": "OFF" + }, + "scheduleExpression": "rate(12 hours)", + "scheduleExpressionTimezone": "Etc/UTC", + "state": "ENABLED", + "target": { + "arn": { + "Fn::GetAtt": [ + "Function76856677", + "Arn" + ] + }, + "roleArn": { + "Fn::GetAtt": [ + "Role1ABCC5F0", + "Arn" + ] + }, + "input": "\"Changed Text\"", + "retryPolicy": { + "maximumEventAgeInSeconds": 360, + "maximumRetryAttempts": 5 } } } diff --git a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.ts b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.ts index a1907102a0ebf..4d0e003e66302 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.ts +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.ts @@ -1,3 +1,4 @@ +/// !cdk-integ aws-cdk-scheduler-schedule import { IntegTest } from '@aws-cdk/integ-tests-alpha'; import * as cdk from 'aws-cdk-lib'; import * as iam from 'aws-cdk-lib/aws-iam'; @@ -12,6 +13,11 @@ class SomeLambdaTarget implements scheduler.IScheduleTarget { return { arn: this.fn.functionArn, role: this.role, + input: scheduler.ScheduleTargetInput.fromText('Input Text'), + retryPolicy: { + maximumEventAgeInSeconds: 180, + maximumRetryAttempts: 3, + }, }; } } @@ -42,6 +48,18 @@ new scheduler.Schedule(stack, 'DisabledSchedule', { enabled: false, }); +new scheduler.Schedule(stack, 'TargetOverrideSchedule', { + schedule: expression, + target: target, + targetOverrides: { + input: scheduler.ScheduleTargetInput.fromText('Changed Text'), + maximumEventAge: cdk.Duration.seconds(360), + maximumRetryAttempts: 5, + }, +}); + new IntegTest(app, 'integtest-schedule', { testCases: [stack], -}); \ No newline at end of file +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-scheduler-alpha/test/retry-policy.test.ts b/packages/@aws-cdk/aws-scheduler-alpha/test/retry-policy.test.ts new file mode 100644 index 0000000000000..3b12588f4794e --- /dev/null +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/retry-policy.test.ts @@ -0,0 +1,138 @@ +import { App, Duration, Stack } from 'aws-cdk-lib'; + +import { Template } from 'aws-cdk-lib/assertions'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import { IScheduleTarget, ScheduleExpression, ScheduleTargetConfig } from '../lib'; +import { Schedule } from '../lib/schedule'; + +class SomeLambdaTarget implements IScheduleTarget { + public constructor(private readonly fn: lambda.IFunction) { + } + + public bind(): ScheduleTargetConfig { + return { + arn: this.fn.functionArn, + retryPolicy: { + maximumEventAgeInSeconds: 180, + maximumRetryAttempts: 10, + }, + role: iam.Role.fromRoleArn(this.fn, 'ImportedRole', 'arn:aws:iam::123456789012:role/someRole'), + }; + } +} + +describe('schedule target retry policy', () => { + let stack: Stack; + let func: lambda.IFunction; + const expr = ScheduleExpression.at(new Date(Date.UTC(1969, 10, 20, 0, 0, 0))); + + beforeEach(() => { + const app = new App(); + stack = new Stack(app); + func = new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_LATEST, + tracing: lambda.Tracing.PASS_THROUGH, + }); + }); + + test('create a schedule with retry policy', () => { + new Schedule(stack, 'MyScheduleDummy', { + schedule: expr, + target: new SomeLambdaTarget(func), + }); + Template.fromStack(stack).hasResource('AWS::Scheduler::Schedule', { + Properties: { + Target: { + RetryPolicy: { + MaximumEventAgeInSeconds: 180, + MaximumRetryAttempts: 10, + }, + }, + }, + }); + }); + + test('can override retry policy', () => { + // WHEN + new Schedule(stack, 'TestSchedule', { + schedule: expr, + target: new SomeLambdaTarget(func), + targetOverrides: { + maximumEventAge: Duration.seconds(120), + maximumRetryAttempts: 5, + }, + enabled: false, + }); + + // THEN + Template.fromStack(stack).hasResource('AWS::Scheduler::Schedule', { + Properties: { + Target: { + RetryPolicy: { + MaximumEventAgeInSeconds: 120, + MaximumRetryAttempts: 5, + }, + }, + }, + }); + }); + + test('apply maximumEventAge min value validation', () => { + expect(() => { + new Schedule(stack, 'TestSchedule', { + schedule: expr, + target: new SomeLambdaTarget(func), + targetOverrides: { + maximumEventAge: Duration.seconds(50), + maximumRetryAttempts: 5, + }, + enabled: false, + }); + }).toThrow(/maximumEventAgeInSeconds must be between 60 and 900, got 50/); + }); + + test('apply maximumEventAge max value validation', () => { + expect(() => { + new Schedule(stack, 'TestSchedule', { + schedule: expr, + target: new SomeLambdaTarget(func), + targetOverrides: { + maximumEventAge: Duration.seconds(1000), + maximumRetryAttempts: 5, + }, + enabled: false, + }); + }).toThrow(/maximumEventAgeInSeconds must be between 60 and 900, got 1000/); + }); + + test('apply maximumRetryAttempts min value validation', () => { + expect(() => { + new Schedule(stack, 'TestSchedule', { + schedule: expr, + target: new SomeLambdaTarget(func), + targetOverrides: { + maximumEventAge: Duration.seconds(120), + maximumRetryAttempts: -1, + }, + enabled: false, + }); + }).toThrow(/maximumRetryAttempts must be between 0 and 185, got -1/); + }); + + test('apply maximumRetryAttempts max value validation', () => { + expect(() => { + new Schedule(stack, 'TestSchedule', { + schedule: expr, + target: new SomeLambdaTarget(func), + targetOverrides: { + maximumEventAge: Duration.seconds(120), + maximumRetryAttempts: 200, + }, + enabled: false, + }); + }).toThrow(/maximumRetryAttempts must be between 0 and 185, got 200/); + }); +});