From 39cbd46f5d25d2304415aa2f0b5034dca0f260d8 Mon Sep 17 00:00:00 2001 From: Filipp Fediakov Date: Sun, 27 Aug 2023 18:23:54 +0100 Subject: [PATCH] feat(scheduler): base target methods and lambda invoke target (#26575) This PR contains implementation for Schedule Targets: 1. Creates a separate module for targets 2. Support imported resources, but not cross account, cross region resources as we discussed in RFC. The unit tests should cover 4 cases (target and role within the same stack, target is imported, role is imported, target and role are imported), 3. I have moved out class `Schedule` from private package to depend on it in `schedule-targets` unit tests. Implementation is based on RFC: https://github.com/aws/aws-cdk-rfcs/blob/master/text/0474-event-bridge-scheduler-l2.md Advances https://github.com/aws/aws-cdk/issues/23394 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-scheduler-alpha/README.md | 104 +++- .../aws-scheduler-alpha/jest.config.js | 2 +- .../@aws-cdk/aws-scheduler-alpha/lib/index.ts | 3 +- .../aws-scheduler-alpha/lib/private/index.ts | 2 - .../lib/private/schedule.ts | 70 --- .../lib/private/targets.ts | 58 -- .../aws-scheduler-alpha/lib/schedule.ts | 116 +++- .../aws-scheduler-alpha/lib/target.ts | 69 +++ .../@aws-cdk/aws-scheduler-alpha/package.json | 7 +- .../rosetta/default.ts-fixture | 3 +- .../aws-scheduler-alpha/test/group.test.ts | 71 +-- .../aws-scheduler-alpha/test/input.test.ts | 88 +-- .../aws-scheduler-targets-alpha/.eslintrc.js | 6 + .../aws-scheduler-targets-alpha/.gitignore | 24 + .../aws-scheduler-targets-alpha/.npmignore | 39 ++ .../aws-scheduler-targets-alpha/LICENSE | 201 +++++++ .../aws-scheduler-targets-alpha/NOTICE | 2 + .../aws-scheduler-targets-alpha/README.md | 63 +++ .../jest.config.js | 2 + .../aws-scheduler-targets-alpha/lib/index.ts | 2 + .../lib/lambda-invoke.ts | 31 ++ .../aws-scheduler-targets-alpha/lib/target.ts | 187 +++++++ .../aws-scheduler-targets-alpha/lib/util.ts | 11 + .../aws-scheduler-targets-alpha/package.json | 121 ++++ .../rosetta/default.ts-fixture | 17 + .../test/target.test.ts | 515 ++++++++++++++++++ .../tsconfig.dev.json | 38 ++ yarn.lock | 2 +- 28 files changed, 1607 insertions(+), 247 deletions(-) delete mode 100644 packages/@aws-cdk/aws-scheduler-alpha/lib/private/index.ts delete mode 100644 packages/@aws-cdk/aws-scheduler-alpha/lib/private/schedule.ts delete mode 100644 packages/@aws-cdk/aws-scheduler-alpha/lib/private/targets.ts create mode 100644 packages/@aws-cdk/aws-scheduler-alpha/lib/target.ts create mode 100644 packages/@aws-cdk/aws-scheduler-targets-alpha/.eslintrc.js create mode 100644 packages/@aws-cdk/aws-scheduler-targets-alpha/.gitignore create mode 100644 packages/@aws-cdk/aws-scheduler-targets-alpha/.npmignore create mode 100644 packages/@aws-cdk/aws-scheduler-targets-alpha/LICENSE create mode 100644 packages/@aws-cdk/aws-scheduler-targets-alpha/NOTICE create mode 100644 packages/@aws-cdk/aws-scheduler-targets-alpha/README.md create mode 100644 packages/@aws-cdk/aws-scheduler-targets-alpha/jest.config.js create mode 100644 packages/@aws-cdk/aws-scheduler-targets-alpha/lib/index.ts create mode 100644 packages/@aws-cdk/aws-scheduler-targets-alpha/lib/lambda-invoke.ts create mode 100644 packages/@aws-cdk/aws-scheduler-targets-alpha/lib/target.ts create mode 100644 packages/@aws-cdk/aws-scheduler-targets-alpha/lib/util.ts create mode 100644 packages/@aws-cdk/aws-scheduler-targets-alpha/package.json create mode 100644 packages/@aws-cdk/aws-scheduler-targets-alpha/rosetta/default.ts-fixture create mode 100644 packages/@aws-cdk/aws-scheduler-targets-alpha/test/target.test.ts create mode 100644 packages/@aws-cdk/aws-scheduler-targets-alpha/tsconfig.dev.json diff --git a/packages/@aws-cdk/aws-scheduler-alpha/README.md b/packages/@aws-cdk/aws-scheduler-alpha/README.md index 0ca6cddd19a7f..5f5ef0d41a997 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/README.md +++ b/packages/@aws-cdk/aws-scheduler-alpha/README.md @@ -37,16 +37,21 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aw ## Defining a schedule -TODO: Schedule is not yet fully 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) - -[comment]: <> (TODO: change for each PR that implements more functionality) +```ts +declare const fn: lambda.Function; -Only an L2 class is created that wraps the L1 class and handles the following properties: +const target = new targets.LambdaInvoke(fn, { + input: ScheduleTargetInput.fromObject({ + "payload": "useful", + }), +}); -- schedule -- schedule group -- target (only LambdaInvoke is supported for now) -- flexibleTimeWindow will be set to `{ mode: 'OFF' }` +const schedule = new Schedule(this, 'Schedule', { + schedule: ScheduleExpression.rate(Duration.minutes(10)), + target, + description: 'This is a test schedule that invokes lambda function every 10 minutes.', +}); +``` ### Schedule Expressions @@ -60,15 +65,17 @@ cron-based schedule you can specify a time zone in which EventBridge Scheduler e [comment]: <> (TODO: Switch to `ts` once Schedule is implemented) -```text +```ts +declare const target: targets.LambdaInvoke; + const rateBasedSchedule = new Schedule(this, 'Schedule', { - scheduleExpression: ScheduleExpression.rate(Duration.minutes(10)), + schedule: ScheduleExpression.rate(Duration.minutes(10)), target, description: 'This is a test rate-based schedule', }); const cronBasedSchedule = new Schedule(this, 'Schedule', { - scheduleExpression: ScheduleExpression.cron({ + schedule: ScheduleExpression.cron({ minute: '0', hour: '23', day: '20', @@ -85,9 +92,11 @@ and time zone in which EventBridge Scheduler evaluates the schedule. [comment]: <> (TODO: Switch to `ts` once Schedule is implemented) -```text +```ts +declare const target: targets.LambdaInvoke; + const oneTimeSchedule = new Schedule(this, 'Schedule', { - scheduleExpression: ScheduleExpression.at( + schedule: ScheduleExpression.at( new Date(2022, 10, 20, 19, 20, 23), TimeZone.AMERICA_NEW_YORK, ), @@ -100,25 +109,21 @@ const oneTimeSchedule = new Schedule(this, 'Schedule', { Your AWS account comes with a default scheduler group. You can access default group in CDK with: -```text +```ts const defaultGroup = Group.fromDefaultGroup(this, "DefaultGroup"); ``` If not specified a schedule is added to the default group. However, you can also add the schedule to a custom scheduling group managed by you: -```text +```ts +declare const target: targets.LambdaInvoke; + const group = new Group(this, "Group", { groupName: "MyGroup", }); -const target = new targets.LambdaInvoke(props.func, { - input: ScheduleTargetInput.fromObject({ - "payload": "useful", - }), -}); - new Schedule(this, 'Schedule', { - scheduleExpression: ScheduleExpression.rate(Duration.minutes(10)), + schedule: ScheduleExpression.rate(Duration.minutes(10)), target, group, }); @@ -126,9 +131,10 @@ new Schedule(this, 'Schedule', { ## Scheduler Targets -TODO: Scheduler Targets Module is 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) - -Only LambdaInvoke target is added for now. +The `@aws-cdk/aws-schedule-targets-alpha` module includes classes that implement the `IScheduleTarget` interface for +various AWS services. EventBridge Scheduler supports two types of targets: templated targets invoke common API +operations across a core groups of services, and customizeable universal targets that you can use to call more +than 6,000 operations across over 270 services. A list of supported targets can be found at `@aws-cdk/aws-schedule-targets-alpha`. ### Input @@ -156,7 +162,28 @@ const input = ScheduleTargetInput.fromText(text); ### Specifying Execution Role -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) +An execution role is an IAM role that EventBridge Scheduler assumes in order to interact with other AWS services on your behalf. + +The classes for templated schedule targets automatically create an IAM role with all the minimum necessary +permissions to interact with the templated target. If you wish you may specify your own IAM role, then the templated targets +will grant minimal required permissions. For example: for invoking Lambda function target `LambdaInvoke` will grant +execution IAM role permission to `lambda:InvokeFunction`. + +```ts +declare const fn: lambda.Function; + +const role = new iam.Role(this, 'Role', { + assumedBy: new iam.ServicePrincipal('scheduler.amazonaws.com'), +}); + +const target = new targets.LambdaInvoke(fn, { + input: ScheduleTargetInput.fromObject({ + "payload": "useful" + }), + role, +}); +``` + ### Cross-account and cross-region targets @@ -168,7 +195,30 @@ TODO: Not yet implemented. See section in [L2 Event Bridge Scheduler RFC](https: ## Error-handling -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) +You can configure how your schedule handles failures, when EventBridge Scheduler is unable to deliver an event +successfully to a target, by using two primary mechanisms: a retry policy, and a dead-letter queue (DLQ). + +A retry policy determines the number of times EventBridge Scheduler must retry a failed event, and how long +to keep an unprocessed event. + +A DLQ is a standard Amazon SQS queue EventBridge Scheduler uses to deliver failed events to, after the retry +policy has been exhausted. You can use a DLQ to troubleshoot issues with your schedule or its downstream target. +If you've configured a retry policy for your schedule, EventBridge Scheduler delivers the dead-letter event after +exhausting the maximum number of retries you set in the retry policy. + +```ts +declare const fn: lambda.Function; + +const dlq = new sqs.Queue(this, "DLQ", { + queueName: 'MyDLQ', +}); + +const target = new targets.LambdaInvoke(fn, { + deadLetterQueue: dlq, + maxEventAge: Duration.minutes(1), + retryAttempts: 3 +}); +``` ## Overriding Target Properties diff --git a/packages/@aws-cdk/aws-scheduler-alpha/jest.config.js b/packages/@aws-cdk/aws-scheduler-alpha/jest.config.js index 7ea6abe8036b2..d052cbb29f05d 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/jest.config.js +++ b/packages/@aws-cdk/aws-scheduler-alpha/jest.config.js @@ -7,4 +7,4 @@ module.exports = { branches: 60, }, }, - };; +}; diff --git a/packages/@aws-cdk/aws-scheduler-alpha/lib/index.ts b/packages/@aws-cdk/aws-scheduler-alpha/lib/index.ts index f6b79a57257ae..a77866188e6c3 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/lib/index.ts +++ b/packages/@aws-cdk/aws-scheduler-alpha/lib/index.ts @@ -1,4 +1,5 @@ export * from './schedule-expression'; export * from './input'; export * from './schedule'; -export * from './group'; \ No newline at end of file +export * from './group'; +export * from './target'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-scheduler-alpha/lib/private/index.ts b/packages/@aws-cdk/aws-scheduler-alpha/lib/private/index.ts deleted file mode 100644 index acb4914fd0c93..0000000000000 --- a/packages/@aws-cdk/aws-scheduler-alpha/lib/private/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './schedule'; -export * from './targets'; diff --git a/packages/@aws-cdk/aws-scheduler-alpha/lib/private/schedule.ts b/packages/@aws-cdk/aws-scheduler-alpha/lib/private/schedule.ts deleted file mode 100644 index 3d8bf5f9e6672..0000000000000 --- a/packages/@aws-cdk/aws-scheduler-alpha/lib/private/schedule.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Resource } from 'aws-cdk-lib'; -import { CfnSchedule } from 'aws-cdk-lib/aws-scheduler'; -import { Construct } from 'constructs'; -import { IGroup } from '../group'; -import { ISchedule } from '../schedule'; -import { ScheduleExpression } from '../schedule-expression'; - -/** - * DISCLAIMER: WORK IN PROGRESS, INTERFACE MIGHT CHANGE - * - * This unit is not yet finished. Only rudimentary Schedule is implemented in order - * to be able to create some sensible unit tests - */ - -export interface IScheduleTarget { - bind(_schedule: ISchedule): CfnSchedule.TargetProperty; -} - -/** - * Construction properties for `Schedule`. - */ -export interface ScheduleProps { - /** - * The expression that defines when the schedule runs. Can be either a `at`, `rate` - * or `cron` expression. - */ - readonly schedule: ScheduleExpression; - - /** - * The schedule's target details. - */ - readonly target: IScheduleTarget; - - /** - * The description you specify for the schedule. - * - * @default - no value - */ - readonly description?: string; - - /** - * The schedule's group. - * - * @deafult - By default a schedule will be associated with the `default` group. - */ - readonly group?: IGroup; -} - -/** - * An EventBridge Schedule - */ -export class Schedule extends Resource implements ISchedule { - public readonly group?: IGroup; - - constructor(scope: Construct, id: string, props: ScheduleProps) { - super(scope, id); - - this.group = props.group; - - new CfnSchedule(this, 'Resource', { - flexibleTimeWindow: { mode: 'OFF' }, - scheduleExpression: props.schedule.expressionString, - scheduleExpressionTimezone: props.schedule.timeZone?.timezoneName, - groupName: this.group?.groupName, - target: { - ...props.target.bind(this), - }, - }); - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-scheduler-alpha/lib/private/targets.ts b/packages/@aws-cdk/aws-scheduler-alpha/lib/private/targets.ts deleted file mode 100644 index 1edff1b13db57..0000000000000 --- a/packages/@aws-cdk/aws-scheduler-alpha/lib/private/targets.ts +++ /dev/null @@ -1,58 +0,0 @@ -import * as iam from 'aws-cdk-lib/aws-iam'; -import * as lambda from 'aws-cdk-lib/aws-lambda'; -import { CfnSchedule } from 'aws-cdk-lib/aws-scheduler'; -import { ScheduleTargetInput } from '../input'; -import { ISchedule } from '../schedule'; - -/** - * DISCLAIMER: WORK IN PROGRESS, INTERFACE MIGHT CHANGE - * - * This unit is not yet finished. The LambaInvoke target is only implemented to be able - * to create some sensible unit tests. - */ - -export namespace targets { - export interface ScheduleTargetBaseProps { - readonly role?: iam.IRole; - readonly input?: ScheduleTargetInput; - } - - abstract class ScheduleTargetBase { - constructor( - private readonly baseProps: ScheduleTargetBaseProps, - protected readonly targetArn: string, - ) { - } - - protected abstract addTargetActionToRole(role: iam.IRole): void; - - protected bindBaseTargetConfig(_schedule: ISchedule): CfnSchedule.TargetProperty { - if (typeof this.baseProps.role === undefined) { - throw Error('A role is needed (for now)'); - } - this.addTargetActionToRole(this.baseProps.role!); - return { - arn: this.targetArn, - roleArn: this.baseProps.role!.roleArn, - input: this.baseProps.input?.bind(_schedule), - }; - } - - bind(schedule: ISchedule): CfnSchedule.TargetProperty { - return this.bindBaseTargetConfig(schedule); - } - } - - export class LambdaInvoke extends ScheduleTargetBase { - constructor( - baseProps: ScheduleTargetBaseProps, - private readonly func: lambda.IFunction, - ) { - super(baseProps, func.functionArn); - } - - protected addTargetActionToRole(role: iam.IRole): void { - this.func.grantInvoke(role); - } - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule.ts b/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule.ts index 6f51b262353cf..343acc398324f 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule.ts +++ b/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule.ts @@ -1,6 +1,118 @@ -import { IResource } from 'aws-cdk-lib'; +import { IResource, Resource } from 'aws-cdk-lib'; +import { CfnSchedule } from 'aws-cdk-lib/aws-scheduler'; +import { Construct } from 'constructs'; +import { IGroup } from './group'; +import { ScheduleExpression } from './schedule-expression'; +import { IScheduleTarget } from './target'; /** * Interface representing a created or an imported `Schedule`. */ -export interface ISchedule extends IResource {} +export interface ISchedule extends IResource { + /** + * The name of the schedule. + */ + readonly scheduleName: string; + /** + * The schedule group associated with this schedule. + */ + readonly group?: IGroup; + /** + * The arn of the schedule. + */ + readonly scheduleArn: string; +} + +/** + * Construction properties for `Schedule`. + */ +export interface ScheduleProps { + /** + * The expression that defines when the schedule runs. Can be either a `at`, `rate` + * or `cron` expression. + */ + readonly schedule: ScheduleExpression; + + /** + * The schedule's target details. + */ + readonly target: IScheduleTarget; + + /** + * The name of the schedule. + * + * Up to 64 letters (uppercase and lowercase), numbers, hyphens, underscores and dots are allowed. + * + * @default - A unique name will be generated + */ + readonly scheduleName?: string; + + /** + * The description you specify for the schedule. + * + * @default - no value + */ + readonly description?: string; + + /** + * The schedule's group. + * + * @deafult - By default a schedule will be associated with the `default` group. + */ + readonly group?: IGroup; +} + +/** + * An EventBridge Schedule + */ +export class Schedule extends Resource implements ISchedule { + /** + * The schedule group associated with this schedule. + */ + public readonly group?: IGroup; + /** + * The arn of the schedule. + */ + public readonly scheduleArn: string; + /** + * The name of the schedule. + */ + public readonly scheduleName: string; + + constructor(scope: Construct, id: string, props: ScheduleProps) { + super(scope, id, { + physicalName: props.scheduleName, + }); + + this.group = props.group; + + const targetConfig = props.target.bind(this); + + const resource = new CfnSchedule(this, 'Resource', { + name: this.physicalName, + flexibleTimeWindow: { mode: 'OFF' }, + scheduleExpression: props.schedule.expressionString, + scheduleExpressionTimezone: props.schedule.timeZone?.timezoneName, + groupName: this.group?.groupName, + target: { + arn: targetConfig.arn, + roleArn: targetConfig.role.roleArn, + input: targetConfig.input?.bind(this), + deadLetterConfig: targetConfig.deadLetterConfig, + retryPolicy: targetConfig.retryPolicy, + ecsParameters: targetConfig.ecsParameters, + kinesisParameters: targetConfig.kinesisParameters, + eventBridgeParameters: targetConfig.eventBridgeParameters, + sageMakerPipelineParameters: targetConfig.sageMakerPipelineParameters, + sqsParameters: targetConfig.sqsParameters, + }, + }); + + this.scheduleName = this.getResourceNameAttribute(resource.ref); + this.scheduleArn = this.getResourceArnAttribute(resource.attrArn, { + service: 'scheduler', + resource: 'schedule', + resourceName: `${this.group?.groupName ?? 'default'}/${this.physicalName}`, + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-scheduler-alpha/lib/target.ts b/packages/@aws-cdk/aws-scheduler-alpha/lib/target.ts new file mode 100644 index 0000000000000..e15b635f7ae08 --- /dev/null +++ b/packages/@aws-cdk/aws-scheduler-alpha/lib/target.ts @@ -0,0 +1,69 @@ +import * as iam from 'aws-cdk-lib/aws-iam'; +import { CfnSchedule } from 'aws-cdk-lib/aws-scheduler'; +import { ScheduleTargetInput } from './input'; +import { ISchedule } from './schedule'; + +/** + * Interface representing a Event Bridge Schedule Target. + */ +export interface IScheduleTarget { + /** + * Returns the schedule target specification. + * + * @param _schedule a schedule the target should be added to. + */ + bind(_schedule: ISchedule): ScheduleTargetConfig; +} + +/** + * Config of a Schedule Target used during initalization of Schedule + */ +export interface ScheduleTargetConfig { + /** + * The Amazon Resource Name (ARN) of the target. + */ + readonly arn: string; + + /** + * Role to use to invoke this event target + */ + readonly role: iam.IRole; + + /** + * What input to pass to the tatget + */ + readonly input?: ScheduleTargetInput; + + /** + * A `RetryPolicy` object that includes information about the retry policy settings, including the maximum age of an event, and the maximum number of times EventBridge Scheduler will try to deliver the event to a target. + */ + readonly retryPolicy?: CfnSchedule.RetryPolicyProperty; + + /** + * An object that contains information about an Amazon SQS queue that EventBridge Scheduler uses as a dead-letter queue for your schedule. If specified, EventBridge Scheduler delivers failed events that could not be successfully delivered to a target to the queue.\ + */ + readonly deadLetterConfig?: CfnSchedule.DeadLetterConfigProperty + + /** + * The templated target type for the Amazon ECS RunTask API Operation. + */ + readonly ecsParameters?: CfnSchedule.EcsParametersProperty; + /** + * The templated target type for the EventBridge PutEvents API operation. + */ + readonly eventBridgeParameters?: CfnSchedule.EventBridgeParametersProperty; + + /** + * The templated target type for the Amazon Kinesis PutRecord API operation. + */ + readonly kinesisParameters?: CfnSchedule.KinesisParametersProperty; + + /** + * The templated target type for the Amazon SageMaker StartPipelineExecution API operation. + */ + readonly sageMakerPipelineParameters?: CfnSchedule.SageMakerPipelineParametersProperty; + /** + * The templated target type for the Amazon SQS SendMessage API Operation + */ + readonly sqsParameters?: CfnSchedule.SqsParametersProperty; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-scheduler-alpha/package.json b/packages/@aws-cdk/aws-scheduler-alpha/package.json index f8f727c5bae66..311e581b128ad 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/package.json +++ b/packages/@aws-cdk/aws-scheduler-alpha/package.json @@ -114,5 +114,10 @@ "naming/package-matches-directory", "assert/assert-dependency" ] + }, + "jsiiRosetta": { + "exampleDependencies": { + "@aws-cdk/aws-scheduler-targets-alpha": "*" + } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-scheduler-alpha/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-scheduler-alpha/rosetta/default.ts-fixture index 661565e353ad3..cbb0128852cb7 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/aws-scheduler-alpha/rosetta/default.ts-fixture @@ -6,8 +6,9 @@ import * as iam from 'aws-cdk-lib/aws-iam'; import * as kms from 'aws-cdk-lib/aws-kms'; import * as sqs from 'aws-cdk-lib/aws-sqs'; import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; +import * as targets from '@aws-cdk/aws-scheduler-targets-alpha'; import { App, Stack, TimeZone, Duration } from 'aws-cdk-lib'; -import { ScheduleExpression, ScheduleTargetInput, ContextAttribute, Group } from '@aws-cdk/aws-scheduler-alpha'; +import { ScheduleExpression, ScheduleTargetInput, ContextAttribute, Group, Schedule } from '@aws-cdk/aws-scheduler-alpha'; class Fixture extends cdk.Stack { constructor(scope: Construct, id: string) { diff --git a/packages/@aws-cdk/aws-scheduler-alpha/test/group.test.ts b/packages/@aws-cdk/aws-scheduler-alpha/test/group.test.ts index 28fa388a9c313..66d254da15ba2 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/test/group.test.ts +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/group.test.ts @@ -1,23 +1,39 @@ -import { Duration, RemovalPolicy, Stack } from 'aws-cdk-lib'; +import { App, Duration, RemovalPolicy, Stack } from 'aws-cdk-lib'; import { Match, Template } from 'aws-cdk-lib/assertions'; import * as cw from 'aws-cdk-lib/aws-cloudwatch'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import { CfnScheduleGroup } from 'aws-cdk-lib/aws-scheduler'; -import { ScheduleExpression, ScheduleTargetInput } from '../lib'; +import { IScheduleTarget, ScheduleExpression, ScheduleTargetConfig } from '../lib'; import { Group, GroupProps } from '../lib/group'; -import { Schedule, targets } from '../lib/private'; +import { Schedule } from '../lib/schedule'; + +class SomeLambdaTarget implements IScheduleTarget { + public constructor(private readonly fn: lambda.IFunction, private readonly role: iam.IRole) { + } + + public bind(): ScheduleTargetConfig { + return { + arn: this.fn.functionArn, + role: this.role, + }; + } +} describe('Schedule Group', () => { let stack: Stack; let func: lambda.IFunction; - let role: iam.IRole; const expr = ScheduleExpression.at(new Date(Date.UTC(1969, 10, 20, 0, 0, 0))); beforeEach(() => { - stack = new Stack(); - role = iam.Role.fromRoleArn(stack, 'Role', 'arn:aws:iam::123456789012:role/johndoe'); - func = lambda.Function.fromFunctionArn(stack, 'Function', 'arn:aws:lambda:us-east-1:123456789012:function/somefunc'); + const app = new App(); + stack = new Stack(app, 'Stack', { env: { region: 'us-east-1', account: '123456789012' } }); + func = new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_14_X, + tracing: lambda.Tracing.PASS_THROUGH, + }); }); test('creates a group with default properties', () => { @@ -97,22 +113,17 @@ describe('Schedule Group', () => { groupName: 'MyGroup', }; const group = new Group(stack, 'TestGroup', props); + const role = iam.Role.fromRoleArn(stack, 'ImportedRole', 'arn:aws:iam::123456789012:role/someRole'); const schedule1 = new Schedule(stack, 'MyScheduleDummy1', { schedule: expr, group: group, - target: new targets.LambdaInvoke({ - role, - input: ScheduleTargetInput.fromText('test'), - }, func), + target: new SomeLambdaTarget(func, role), }); const schedule2 = new Schedule(stack, 'MyScheduleDummy2', { schedule: expr, group: group, - target: new targets.LambdaInvoke({ - role, - input: ScheduleTargetInput.fromText('test'), - }, func), + target: new SomeLambdaTarget(func, role), }); expect(schedule1.group).toEqual(group); @@ -154,15 +165,7 @@ describe('Schedule Group', () => { { Ref: 'AWS::Partition', }, - ':scheduler:', - { - Ref: 'AWS::Region', - }, - ':', - { - Ref: 'AWS::AccountId', - }, - ':schedule/MyGroup/*', + ':scheduler:us-east-1:123456789012:schedule/MyGroup/*', ], ], }, @@ -203,15 +206,7 @@ describe('Schedule Group', () => { { Ref: 'AWS::Partition', }, - ':scheduler:', - { - Ref: 'AWS::Region', - }, - ':', - { - Ref: 'AWS::AccountId', - }, - ':schedule/MyGroup/*', + ':scheduler:us-east-1:123456789012:schedule/MyGroup/*', ], ], }, @@ -249,15 +244,7 @@ describe('Schedule Group', () => { { Ref: 'AWS::Partition', }, - ':scheduler:', - { - Ref: 'AWS::Region', - }, - ':', - { - Ref: 'AWS::AccountId', - }, - ':schedule/MyGroup/*', + ':scheduler:us-east-1:123456789012:schedule/MyGroup/*', ], ], }, 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 adb6042ba3879..3e3ffd5e9f9bb 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/test/input.test.ts +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/input.test.ts @@ -1,29 +1,44 @@ -import { Stack } from 'aws-cdk-lib'; +import { App, 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 { ContextAttribute, ScheduleExpression, ScheduleTargetInput } from '../lib'; -import { Schedule, targets } from '../lib/private'; +import { ContextAttribute, IScheduleTarget, ScheduleExpression, ScheduleTargetConfig, ScheduleTargetInput } from '../lib'; +import { Schedule } from '../lib/schedule'; + +class SomeLambdaTarget implements IScheduleTarget { + public constructor(private readonly fn: lambda.IFunction, private readonly input: ScheduleTargetInput) { + } + + public bind(): ScheduleTargetConfig { + return { + arn: this.fn.functionArn, + input: this.input, + role: iam.Role.fromRoleArn(this.fn, 'ImportedRole', 'arn:aws:iam::123456789012:role/someRole'), + }; + } +} describe('schedule target input', () => { let stack: Stack; - let role: iam.IRole; let func: lambda.IFunction; const expr = ScheduleExpression.at(new Date(Date.UTC(1969, 10, 20, 0, 0, 0))); beforeEach(() => { - stack = new Stack(); - role = iam.Role.fromRoleArn(stack, 'Role', 'arn:aws:iam::123456789012:role/johndoe'); - func = lambda.Function.fromFunctionArn(stack, 'Function', 'arn:aws:lambda:us-east-1:123456789012:function/somefunc'); + 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_14_X, + tracing: lambda.Tracing.PASS_THROUGH, + }); }); test('create an input from text', () => { new Schedule(stack, 'MyScheduleDummy', { schedule: expr, - target: new targets.LambdaInvoke({ - role, - input: ScheduleTargetInput.fromText('test'), - }, func), + target: new SomeLambdaTarget(func, ScheduleTargetInput.fromText('test')), }); Template.fromStack(stack).hasResource('AWS::Scheduler::Schedule', { Properties: { @@ -37,10 +52,7 @@ describe('schedule target input', () => { test('create an input from text with a ref inside', () => { new Schedule(stack, 'MyScheduleDummy', { schedule: expr, - target: new targets.LambdaInvoke({ - role, - input: ScheduleTargetInput.fromText(stack.account), - }, func), + target: new SomeLambdaTarget(func, ScheduleTargetInput.fromText(stack.account)), }); Template.fromStack(stack).hasResource('AWS::Scheduler::Schedule', { Properties: { @@ -54,14 +66,12 @@ describe('schedule target input', () => { }); test('create an input from object', () => { + const input = ScheduleTargetInput.fromObject({ + test: 'test', + }); new Schedule(stack, 'MyScheduleDummy', { schedule: expr, - target: new targets.LambdaInvoke({ - role, - input: ScheduleTargetInput.fromObject({ - test: 'test', - }), - }, func), + target: new SomeLambdaTarget(func, input), }); Template.fromStack(stack).hasResource('AWS::Scheduler::Schedule', { Properties: { @@ -73,14 +83,12 @@ describe('schedule target input', () => { }); test('create an input from object with a ref', () => { + const input = ScheduleTargetInput.fromObject({ + test: stack.account, + }); new Schedule(stack, 'MyScheduleDummy', { schedule: expr, - target: new targets.LambdaInvoke({ - role, - input: ScheduleTargetInput.fromObject({ - test: stack.account, - }), - }, func), + target: new SomeLambdaTarget(func, input), }); Template.fromStack(stack).hasResource('AWS::Scheduler::Schedule', { Properties: { @@ -98,12 +106,11 @@ describe('schedule target input', () => { }); test('create an input with fromText with ContextAttribute', () => { + const input = ScheduleTargetInput.fromText(`Test=${ContextAttribute.scheduleArn}`); + new Schedule(stack, 'MyScheduleDummy', { schedule: expr, - target: new targets.LambdaInvoke({ - role, - input: ScheduleTargetInput.fromText(`Test=${ContextAttribute.scheduleArn}`), - }, func), + target: new SomeLambdaTarget(func, input), }); Template.fromStack(stack).hasResource('AWS::Scheduler::Schedule', { Properties: { @@ -115,18 +122,17 @@ describe('schedule target input', () => { }); test('create an input with fromObject with ContextAttribute', () => { + const input = ScheduleTargetInput.fromObject({ + arn: ContextAttribute.scheduleArn, + att: ContextAttribute.attemptNumber, + xid: ContextAttribute.executionId, + tim: ContextAttribute.scheduledTime, + cus: ContextAttribute.fromName('escapehatch'), + }); + new Schedule(stack, 'MyScheduleDummy', { schedule: expr, - target: new targets.LambdaInvoke({ - role, - input: ScheduleTargetInput.fromObject({ - arn: ContextAttribute.scheduleArn, - att: ContextAttribute.attemptNumber, - xid: ContextAttribute.executionId, - tim: ContextAttribute.scheduledTime, - cus: ContextAttribute.fromName('escapehatch'), - }), - }, func), + target: new SomeLambdaTarget(func, input), }); Template.fromStack(stack).hasResource('AWS::Scheduler::Schedule', { Properties: { diff --git a/packages/@aws-cdk/aws-scheduler-targets-alpha/.eslintrc.js b/packages/@aws-cdk/aws-scheduler-targets-alpha/.eslintrc.js new file mode 100644 index 0000000000000..2a2c7498774d0 --- /dev/null +++ b/packages/@aws-cdk/aws-scheduler-targets-alpha/.eslintrc.js @@ -0,0 +1,6 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; + +baseConfig.rules['import/no-extraneous-dependencies'] = ['error', { devDependencies: true, peerDependencies: true }]; + +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-scheduler-targets-alpha/.gitignore b/packages/@aws-cdk/aws-scheduler-targets-alpha/.gitignore new file mode 100644 index 0000000000000..4b4c1e6d4716a --- /dev/null +++ b/packages/@aws-cdk/aws-scheduler-targets-alpha/.gitignore @@ -0,0 +1,24 @@ +*.d.ts +*.generated.ts +*.js +*.js.map +*.snk +.jsii +.jsii.gz +.LAST_BUILD +.LAST_PACKAGE +nyc.config.js +.nyc_output +.vscode +.types-compat +coverage +dist +tsconfig.json +!.eslintrc.js +!jest.config.js + +junit.xml +!**/*.snapshot/**/asset.*/*.js +!**/*.snapshot/**/asset.*/*.d.ts + +!**/*.snapshot/**/asset.*/** diff --git a/packages/@aws-cdk/aws-scheduler-targets-alpha/.npmignore b/packages/@aws-cdk/aws-scheduler-targets-alpha/.npmignore new file mode 100644 index 0000000000000..39a19f1bc4c14 --- /dev/null +++ b/packages/@aws-cdk/aws-scheduler-targets-alpha/.npmignore @@ -0,0 +1,39 @@ +# The basics +*.ts +*.tgz +*.snk +!*.d.ts +!*.js +test/ +**/test/** + +# Coverage +coverage +.nyc_output +.nycrc + +# Build gear +build-tools +dist +scripts +.LAST_BUILD +.LAST_PACKAGE + +tsconfig.json +*.tsbuildinfo + +!.jsii +!.jsii.gz +.eslintrc.js + +# exclude cdk artifacts +**/cdk.out +junit.xml + +!*.lit.ts + +# exclude source maps as they only work locally +*.map + +# Nested node_modules +aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/node_modules diff --git a/packages/@aws-cdk/aws-scheduler-targets-alpha/LICENSE b/packages/@aws-cdk/aws-scheduler-targets-alpha/LICENSE new file mode 100644 index 0000000000000..9b722c65c5481 --- /dev/null +++ b/packages/@aws-cdk/aws-scheduler-targets-alpha/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-scheduler-targets-alpha/NOTICE b/packages/@aws-cdk/aws-scheduler-targets-alpha/NOTICE new file mode 100644 index 0000000000000..a27b7dd317649 --- /dev/null +++ b/packages/@aws-cdk/aws-scheduler-targets-alpha/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-scheduler-targets-alpha/README.md b/packages/@aws-cdk/aws-scheduler-targets-alpha/README.md new file mode 100644 index 0000000000000..f2d5471949004 --- /dev/null +++ b/packages/@aws-cdk/aws-scheduler-targets-alpha/README.md @@ -0,0 +1,63 @@ +# Amazon EventBridge Scheduler Construct Library + + +--- + +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + +--- + + + +[Amazon EventBridge Scheduler](https://aws.amazon.com/blogs/compute/introducing-amazon-eventbridge-scheduler/) is a feature from Amazon EventBridge +that allows you to create, run, and manage scheduled tasks at scale. With EventBridge Scheduler, you can schedule one-time or recurrently tens +of millions of tasks across many AWS services without provisioning or managing underlying infrastructure. + +This library contains integration classes for Amazon EventBridge Scheduler to call any +number of supported AWS Services. + +The following targets are supported: + +1. `targets.LambdaInvoke`: [Invoke an AWS Lambda function](#invoke-a-lambda-function)) + +## Invoke a Lambda function + +Use the `LambdaInvoke` target to invoke a lambda function. + +The code snippet below creates an event rule with a Lambda function as a target +called every hour by Event Bridge Scheduler with custom payload. You can optionally attach a +[dead letter queue](https://docs.aws.amazon.com/eventbridge/latest/userguide/rule-dlq.html). + +```ts +import * as lambda from 'aws-cdk-lib/aws-lambda'; + +const fn = new lambda.Function(this, 'MyFunc', { + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + code: lambda.Code.fromInline(`exports.handler = handler.toString()`), +}); + +const dlq = new sqs.Queue(this, "DLQ", { + queueName: 'MyDLQ', +}); + +const target = new targets.LambdaInvoke(fn, { + deadLetterQueue: dlq, + maxEventAge: Duration.minutes(1), + retryAttempts: 3, + input: ScheduleTargetInput.fromObject({ + 'payload': 'useful' + }), +}); + +const schedule = new Schedule(this, 'Schedule', { + schedule: ScheduleExpression.rate(Duration.hours(1)), + target +}); +``` diff --git a/packages/@aws-cdk/aws-scheduler-targets-alpha/jest.config.js b/packages/@aws-cdk/aws-scheduler-targets-alpha/jest.config.js new file mode 100644 index 0000000000000..3a2fd93a1228a --- /dev/null +++ b/packages/@aws-cdk/aws-scheduler-targets-alpha/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-scheduler-targets-alpha/lib/index.ts b/packages/@aws-cdk/aws-scheduler-targets-alpha/lib/index.ts new file mode 100644 index 0000000000000..f64be0cb17943 --- /dev/null +++ b/packages/@aws-cdk/aws-scheduler-targets-alpha/lib/index.ts @@ -0,0 +1,2 @@ +export * from './target'; +export * from './lambda-invoke'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-scheduler-targets-alpha/lib/lambda-invoke.ts b/packages/@aws-cdk/aws-scheduler-targets-alpha/lib/lambda-invoke.ts new file mode 100644 index 0000000000000..352daf9a09745 --- /dev/null +++ b/packages/@aws-cdk/aws-scheduler-targets-alpha/lib/lambda-invoke.ts @@ -0,0 +1,31 @@ +import { ISchedule, IScheduleTarget } from '@aws-cdk/aws-scheduler-alpha'; +import { Names } from 'aws-cdk-lib'; +import { IRole } from 'aws-cdk-lib/aws-iam'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import { ScheduleTargetBase, ScheduleTargetBaseProps } from './target'; +import { sameEnvDimension } from './util'; + +export class LambdaInvoke extends ScheduleTargetBase implements IScheduleTarget { + constructor( + private readonly func: lambda.IFunction, + private readonly props: ScheduleTargetBaseProps, + ) { + super(props, func.functionArn); + } + + protected addTargetActionToRole(schedule: ISchedule, role: IRole): void { + if (!sameEnvDimension(this.func.env.region, schedule.env.region)) { + throw new Error(`Cannot assign function in region ${this.func.env.region} to the schedule ${Names.nodeUniqueId(schedule.node)} in region ${schedule.env.region}. Both the schedule and the function must be in the same region.`); + } + + if (!sameEnvDimension(this.func.env.account, schedule.env.account)) { + throw new Error(`Cannot assign function in account ${this.func.env.account} to the schedule ${Names.nodeUniqueId(schedule.node)} in account ${schedule.env.region}. Both the schedule and the function must be in the same account.`); + } + + if (this.props.role && !sameEnvDimension(this.props.role.env.account, this.func.env.account)) { + throw new Error(`Cannot grant permission to execution role in account ${this.props.role.env.account} to invoke target ${Names.nodeUniqueId(this.func.node)} in account ${this.func.env.account}. Both the target and the execution role must be in the same account.`); + } + + this.func.grantInvoke(role); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-scheduler-targets-alpha/lib/target.ts b/packages/@aws-cdk/aws-scheduler-targets-alpha/lib/target.ts new file mode 100644 index 0000000000000..868d055ae02d2 --- /dev/null +++ b/packages/@aws-cdk/aws-scheduler-targets-alpha/lib/target.ts @@ -0,0 +1,187 @@ +import { ISchedule, ScheduleTargetConfig, ScheduleTargetInput } from '@aws-cdk/aws-scheduler-alpha'; +import { Annotations, Duration, Names, PhysicalName, Token, Stack } from 'aws-cdk-lib'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import { CfnSchedule } from 'aws-cdk-lib/aws-scheduler'; +import * as sqs from 'aws-cdk-lib/aws-sqs'; +import { md5hash } from 'aws-cdk-lib/core/lib/helpers-internal'; +import { sameEnvDimension } from './util'; + +/** + * Base properties for a Schedule Target + */ +export interface ScheduleTargetBaseProps { + /** + * An execution role is an IAM role that EventBridge Scheduler assumes in order to interact with other AWS services on your behalf. + * + * If none provided templates target will automatically create an IAM role with all the minimum necessary + * permissions to interact with the templated target. If you wish you may specify your own IAM role, then the templated targets + * will grant minimal required permissions. + * + * Universal target automatically create an IAM role if you do not specify your own IAM role. + * However, in comparison with templated targets, for universal targets you must grant the required + * IAM permissions yourself. + * + * @default - created by target + */ + readonly role?: iam.IRole; + /** + * The SQS queue to be used as deadLetterQueue. + * + * The events not successfully delivered are automatically retried for a specified period of time, + * depending on the retry policy of the target. + * If an event is not delivered before all retry attempts are exhausted, it will be sent to the dead letter queue. + * + * @default - no dead-letter queue + */ + readonly deadLetterQueue?: sqs.IQueue; + + /** + * Input passed to the target. + * + * @default - no input. + */ + readonly input?: ScheduleTargetInput; + + /** + * The maximum age of a request that Scheduler sends to a target for processing. + * + * Minimum value of 60. + * Maximum value of 86400. + * + * @default Duration.hours(24) + */ + readonly maxEventAge?: Duration; + /** + * The maximum number of times to retry when the target returns an error. + * + * Minimum value of 0. + * Maximum value of 185. + * + * @default 185 + */ + readonly retryAttempts?: number; +} + +export abstract class ScheduleTargetBase { + + constructor( + private readonly baseProps: ScheduleTargetBaseProps, + protected readonly targetArn: string, + ) { + } + + protected abstract addTargetActionToRole(schedule: ISchedule, role: iam.IRole): void; + + protected bindBaseTargetConfig(_schedule: ISchedule): ScheduleTargetConfig { + const role: iam.IRole = this.baseProps.role ?? this.singletonScheduleRole(_schedule, this.targetArn); + this.addTargetActionToRole(_schedule, role); + + if (this.baseProps.deadLetterQueue) { + this.addToDeadLetterQueueResourcePolicy(_schedule, this.baseProps.deadLetterQueue); + } + + return { + arn: this.targetArn, + role: role, + deadLetterConfig: this.baseProps.deadLetterQueue ? { + arn: this.baseProps.deadLetterQueue.queueArn, + } : undefined, + retryPolicy: this.renderRetryPolicy(this.baseProps.maxEventAge, this.baseProps.retryAttempts), + input: this.baseProps.input, + }; + } + + bind(schedule: ISchedule): ScheduleTargetConfig { + return this.bindBaseTargetConfig(schedule); + } + + /** + * Obtain the Role for the EventBridge Scheduler event + * + * If a role already exists, it will be returned. This ensures that if multiple + * events have the same target, they will share a role. + */ + private singletonScheduleRole(schedule: ISchedule, targetArn: string): iam.IRole { + const stack = Stack.of(schedule); + const arn = Token.isUnresolved(targetArn) ? stack.resolve(targetArn).toString() : targetArn; + const hash = md5hash(arn).slice(0, 6); + const id = 'SchedulerRoleForTarget-' + hash; + const existingRole = stack.node.tryFindChild(id) as iam.Role; + + const principal = new iam.PrincipalWithConditions(new iam.ServicePrincipal('scheduler.amazonaws.com'), { + StringEquals: { + 'aws:SourceAccount': schedule.env.account, + }, + }); + if (existingRole) { + existingRole.assumeRolePolicy?.addStatements(new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + principals: [principal], + actions: ['sts:AssumeRole'], + })); + return existingRole; + } + const role = new iam.Role(stack, id, { + roleName: PhysicalName.GENERATE_IF_NEEDED, + assumedBy: principal, + }); + return role; + } + + /** + * Allow a schedule to send events with failed invocation to an Amazon SQS queue. + * @param schedule schedule to add DLQ to + * @param queue the DLQ + */ + private addToDeadLetterQueueResourcePolicy(schedule: ISchedule, queue: sqs.IQueue) { + if (!sameEnvDimension(schedule.env.region, queue.env.region)) { + throw new Error(`Cannot assign Dead Letter Queue in region ${queue.env.region} to the schedule ${Names.nodeUniqueId(schedule.node)} in region ${schedule.env.region}. Both the queue and the schedule must be in the same region.`); + } + + // Skip Resource Policy creation if the Queue is not in the same account. + // There is no way to add a target onto an imported schedule, so we can assume we will run the following code only + // in the account where the schedule is created. + if (sameEnvDimension(schedule.env.account, queue.env.account)) { + const policyStatementId = `AllowSchedule${Names.nodeUniqueId(schedule.node)}`; + + queue.addToResourcePolicy(new iam.PolicyStatement({ + sid: policyStatementId, + principals: [new iam.ServicePrincipal('scheduler.amazonaws.com')], + effect: iam.Effect.ALLOW, + actions: ['sqs:SendMessage'], + resources: [queue.queueArn], + })); + } else { + Annotations.of(schedule).addWarning(`Cannot add a resource policy to your dead letter queue associated with schedule ${schedule.scheduleName} because the queue is in a different account. You must add the resource policy manually to the dead letter queue in account ${queue.env.account}.`); + } + } + + private renderRetryPolicy(maximumEventAge: Duration | undefined, maximumRetryAttempts: number | undefined): CfnSchedule.RetryPolicyProperty { + const maxMaxAge = Duration.days(1).toSeconds(); + const minMaxAge = Duration.minutes(15).toSeconds(); + let maxAge: number = maxMaxAge; + if (maximumEventAge) { + maxAge = maximumEventAge.toSeconds({ integral: true }); + if (maxAge > maxMaxAge) { + throw new Error('Maximum event age is 1 day'); + } + if (maxAge < minMaxAge) { + throw new Error('Minimum event age is 15 minutes'); + } + }; + let maxAttempts = 185; + if (typeof maximumRetryAttempts != 'undefined') { + if (maximumRetryAttempts < 0) { + throw Error('Number of retry attempts should be greater or equal than 0'); + } + if (maximumRetryAttempts > 185) { + throw Error('Number of retry attempts should be less or equal than 185'); + } + maxAttempts = maximumRetryAttempts; + } + return { + maximumEventAgeInSeconds: maxAge, + maximumRetryAttempts: maxAttempts, + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-scheduler-targets-alpha/lib/util.ts b/packages/@aws-cdk/aws-scheduler-targets-alpha/lib/util.ts new file mode 100644 index 0000000000000..3ea723c29ef2c --- /dev/null +++ b/packages/@aws-cdk/aws-scheduler-targets-alpha/lib/util.ts @@ -0,0 +1,11 @@ +import { Token, TokenComparison } from 'aws-cdk-lib'; + +/** + * Whether two string probably contain the same environment dimension (region or account) + * + * Used to compare either accounts or regions, and also returns true if both + * are unresolved (in which case both are expted to be "current region" or "current account"). + */ +export function sameEnvDimension(dim1: string, dim2: string) { + return [TokenComparison.SAME, TokenComparison.BOTH_UNRESOLVED].includes(Token.compareStrings(dim1, dim2)); +} diff --git a/packages/@aws-cdk/aws-scheduler-targets-alpha/package.json b/packages/@aws-cdk/aws-scheduler-targets-alpha/package.json new file mode 100644 index 0000000000000..897d0bf798ecd --- /dev/null +++ b/packages/@aws-cdk/aws-scheduler-targets-alpha/package.json @@ -0,0 +1,121 @@ +{ + "name": "@aws-cdk/aws-scheduler-targets-alpha", + "version": "0.0.0", + "description": "The CDK Construct Library for Amazon Scheduler Targets", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.awscdk.services.scheduler.targets.alpha", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "scheduler-targets-alpha" + } + }, + "dotnet": { + "namespace": "Amazon.CDK.AWS.Scheduler.Targets.Alpha", + "packageId": "Amazon.CDK.AWS.Scheduler.Targets.Alpha", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/main/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-cdk.aws-scheduler-targets-alpha", + "module": "aws_cdk.aws_scheduler_targets_alpha", + "classifiers": [ + "Framework :: AWS CDK", + "Framework :: AWS CDK :: 2" + ] + }, + "go": { + "moduleName": "github.com/aws/aws-cdk-go", + "packageName": "awscdkschedulertargetsalpha" + } + }, + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-scheduler-targets-alpha" + }, + "homepage": "https://github.com/aws/aws-cdk", + "scripts": { + "build": "cdk-build", + "integ": "integ-runner", + "lint": "cdk-lint", + "package": "cdk-package", + "awslint": "cdk-awslint", + "pkglint": "pkglint -f", + "test": "cdk-test", + "watch": "cdk-watch", + "compat": "cdk-compat", + "build+test": "yarn build && yarn test", + "build+test+package": "yarn build+test && yarn package", + "rosetta:extract": "yarn --silent jsii-rosetta extract", + "build+extract": "yarn build && yarn rosetta:extract", + "build+test+extract": "yarn build+test && yarn rosetta:extract" + }, + "cdk-build": { + "env": { + "AWSLINT_BASE_CONSTRUCT": true + } + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "aws-scheduler", + "aws-scheduler-targets" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/cdk-build-tools": "0.0.0", + "@aws-cdk/integ-runner": "0.0.0", + "@aws-cdk/pkglint": "0.0.0", + "@types/jest": "^29.5.3", + "aws-cdk-lib": "0.0.0", + "@aws-cdk/aws-scheduler-alpha": "0.0.0", + "constructs": "^10.0.0" + }, + "dependencies": {}, + "peerDependencies": { + "aws-cdk-lib": "^0.0.0", + "@aws-cdk/aws-scheduler-alpha": "0.0.0", + "constructs": "^10.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "stability": "experimental", + "maturity": "experimental", + "awscdkio": { + "announce": false + }, + "publishConfig": { + "tag": "latest" + }, + "awslint": { + "exclude": [ + "*:*" + ] + }, + "pkglint": { + "exclude": [ + "naming/package-matches-directory", + "assert/assert-dependency" + ] + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-scheduler-targets-alpha/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-scheduler-targets-alpha/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..1285eef51d196 --- /dev/null +++ b/packages/@aws-cdk/aws-scheduler-targets-alpha/rosetta/default.ts-fixture @@ -0,0 +1,17 @@ +// Fixture with packages imported, but nothing else +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as kms from 'aws-cdk-lib/aws-kms'; +import * as sqs from 'aws-cdk-lib/aws-sqs'; +import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; +import * as targets from '@aws-cdk/aws-scheduler-targets-alpha'; +import { App, Stack, TimeZone, Duration } from 'aws-cdk-lib'; +import { ScheduleExpression, ScheduleTargetInput, ContextAttribute, Group, Schedule } from '@aws-cdk/aws-scheduler-alpha'; + +class Fixture extends cdk.Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} diff --git a/packages/@aws-cdk/aws-scheduler-targets-alpha/test/target.test.ts b/packages/@aws-cdk/aws-scheduler-targets-alpha/test/target.test.ts new file mode 100644 index 0000000000000..faf0ec6aac2b4 --- /dev/null +++ b/packages/@aws-cdk/aws-scheduler-targets-alpha/test/target.test.ts @@ -0,0 +1,515 @@ +import { ScheduleExpression, Schedule } from '@aws-cdk/aws-scheduler-alpha'; +import { App, Duration, Stack } from 'aws-cdk-lib'; +import { Template } from 'aws-cdk-lib/assertions'; +import { AccountRootPrincipal, Role } from 'aws-cdk-lib/aws-iam'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as sqs from 'aws-cdk-lib/aws-sqs'; +import { LambdaInvoke } from '../lib/lambda-invoke'; + +describe('schedule target', () => { + let app: App; + let stack: Stack; + let func: lambda.IFunction; + const expr = ScheduleExpression.at(new Date(Date.UTC(1969, 10, 20, 0, 0, 0))); + + beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack', { env: { region: 'us-east-1', account: '123456789012' } }); + func = new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_14_X, + tracing: lambda.Tracing.PASS_THROUGH, + }); + }); + + test('creates IAM role and IAM policy for lambda target in the same account', () => { + const lambdaTarget = new LambdaInvoke(func, {}); + + new Schedule(stack, 'MyScheduleDummy', { + schedule: expr, + target: lambdaTarget, + }); + + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Permission', 0); + + Template.fromStack(stack).hasResource('AWS::Scheduler::Schedule', { + Properties: { + Target: { + Arn: { + 'Fn::GetAtt': ['MyLambdaCCE802FB', 'Arn'], + }, + RoleArn: { 'Fn::GetAtt': ['SchedulerRoleForTarget1441a743A31888', 'Arn'] }, + RetryPolicy: {}, + }, + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'lambda:InvokeFunction', + Effect: 'Allow', + Resource: [{ + 'Fn::GetAtt': ['MyLambdaCCE802FB', 'Arn'], + }, + { + 'Fn::Join': [ + '', [ + { 'Fn::GetAtt': ['MyLambdaCCE802FB', 'Arn'] }, + ':*', + ], + ], + }], + }, + ], + }, + Roles: [{ Ref: 'SchedulerRoleForTarget1441a743A31888' }], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Condition: { StringEquals: { 'aws:SourceAccount': '123456789012' } }, + Principal: { + Service: 'scheduler.amazonaws.com', + }, + Action: 'sts:AssumeRole', + }, + ], + }, + }); + }); + + test('creates IAM policy for provided IAM role', () => { + const targetExecutionRole = new Role(stack, 'ProvidedTargetRole', { + assumedBy: new AccountRootPrincipal(), + }); + + const lambdaTarget = new LambdaInvoke(func, { + role: targetExecutionRole, + }); + + new Schedule(stack, 'MyScheduleDummy', { + schedule: expr, + target: lambdaTarget, + }); + + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Permission', 0); + Template.fromStack(stack).hasResource('AWS::Scheduler::Schedule', { + Properties: { + Target: { + Arn: { + 'Fn::GetAtt': ['MyLambdaCCE802FB', 'Arn'], + }, + RoleArn: { 'Fn::GetAtt': ['ProvidedTargetRole8CFDD54A', 'Arn'] }, + RetryPolicy: {}, + }, + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'lambda:InvokeFunction', + Effect: 'Allow', + Resource: [{ + 'Fn::GetAtt': ['MyLambdaCCE802FB', 'Arn'], + }, + { + 'Fn::Join': [ + '', [ + { 'Fn::GetAtt': ['MyLambdaCCE802FB', 'Arn'] }, + ':*', + ], + ], + }], + }, + ], + }, + Roles: [{ Ref: 'ProvidedTargetRole8CFDD54A' }], + }); + }); + + test('reuses IAM role and IAM policy for two schedules from the same account', () => { + const lambdaTarget = new LambdaInvoke(func, { }); + + new Schedule(stack, 'MyScheduleDummy1', { + schedule: expr, + target: lambdaTarget, + }); + + new Schedule(stack, 'MyScheduleDummy2', { + schedule: expr, + target: lambdaTarget, + }); + + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Permission', 0); + Template.fromStack(stack).resourcePropertiesCountIs('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Condition: { StringEquals: { 'aws:SourceAccount': '123456789012' } }, + Principal: { + Service: 'scheduler.amazonaws.com', + }, + Action: 'sts:AssumeRole', + }, + ], + }, + }, 1); + + Template.fromStack(stack).resourcePropertiesCountIs('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'lambda:InvokeFunction', + Effect: 'Allow', + Resource: [{ + 'Fn::GetAtt': ['MyLambdaCCE802FB', 'Arn'], + }, + { + 'Fn::Join': [ + '', [ + { 'Fn::GetAtt': ['MyLambdaCCE802FB', 'Arn'] }, + ':*', + ], + ], + }], + }, + ], + }, + Roles: [{ Ref: 'SchedulerRoleForTarget1441a743A31888' }], + }, 1); + }); + + test('creates IAM policy for imported lambda function in the same account', () => { + const importedFunc = lambda.Function.fromFunctionArn(stack, 'ImportedFunction', 'arn:aws:lambda:us-east-1:123456789012:function/somefunc'); + + const lambdaTarget = new LambdaInvoke(importedFunc, {}); + + new Schedule(stack, 'MyScheduleDummy', { + schedule: expr, + target: lambdaTarget, + }); + + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Permission', 0); + + Template.fromStack(stack).hasResource('AWS::Scheduler::Schedule', { + Properties: { + Target: { + Arn: 'arn:aws:lambda:us-east-1:123456789012:function/somefunc', + RoleArn: { 'Fn::GetAtt': ['SchedulerRoleForTarget2ad129D7CAA2E6', 'Arn'] }, + RetryPolicy: {}, + }, + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'lambda:InvokeFunction', + Effect: 'Allow', + Resource: [ + 'arn:aws:lambda:us-east-1:123456789012:function/somefunc', + 'arn:aws:lambda:us-east-1:123456789012:function/somefunc:*', + ], + }, + ], + }, + Roles: [{ Ref: 'SchedulerRoleForTarget2ad129D7CAA2E6' }], + }); + }); + + test('creates IAM policy for imported role for lambda function in the same account', () => { + const importedRole = Role.fromRoleArn(stack, 'ImportedRole', 'arn:aws:iam::123456789012:role/someRole'); + + const lambdaTarget = new LambdaInvoke(func, { + role: importedRole, + }); + + new Schedule(stack, 'MyScheduleDummy', { + schedule: expr, + target: lambdaTarget, + }); + + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Permission', 0); + Template.fromStack(stack).hasResource('AWS::Scheduler::Schedule', { + Properties: { + Target: { + Arn: { + 'Fn::GetAtt': ['MyLambdaCCE802FB', 'Arn'], + }, + RoleArn: 'arn:aws:iam::123456789012:role/someRole', + RetryPolicy: {}, + }, + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'lambda:InvokeFunction', + Effect: 'Allow', + Resource: [{ + 'Fn::GetAtt': ['MyLambdaCCE802FB', 'Arn'], + }, + { + 'Fn::Join': [ + '', [ + { 'Fn::GetAtt': ['MyLambdaCCE802FB', 'Arn'] }, + ':*', + ], + ], + }], + }, + ], + }, + Roles: ['someRole'], + }); + }); + + test('creates IAM policy for imported lambda function with imported IAM role in the same account', () => { + const importedFunc = lambda.Function.fromFunctionArn(stack, 'ImportedFunction', 'arn:aws:lambda:us-east-1:123456789012:function/somefunc'); + const importedRole = Role.fromRoleArn(stack, 'ImportedRole', 'arn:aws:iam::123456789012:role/someRole'); + + const lambdaTarget = new LambdaInvoke(importedFunc, { + role: importedRole, + }); + + new Schedule(stack, 'MyScheduleDummy', { + schedule: expr, + target: lambdaTarget, + }); + + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Permission', 0); + Template.fromStack(stack).hasResource('AWS::Scheduler::Schedule', { + Properties: { + Target: { + Arn: 'arn:aws:lambda:us-east-1:123456789012:function/somefunc', + RoleArn: 'arn:aws:iam::123456789012:role/someRole', + RetryPolicy: {}, + }, + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'lambda:InvokeFunction', + Effect: 'Allow', + Resource: [ + 'arn:aws:lambda:us-east-1:123456789012:function/somefunc', + 'arn:aws:lambda:us-east-1:123456789012:function/somefunc:*', + ], + }, + ], + }, + Roles: ['someRole'], + }); + }); + + test('throws when lambda function is imported from different account', () => { + const importedFunc = lambda.Function.fromFunctionArn(stack, 'ImportedFunction', 'arn:aws:lambda:us-east-1:234567890123:function/somefunc'); + + const lambdaTarget = new LambdaInvoke(importedFunc, {}); + + expect(() => + new Schedule(stack, 'MyScheduleDummy', { + schedule: expr, + target: lambdaTarget, + })).toThrow(/Both the schedule and the function must be in the same account/); + }); + + test('throws when lambda function is imported from different region', () => { + const importedFunc = lambda.Function.fromFunctionArn(stack, 'ImportedFunction', 'arn:aws:lambda:us-west-2:123456789012:function/somefunc'); + + const lambdaTarget = new LambdaInvoke(importedFunc, {}); + + expect(() => + new Schedule(stack, 'MyScheduleDummy', { + schedule: expr, + target: lambdaTarget, + })).toThrow(/Both the schedule and the function must be in the same region/); + }); + + test('throws when IAM role is imported from different account', () => { + const importedRole = Role.fromRoleArn(stack, 'ImportedRole', 'arn:aws:iam::234567890123:role/someRole'); + + const lambdaTarget = new LambdaInvoke(func, { + role: importedRole, + }); + + expect(() => + new Schedule(stack, 'MyScheduleDummy', { + schedule: expr, + target: lambdaTarget, + })).toThrow(/Both the target and the execution role must be in the same account/); + }); + + test('adds permissions to DLQ', () => { + const dlq = new sqs.Queue(stack, 'DummyDeadLetterQueue'); + + const lambdaTarget = new LambdaInvoke(func, { + deadLetterQueue: dlq, + }); + + new Schedule(stack, 'MyScheduleDummy', { + schedule: expr, + target: lambdaTarget, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::SQS::QueuePolicy', { + PolicyDocument: { + Statement: [ + { + Action: 'sqs:SendMessage', + Principal: { + Service: 'scheduler.amazonaws.com', + }, + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['DummyDeadLetterQueueCEBF3463', 'Arn'], + }, + }, + ], + }, + Queues: [ + { + Ref: 'DummyDeadLetterQueueCEBF3463', + }, + ], + }); + }); + + test('throws when adding permissions to DLQ from a different region', () => { + const stack2 = new Stack(app, 'Stack2', { + env: { + region: 'eu-west-2', + }, + }); + const queue = new sqs.Queue(stack2, 'DummyDeadLetterQueue'); + + const lambdaTarget = new LambdaInvoke(func, { + deadLetterQueue: queue, + }); + + expect(() => + new Schedule(stack, 'MyScheduleDummy', { + schedule: expr, + target: lambdaTarget, + })).toThrow(/Both the queue and the schedule must be in the same region./); + }); + + test('does not create a queue policy when DLQ is imported', () => { + const importedQueue = sqs.Queue.fromQueueArn(stack, 'ImportedQueue', 'arn:aws:sqs:us-east-1:123456789012:queue1'); + + const lambdaTarget = new LambdaInvoke(func, { + deadLetterQueue: importedQueue, + }); + + new Schedule(stack, 'MyScheduleDummy', { + schedule: expr, + target: lambdaTarget, + }); + + Template.fromStack(stack).resourceCountIs('AWS::SQS::QueuePolicy', 0); + }); + + test('does not create a queue policy when DLQ is created in a different account', () => { + const stack2 = new Stack(app, 'Stack2', { + env: { + region: 'us-east-1', + account: '234567890123', + }, + }); + + const queue = new sqs.Queue(stack2, 'DummyDeadLetterQueue', { + queueName: 'DummyDeadLetterQueue', + }); + + const lambdaTarget = new LambdaInvoke(func, { + deadLetterQueue: queue, + }); + + new Schedule(stack, 'MyScheduleDummy', { + schedule: expr, + target: lambdaTarget, + }); + + Template.fromStack(stack).resourceCountIs('AWS::SQS::QueuePolicy', 0); + }); + + test('renders expected retry policy', () => { + const lambdaTarget = new LambdaInvoke(func, { + retryAttempts: 5, + maxEventAge: Duration.hours(3), + }); + + new Schedule(stack, 'MyScheduleDummy', { + schedule: expr, + target: lambdaTarget, + }); + + Template.fromStack(stack).hasResource('AWS::Scheduler::Schedule', { + Properties: { + Target: { + Arn: { + 'Fn::GetAtt': ['MyLambdaCCE802FB', 'Arn'], + }, + RoleArn: { 'Fn::GetAtt': ['SchedulerRoleForTarget1441a743A31888', 'Arn'] }, + RetryPolicy: { + MaximumEventAgeInSeconds: 10800, + MaximumRetryAttempts: 5, + }, + }, + }, + }); + }); + + test('throws when retry policy max age is more than 1 day', () => { + const lambdaTarget = new LambdaInvoke(func, { + maxEventAge: Duration.days(3), + }); + + expect(() => + new Schedule(stack, 'MyScheduleDummy', { + schedule: expr, + target: lambdaTarget, + })).toThrow(/Maximum event age is 1 day/); + }); + + test('throws when retry policy max age is less than 15 minutes', () => { + const lambdaTarget = new LambdaInvoke(func, { + maxEventAge: Duration.minutes(5), + }); + + expect(() => + new Schedule(stack, 'MyScheduleDummy', { + schedule: expr, + target: lambdaTarget, + })).toThrow(/Minimum event age is 15 minutes/); + }); + + test('throws when retry policy max retry attempts is out of the allowed limits', () => { + const lambdaTarget = new LambdaInvoke(func, { + retryAttempts: 200, + }); + + expect(() => + new Schedule(stack, 'MyScheduleDummy', { + schedule: expr, + target: lambdaTarget, + })).toThrow(/Number of retry attempts should be less or equal than 185/); + }); +}); diff --git a/packages/@aws-cdk/aws-scheduler-targets-alpha/tsconfig.dev.json b/packages/@aws-cdk/aws-scheduler-targets-alpha/tsconfig.dev.json new file mode 100644 index 0000000000000..4470bb29bf6da --- /dev/null +++ b/packages/@aws-cdk/aws-scheduler-targets-alpha/tsconfig.dev.json @@ -0,0 +1,38 @@ +{ + "$": "Config file for ts-node", + "ts-node": { + "preferTsExts": true + }, + "compilerOptions": { + "alwaysStrict": true, + "experimentalDecorators": true, + "incremental": true, + "lib": [ + "es2020" + ], + "module": "CommonJS", + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "stripInternal": false, + "target": "ES2020", + "composite": false, + "tsBuildInfoFile": "tsconfig.dev.tsbuildinfo" + }, + "include": [ + "**/*.ts" + ], + "exclude": [ + "node_modules", + ".types-compat", + "**/*.d.ts" + ] +} diff --git a/yarn.lock b/yarn.lock index 106d566f89926..700b636f5a97d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3868,7 +3868,7 @@ expect "^28.0.0" pretty-format "^28.0.0" -"@types/jest@^29.5.4": +"@types/jest@^29.5.3", "@types/jest@^29.5.4": version "29.5.4" resolved "https://registry.npmjs.org/@types/jest/-/jest-29.5.4.tgz#9d0a16edaa009a71e6a71a999acd582514dab566" integrity sha512-PhglGmhWeD46FYOVLt3X7TiWjzwuVGW9wG/4qocPevXMjCmrIc5b6db9WjeGE4QYVpUAWMDv3v0IiBwObY289A==