packages/aws-cdk-lib/aws-scheduler-targets/lib/target.ts (108 lines of code) (raw):
import * as iam from '../../aws-iam';
import { ISchedule, ScheduleTargetConfig, ScheduleTargetInput } from '../../aws-scheduler';
import { CfnSchedule } from '../../aws-scheduler/lib/scheduler.generated';
import * as sqs from '../../aws-sqs';
import { Duration, PhysicalName, Stack, Token } from '../../core';
import { md5hash } from '../../core/lib/helpers-internal';
/**
* 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.
*
* @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;
}
/**
* Base class for Schedule Targets
*/
export abstract class ScheduleTargetBase {
constructor(
private readonly baseProps: ScheduleTargetBaseProps,
protected readonly targetArn: string,
) {
}
protected abstract addTargetActionToRole(role: iam.IRole): void;
protected bindBaseTargetConfig(_schedule: ISchedule): ScheduleTargetConfig {
const role: iam.IRole = this.baseProps.role ?? this.createOrGetScheduleTargetRole(_schedule, this.targetArn);
this.addTargetActionToRole(role);
if (this.baseProps.deadLetterQueue) {
this.addDeadLetterQueueActionToRole(role, this.baseProps.deadLetterQueue);
}
return {
arn: this.targetArn,
role,
deadLetterConfig: this.baseProps.deadLetterQueue ? {
arn: this.baseProps.deadLetterQueue.queueArn,
} : undefined,
retryPolicy: this.renderRetryPolicy(this.baseProps.maxEventAge, this.baseProps.retryAttempts),
input: this.baseProps.input,
};
}
/**
* Create a return a Schedule Target Configuration for the given schedule
* @returns a Schedule Target Configuration
*/
bind(schedule: ISchedule): ScheduleTargetConfig {
return this.bindBaseTargetConfig(schedule);
}
/**
* Get or create the Role for the EventBridge Scheduler event
*
* If a role already exists, it will be returned. This ensures that if multiple
* schedules have the same target, they will share a role.
*/
private createOrGetScheduleTargetRole(schedule: ISchedule, targetArn: string): iam.IRole {
const stack = Stack.of(schedule);
const arn = Token.isUnresolved(targetArn) ? JSON.stringify(stack.resolve(targetArn)) : 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.ServicePrincipal('scheduler.amazonaws.com', {
conditions: {
StringEquals: {
'aws:SourceAccount': schedule.env.account,
'aws:SourceArn': schedule.scheduleGroup?.scheduleGroupArn ?? Stack.of(schedule).formatArn({
service: 'scheduler',
resource: 'schedule-group',
region: schedule.env.region,
account: schedule.env.account,
resourceName: schedule.scheduleGroup?.scheduleGroupName ?? 'default',
}),
},
},
});
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 schedule to send events with failed invocation to an Amazon SQS queue.
*/
private addDeadLetterQueueActionToRole(role: iam.IRole, queue: sqs.IQueue) {
role.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['sqs:SendMessage'],
resources: [queue.queueArn],
}));
}
private renderRetryPolicy(maximumEventAge: Duration | undefined, maximumRetryAttempts: number | undefined): CfnSchedule.RetryPolicyProperty {
const maxMaxAge = Duration.days(1).toSeconds();
const minMaxAge = Duration.minutes(1).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 1 minute');
}
}
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,
};
}
}