diff --git a/packages/@aws-cdk/aws-scheduler-alpha/README.md b/packages/@aws-cdk/aws-scheduler-alpha/README.md index 93dd3ec5b9262..dfdb11875852a 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/README.md +++ b/packages/@aws-cdk/aws-scheduler-alpha/README.md @@ -100,7 +100,7 @@ const oneTimeSchedule = new Schedule(this, 'Schedule', { Your AWS account comes with a default scheduler group. You can access the default group in CDK with: ```ts -const defaultGroup = Group.fromDefaultGroup(this, "DefaultGroup"); +const defaultScheduleGroup = ScheduleGroup.fromDefaultScheduleGroup(this, "DefaultGroup"); ``` You can add a schedule to a custom scheduling group managed by you. If a custom group is not specified, the schedule is added to the default group. @@ -108,14 +108,14 @@ You can add a schedule to a custom scheduling group managed by you. If a custom ```ts declare const target: targets.LambdaInvoke; -const group = new Group(this, "Group", { - groupName: "MyGroup", +const scheduleGroup = new ScheduleGroup(this, "ScheduleGroup", { + scheduleGroupName: "MyScheduleGroup", }); new Schedule(this, 'Schedule', { schedule: ScheduleExpression.rate(Duration.minutes(10)), target, - group, + scheduleGroup, }); ``` @@ -300,25 +300,25 @@ new cloudwatch.Alarm(this, 'SchedulesErrorAlarm', { }); ``` -### Metrics for a Group +### Metrics for a Schedule Group -To view metrics for a specific group you can use methods on class `Group`: +To view metrics for a specific group you can use methods on class `ScheduleGroup`: ```ts -const group = new Group(this, "Group", { - groupName: "MyGroup", +const scheduleGroup = new ScheduleGroup(this, "ScheduleGroup", { + scheduleGroupName: "MyScheduleGroup", }); new cloudwatch.Alarm(this, 'MyGroupErrorAlarm', { - metric: group.metricTargetErrors(), + metric: scheduleGroup.metricTargetErrors(), evaluationPeriods: 1, threshold: 0 }); // Or use default group -const defaultGroup = Group.fromDefaultGroup(this, "DefaultGroup"); -new cloudwatch.Alarm(this, 'DefaultGroupErrorAlarm', { - metric: defaultGroup.metricTargetErrors(), +const defaultScheduleGroup = ScheduleGroup.fromDefaultScheduleGroup(this, "DefaultScheduleGroup"); +new cloudwatch.Alarm(this, 'DefaultScheduleGroupErrorAlarm', { + metric: defaultScheduleGroup.metricTargetErrors(), evaluationPeriods: 1, threshold: 0 }); diff --git a/packages/@aws-cdk/aws-scheduler-alpha/awslint.json b/packages/@aws-cdk/aws-scheduler-alpha/awslint.json index b91f1ee2d3f6d..dfb1ef25db752 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/awslint.json +++ b/packages/@aws-cdk/aws-scheduler-alpha/awslint.json @@ -1,23 +1,10 @@ { "exclude": [ - "construct-ctor-props-optional:@aws-cdk/aws-scheduler-alpha.Group", "props-physical-name:@aws-cdk/aws-scheduler-alpha.GroupProps", - "from-method:@aws-cdk/aws-scheduler-alpha.Schedule", - "attribute-tag:@aws-cdk/aws-scheduler-alpha.Schedule.scheduleArn", - "attribute-tag:@aws-cdk/aws-scheduler-alpha.Schedule.scheduleName", + "attribute-tag:@aws-cdk/aws-scheduler-alpha.Schedule.scheduleGroup", "docs-public-apis:@aws-cdk/aws-scheduler-alpha.ContextAttribute.name", "docs-public-apis:@aws-cdk/aws-scheduler-alpha.Group", "docs-public-apis:@aws-cdk/aws-scheduler-alpha.GroupProps", - "docs-public-apis:@aws-cdk/aws-scheduler-alpha.IGroup", - "props-default-doc:@aws-cdk/aws-scheduler-alpha.ScheduleProps.targetOverrides", - "props-default-doc:@aws-cdk/aws-scheduler-alpha.ScheduleTargetConfig.deadLetterConfig", - "props-default-doc:@aws-cdk/aws-scheduler-alpha.ScheduleTargetConfig.ecsParameters", - "props-default-doc:@aws-cdk/aws-scheduler-alpha.ScheduleTargetConfig.eventBridgeParameters", - "props-default-doc:@aws-cdk/aws-scheduler-alpha.ScheduleTargetConfig.input", - "props-default-doc:@aws-cdk/aws-scheduler-alpha.ScheduleTargetConfig.kinesisParameters", - "props-default-doc:@aws-cdk/aws-scheduler-alpha.ScheduleTargetConfig.retryPolicy", - "props-default-doc:@aws-cdk/aws-scheduler-alpha.ScheduleTargetConfig.sageMakerPipelineParameters", - "props-default-doc:@aws-cdk/aws-scheduler-alpha.ScheduleTargetConfig.sqsParameters", - "docs-public-apis:@aws-cdk/aws-scheduler-alpha.ScheduleTargetProps" + "docs-public-apis:@aws-cdk/aws-scheduler-alpha.IGroup" ] } diff --git a/packages/@aws-cdk/aws-scheduler-alpha/lib/group.ts b/packages/@aws-cdk/aws-scheduler-alpha/lib/group.ts index 3cad4f2dc987d..061272a3abffd 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/lib/group.ts +++ b/packages/@aws-cdk/aws-scheduler-alpha/lib/group.ts @@ -290,6 +290,7 @@ abstract class GroupBase extends Resource implements IGroup { } /** * @resource AWS::Scheduler::ScheduleGroup + * @deprecated Use `ScheduleGroup` instead. `Group` will be removed when this module is stabilized. */ export class Group extends GroupBase { /** @@ -298,6 +299,7 @@ export class Group extends GroupBase { * @param scope construct scope * @param id construct id * @param groupArn the ARN of the group to import (e.g. `arn:aws:scheduler:region:account-id:schedule-group/group-name`) + * @deprecated Use `ScheduleGroup.fromScheduleGroupArn()` instead. */ public static fromGroupArn(scope: Construct, id: string, groupArn: string): IGroup { const arnComponents = Stack.of(scope).splitArn(groupArn, ArnFormat.SLASH_RESOURCE_NAME); @@ -314,6 +316,7 @@ export class Group extends GroupBase { * * @param scope construct scope * @param id construct id + * @deprecated Use `ScheduleGroup.fromDefaultScheduleGroup()` instead. */ public static fromDefaultGroup(scope: Construct, id: string): IGroup { return Group.fromGroupName(scope, id, 'default'); @@ -325,6 +328,7 @@ export class Group extends GroupBase { * @param scope construct scope * @param id construct id * @param groupName the name of the existing group to import + * @deprecated Use `ScheduleGroup.fromScheduleGroupName()` instead. */ public static fromGroupName(scope: Construct, id: string, groupName: string): IGroup { const groupArn = Stack.of(scope).formatArn({ @@ -338,12 +342,12 @@ export class Group extends GroupBase { public readonly groupName: string; public readonly groupArn: string; - public constructor(scope: Construct, id: string, props: GroupProps) { + public constructor(scope: Construct, id: string, props?: GroupProps) { super(scope, id); // Enhanced CDK Analytics Telemetry addConstructMetadata(this, props); - this.groupName = props.groupName ?? Names.uniqueResourceName(this, { + this.groupName = props?.groupName ?? Names.uniqueResourceName(this, { maxLength: 64, separator: '-', }); @@ -352,7 +356,7 @@ export class Group extends GroupBase { name: this.groupName, }); - group.applyRemovalPolicy(props.removalPolicy); + group.applyRemovalPolicy(props?.removalPolicy); this.groupArn = this.getResourceArnAttribute(group.attrArn, { service: 'scheduler', diff --git a/packages/@aws-cdk/aws-scheduler-alpha/lib/index.ts b/packages/@aws-cdk/aws-scheduler-alpha/lib/index.ts index 644bc4bd27b4a..542063013983c 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/lib/index.ts +++ b/packages/@aws-cdk/aws-scheduler-alpha/lib/index.ts @@ -3,3 +3,4 @@ export * from './input'; export * from './schedule'; export * from './group'; export * from './target'; +export * from './schedule-group'; diff --git a/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule-group.ts b/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule-group.ts new file mode 100644 index 0000000000000..aa19c11a7f657 --- /dev/null +++ b/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule-group.ts @@ -0,0 +1,371 @@ +import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import { CfnScheduleGroup } from 'aws-cdk-lib/aws-scheduler'; +import { Arn, ArnFormat, Aws, IResource, Names, RemovalPolicy, Resource, Stack } from 'aws-cdk-lib/core'; +import { addConstructMetadata } from 'aws-cdk-lib/core/lib/metadata-resource'; +import { Construct } from 'constructs'; + +/** + * Properties for a Schedule Group. + */ +export interface ScheduleGroupProps { + /** + * The name of the schedule group. + * + * Up to 64 letters (uppercase and lowercase), numbers, hyphens, underscores and dots are allowed. + * + * @default - A unique name will be generated + */ + readonly scheduleGroupName?: string; + + /** + * The removal policy for the group. If the group is removed also all schedules are removed. + * + * @default RemovalPolicy.RETAIN + */ + readonly removalPolicy?: RemovalPolicy; +} + +/** + * Interface representing a created or an imported `ScheduleGroup`. + */ +export interface IScheduleGroup extends IResource { + /** + * The name of the schedule group + * + * @attribute + */ + readonly scheduleGroupName: string; + + /** + * The arn of the schedule group + * + * @attribute + */ + readonly scheduleGroupArn: string; + + /** + * Return the given named metric for this group schedules + * + * @default - sum over 5 minutes + */ + metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for the number of invocations that were throttled because it exceeds your service quotas. + * + * @see https://docs.aws.amazon.com/scheduler/latest/UserGuide/scheduler-quotas.html + * + * @default - sum over 5 minutes + */ + metricThrottled(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for all invocation attempts. + * + * @default - sum over 5 minutes + */ + metricAttempts(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Emitted when the target returns an exception after EventBridge Scheduler calls the target API. + * + * @default - sum over 5 minutes + */ + metricTargetErrors(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for invocation failures due to API throttling by the target. + * + * @default - sum over 5 minutes + */ + metricTargetThrottled(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for dropped invocations when EventBridge Scheduler stops attempting to invoke the target after a schedule's retry policy has been exhausted. + * + * @default - sum over 5 minutes + */ + metricDropped(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for invocations delivered to the DLQ + * + * @default - sum over 5 minutes + */ + metricSentToDLQ(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for failed invocations that also failed to deliver to DLQ. + * + * @default - sum over 5 minutes + */ + metricFailedToBeSentToDLQ(errorCode?: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for delivery of failed invocations to DLQ when the payload of the event sent to the DLQ exceeds the maximum size allowed by Amazon SQS. + * + * @default - sum over 5 minutes + */ + metricSentToDLQTruncated(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Grant the indicated permissions on this group to the given principal + */ + grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant; + /** + * Grant list and get schedule permissions for schedules in this group to the given principal + */ + grantReadSchedules(identity: iam.IGrantable): iam.Grant; + /** + * Grant create and update schedule permissions for schedules in this group to the given principal + */ + grantWriteSchedules(identity: iam.IGrantable): iam.Grant; + /** + * Grant delete schedule permission for schedules in this group to the given principal + */ + grantDeleteSchedules(identity: iam.IGrantable): iam.Grant; +} + +abstract class ScheduleGroupBase extends Resource implements IScheduleGroup { + /** + * The name of the schedule group + * + * @attribute + */ + public abstract readonly scheduleGroupName: string; + + /** + * The arn of the schedule group + * + * @attribute + */ + public abstract readonly scheduleGroupArn: string; + + /** + * Return the given named metric for this schedule group + * + * @default - sum over 5 minutes + */ + public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace: 'AWS/Scheduler', + metricName, + dimensionsMap: { ScheduleGroup: this.scheduleGroupName }, + statistic: 'sum', + ...props, + }).attachTo(this); + } + + /** + * Metric for the number of invocations that were throttled because it exceeds your service quotas. + * + * @see https://docs.aws.amazon.com/scheduler/latest/UserGuide/scheduler-quotas.html + * + * @default - sum over 5 minutes + */ + public metricThrottled(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('InvocationThrottleCount', props); + } + + /** + * Metric for all invocation attempts. + * + * @default - sum over 5 minutes + */ + public metricAttempts(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('InvocationAttemptCount', props); + } + + /** + * Emitted when the target returns an exception after EventBridge Scheduler calls the target API. + * + * @default - sum over 5 minutes + */ + public metricTargetErrors(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('TargetErrorCount', props); + } + + /** + * Metric for invocation failures due to API throttling by the target. + * + * @default - sum over 5 minutes + */ + public metricTargetThrottled(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('TargetErrorThrottledCount', props); + } + + /** + * Metric for dropped invocations when EventBridge Scheduler stops attempting to invoke the target after a schedule's retry policy has been exhausted. + * + * @default - sum over 5 minutes + */ + public metricDropped(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('InvocationDroppedCount', props); + } + + /** + * Metric for invocations delivered to the DLQ + * + * @default - sum over 5 minutes + */ + public metricSentToDLQ(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('InvocationsSentToDeadLetterCount', props); + } + + /** + * Metric for failed invocations that also failed to deliver to DLQ. + * + * @default - sum over 5 minutes + */ + public metricFailedToBeSentToDLQ(errorCode?: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { + if (errorCode) { + return this.metric(`InvocationsFailedToBeSentToDeadLetterCount_${errorCode}`, props); + } + + return this.metric('InvocationsFailedToBeSentToDeadLetterCount', props); + } + + /** + * Metric for delivery of failed invocations to DLQ when the payload of the event sent to the DLQ exceeds the maximum size allowed by Amazon SQS. + * + * @default - sum over 5 minutes + */ + public metricSentToDLQTruncated(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('InvocationsSentToDeadLetterCount_Truncated_MessageSizeExceeded', props); + } + + /** + * Grant the indicated permissions on this schedule group to the given principal + */ + public grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant { + return iam.Grant.addToPrincipal({ + grantee, + actions, + resourceArns: [this.scheduleGroupArn], + scope: this, + }); + } + + private arnForScheduleInGroup(scheduleName: string): string { + return Arn.format({ + region: this.env.region, + account: this.env.account, + partition: Aws.PARTITION, + service: 'scheduler', + resource: 'schedule', + resourceName: this.scheduleGroupName + '/' + scheduleName, + }); + } + + /** + * Grant list and get schedule permissions for schedules in this group to the given principal + */ + public grantReadSchedules(identity: iam.IGrantable) { + return iam.Grant.addToPrincipal({ + grantee: identity, + actions: ['scheduler:GetSchedule', 'scheduler:ListSchedules'], + resourceArns: [this.arnForScheduleInGroup('*')], + scope: this, + }); + } + + /** + * Grant create and update schedule permissions for schedules in this group to the given principal + */ + public grantWriteSchedules(identity: iam.IGrantable): iam.Grant { + return iam.Grant.addToPrincipal({ + grantee: identity, + actions: ['scheduler:CreateSchedule', 'scheduler:UpdateSchedule'], + resourceArns: [this.arnForScheduleInGroup('*')], + scope: this, + }); + } + + /** + * Grant delete schedule permission for schedules in this group to the given principal + */ + public grantDeleteSchedules(identity: iam.IGrantable): iam.Grant { + return iam.Grant.addToPrincipal({ + grantee: identity, + actions: ['scheduler:DeleteSchedule'], + resourceArns: [this.arnForScheduleInGroup('*')], + scope: this, + }); + } +} + +/** + * A Schedule Group. + * @resource AWS::Scheduler::ScheduleGroup + */ +export class ScheduleGroup extends ScheduleGroupBase { + /** + * Import an external schedule group by ARN. + * + * @param scope construct scope + * @param id construct id + * @param scheduleGroupArn the ARN of the schedule group to import (e.g. `arn:aws:scheduler:region:account-id:schedule-group/group-name`) + */ + public static fromScheduleGroupArn(scope: Construct, id: string, scheduleGroupArn: string): IScheduleGroup { + const arnComponents = Stack.of(scope).splitArn(scheduleGroupArn, ArnFormat.SLASH_RESOURCE_NAME); + const scheduleGroupName = arnComponents.resourceName!; + class Import extends ScheduleGroupBase { + scheduleGroupName = scheduleGroupName; + scheduleGroupArn = scheduleGroupArn; + } + return new Import(scope, id); + } + + /** + * Import a default schedule group. + * + * @param scope construct scope + * @param id construct id + */ + public static fromDefaultScheduleGroup(scope: Construct, id: string): IScheduleGroup { + return ScheduleGroup.fromScheduleGroupName(scope, id, 'default'); + } + + /** + * Import an existing schedule group with a given name. + * + * @param scope construct scope + * @param id construct id + * @param scheduleGroupName the name of the existing schedule group to import + */ + public static fromScheduleGroupName(scope: Construct, id: string, scheduleGroupName: string): IScheduleGroup { + const groupArn = Stack.of(scope).formatArn({ + service: 'scheduler', + resource: 'schedule-group', + resourceName: scheduleGroupName, + }); + return ScheduleGroup.fromScheduleGroupArn(scope, id, groupArn); + } + + public readonly scheduleGroupName: string; + public readonly scheduleGroupArn: string; + + public constructor(scope: Construct, id: string, props?: ScheduleGroupProps) { + super(scope, id); + // Enhanced CDK Analytics Telemetry + addConstructMetadata(this, props); + + this.scheduleGroupName = props?.scheduleGroupName ?? Names.uniqueResourceName(this, { + maxLength: 64, + separator: '-', + }); + + const resource = new CfnScheduleGroup(this, 'Resource', { + name: this.scheduleGroupName, + }); + + resource.applyRemovalPolicy(props?.removalPolicy); + + this.scheduleGroupArn = this.getResourceArnAttribute(resource.attrArn, { + service: 'scheduler', + resource: 'schedule-group', + resourceName: this.scheduleGroupName, + }); + } +} diff --git a/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule.ts b/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule.ts index 56388aad71734..87337d446dbed 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule.ts +++ b/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule.ts @@ -1,4 +1,4 @@ -import { Duration, IResource, Resource, Token } from 'aws-cdk-lib'; +import { Arn, ArnFormat, Duration, IResource, Resource, Token } from 'aws-cdk-lib'; import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; import * as kms from 'aws-cdk-lib/aws-kms'; import { CfnSchedule } from 'aws-cdk-lib/aws-scheduler'; @@ -6,26 +6,35 @@ import { addConstructMetadata } from 'aws-cdk-lib/core/lib/metadata-resource'; import { Construct } from 'constructs'; import { IGroup } from './group'; import { ScheduleExpression } from './schedule-expression'; +import { IScheduleGroup } from './schedule-group'; import { IScheduleTarget } from './target'; /** * Interface representing a created or an imported `Schedule`. */ export interface ISchedule extends IResource { + /** + * The arn of the schedule. + * @attribute + */ + readonly scheduleArn: string; + /** * The name of the schedule. + * @attribute */ readonly scheduleName: string; /** * The schedule group associated with this schedule. */ - readonly group?: IGroup; + readonly scheduleGroup?: IScheduleGroup; /** - * The arn of the schedule. + * The schedule group associated with this schedule. + * @deprecated Use `scheduleGroup` instead. `group` will be removed when this module is stabilized. */ - readonly scheduleArn: string; + readonly group?: IGroup; } /** @@ -104,9 +113,17 @@ export interface ScheduleProps { * The schedule's group. * * @default - By default a schedule will be associated with the `default` group. + * @deprecated Use `scheduleGroup` instead. `group` will be removed when this module is stabilized. */ readonly group?: IGroup; + /** + * The schedule's group. + * + * @default - By default a schedule will be associated with the `default` group. + */ + readonly scheduleGroup?: IScheduleGroup; + /** * Indicates whether the schedule is enabled. * @@ -244,11 +261,28 @@ export class Schedule extends Resource implements ISchedule { return this.metricAll('InvocationsSentToDeadLetterCount_Truncated_MessageSizeExceeded', props); } + /** + * Import an existing schedule using the ARN. + */ + public static fromScheduleArn(scope: Construct, id: string, scheduleArn: string): ISchedule { + class Import extends Resource implements ISchedule { + public readonly scheduleArn = scheduleArn; + public readonly scheduleName = Arn.split(scheduleArn, ArnFormat.SLASH_RESOURCE_NAME).resourceName!.split('/')[1]; + } + return new Import(scope, id); + } + /** * The schedule group associated with this schedule. + * @deprecated Use `scheduleGroup` instead. `group` will be removed when this module is stabilized. */ public readonly group?: IGroup; + /** + * The schedule group associated with this schedule. + */ + public readonly scheduleGroup?: IScheduleGroup; + /** * The arn of the schedule. */ @@ -277,6 +311,7 @@ export class Schedule extends Resource implements ISchedule { addConstructMetadata(this, props); this.group = props.group; + this.scheduleGroup = props.scheduleGroup; const targetConfig = props.target.bind(this); @@ -302,7 +337,7 @@ export class Schedule extends Resource implements ISchedule { }, scheduleExpression: props.schedule.expressionString, scheduleExpressionTimezone: props.schedule.timeZone?.timezoneName, - groupName: this.group?.groupName, + groupName: this.scheduleGroup?.scheduleGroupName ?? this.group?.groupName, state: (props.enabled ?? true) ? 'ENABLED' : 'DISABLED', kmsKeyArn: this.key?.keyArn, target: { @@ -326,7 +361,7 @@ export class Schedule extends Resource implements ISchedule { this.scheduleArn = this.getResourceArnAttribute(resource.attrArn, { service: 'scheduler', resource: 'schedule', - resourceName: `${this.group?.groupName ?? 'default'}/${this.physicalName}`, + resourceName: `${this.scheduleGroup?.scheduleGroupName ?? this.group?.groupName ?? 'default'}/${this.physicalName}`, }); } diff --git a/packages/@aws-cdk/aws-scheduler-alpha/lib/target.ts b/packages/@aws-cdk/aws-scheduler-alpha/lib/target.ts index 1d5335986e120..56df1291f9087 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/lib/target.ts +++ b/packages/@aws-cdk/aws-scheduler-alpha/lib/target.ts @@ -31,39 +31,50 @@ export interface ScheduleTargetConfig { /** * What input to pass to the target + * @default - No input */ 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. + * @default - Maximum retry attempts of 185 and maximum age of 86400 seconds (1 day) */ 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.\ + * 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. + * @default - No dead-letter queue */ readonly deadLetterConfig?: CfnSchedule.DeadLetterConfigProperty; /** * The templated target type for the Amazon ECS RunTask API Operation. + * @default - No parameters */ readonly ecsParameters?: CfnSchedule.EcsParametersProperty; + /** * The templated target type for the EventBridge PutEvents API operation. + * @default - No parameters */ readonly eventBridgeParameters?: CfnSchedule.EventBridgeParametersProperty; /** * The templated target type for the Amazon Kinesis PutRecord API operation. + * @default - No parameters */ readonly kinesisParameters?: CfnSchedule.KinesisParametersProperty; /** * The templated target type for the Amazon SageMaker StartPipelineExecution API operation. + * @default - No parameters */ readonly sageMakerPipelineParameters?: CfnSchedule.SageMakerPipelineParametersProperty; + /** * The templated target type for the Amazon SQS SendMessage API Operation + * @default - No parameters */ readonly sqsParameters?: CfnSchedule.SqsParametersProperty; } 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 4c66c83a7dd44..3303794202508 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/aws-scheduler-alpha/rosetta/default.ts-fixture @@ -8,7 +8,7 @@ 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, TimeWindow } from '@aws-cdk/aws-scheduler-alpha'; +import { ScheduleExpression, ScheduleTargetInput, ContextAttribute, ScheduleGroup, Schedule, TimeWindow } 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 e0de97b02ce30..82207aa2bd65d 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/test/group.test.ts +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/group.test.ts @@ -1,3 +1,4 @@ +import { testDeprecated } from '@aws-cdk/cdk-build-tools'; 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'; @@ -7,7 +8,6 @@ import { CfnScheduleGroup } from 'aws-cdk-lib/aws-scheduler'; import { IScheduleTarget, ScheduleExpression, ScheduleTargetConfig } from '../lib'; import { Group, GroupProps } from '../lib/group'; import { Schedule } from '../lib/schedule'; - class SomeLambdaTarget implements IScheduleTarget { public constructor(private readonly fn: lambda.IFunction, private readonly role: iam.IRole) { } @@ -36,7 +36,7 @@ describe('Schedule Group', () => { }); }); - test('creates a group with default properties', () => { + testDeprecated('creates a group with default properties', () => { const props: GroupProps = {}; const group = new Group(stack, 'TestGroup', props); @@ -49,7 +49,7 @@ describe('Schedule Group', () => { expect(resource.name).toEqual(group.groupName); }); - test('creates a group with removal policy', () => { + testDeprecated('creates a group with removal policy', () => { const props: GroupProps = { removalPolicy: RemovalPolicy.RETAIN, }; @@ -60,7 +60,7 @@ describe('Schedule Group', () => { }); }); - test('creates a group with specified name', () => { + testDeprecated('creates a group with specified name', () => { const props: GroupProps = { groupName: 'MyGroup', }; @@ -76,7 +76,7 @@ describe('Schedule Group', () => { }); }); - test('creates a group from ARN', () => { + testDeprecated('creates a group from ARN', () => { const groupArn = 'arn:aws:scheduler:region:account-id:schedule-group/group-name'; const group = Group.fromGroupArn(stack, 'TestGroup', groupArn); @@ -87,7 +87,7 @@ describe('Schedule Group', () => { expect(groups).toEqual({}); }); - test('creates a group from name', () => { + testDeprecated('creates a group from name', () => { const groupName = 'MyGroup'; const group = Group.fromGroupName(stack, 'TestGroup', groupName); @@ -98,7 +98,7 @@ describe('Schedule Group', () => { expect(groups).toEqual({}); }); - test('creates a group from default group', () => { + testDeprecated('creates a group from default group', () => { const group = Group.fromDefaultGroup(stack, 'DefaultGroup'); expect(group.groupArn).toBeDefined(); @@ -108,7 +108,7 @@ describe('Schedule Group', () => { expect(groups).toEqual({}); }); - test('adds schedules to the group', () => { + testDeprecated('adds schedules to the group', () => { const props: GroupProps = { groupName: 'MyGroup', }; @@ -136,7 +136,7 @@ describe('Schedule Group', () => { }); }); - test('adds schedules to the group with unspecified name', () => { + testDeprecated('adds schedules to the group with unspecified name', () => { const group = new Group(stack, 'TestGroup', {}); const role = iam.Role.fromRoleArn(stack, 'ImportedRole', 'arn:aws:iam::123456789012:role/someRole'); @@ -161,7 +161,7 @@ describe('Schedule Group', () => { }); }); - test('grantReadSchedules', () => { + testDeprecated('grantReadSchedules', () => { // GIVEN const props: GroupProps = { groupName: 'MyGroup', @@ -201,7 +201,7 @@ describe('Schedule Group', () => { }); }); - test('grantWriteSchedules', () => { + testDeprecated('grantWriteSchedules', () => { // GIVEN const props: GroupProps = { groupName: 'MyGroup', @@ -242,7 +242,7 @@ describe('Schedule Group', () => { }); }); - test('grantDeleteSchedules', () => { + testDeprecated('grantDeleteSchedules', () => { // GIVEN const props: GroupProps = { groupName: 'MyGroup', @@ -324,7 +324,7 @@ describe('Schedule Group Metrics', () => { }); }); - test('Invocations Failed to Deliver to DLQ Metrics', () => { + testDeprecated('Invocations Failed to Deliver to DLQ Metrics', () => { // GIVEN const app = new App(); const props: GroupProps = { diff --git a/packages/@aws-cdk/aws-scheduler-alpha/test/schedule-group.test.ts b/packages/@aws-cdk/aws-scheduler-alpha/test/schedule-group.test.ts new file mode 100644 index 0000000000000..6c122ab7e7024 --- /dev/null +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/schedule-group.test.ts @@ -0,0 +1,359 @@ +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 { IScheduleTarget, ScheduleExpression, ScheduleTargetConfig } from '../lib'; +import { Schedule } from '../lib/schedule'; +import { ScheduleGroup, ScheduleGroupProps } from '../lib/schedule-group'; + +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; + const expr = ScheduleExpression.at(new Date(Date.UTC(1969, 10, 20, 0, 0, 0))); + + beforeEach(() => { + 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_LATEST, + tracing: lambda.Tracing.PASS_THROUGH, + }); + }); + + test('creates a group with default properties', () => { + const props: ScheduleGroupProps = {}; + const group = new ScheduleGroup(stack, 'TestGroup', props); + + expect(group.scheduleGroupName).toBeDefined(); + expect(group.scheduleGroupArn).toBeDefined(); + + const resource = group.node.findChild('Resource') as CfnScheduleGroup; + expect(resource).toBeInstanceOf(CfnScheduleGroup); + expect(resource.name).toEqual(group.scheduleGroupName); + }); + + test('creates a group with removal policy', () => { + const props: ScheduleGroupProps = { + removalPolicy: RemovalPolicy.RETAIN, + }; + new ScheduleGroup(stack, 'TestGroup', props); + + Template.fromStack(stack).hasResource('AWS::Scheduler::ScheduleGroup', { + DeletionPolicy: 'Retain', + }); + }); + + test('creates a group with specified name', () => { + const props: ScheduleGroupProps = { + scheduleGroupName: 'MyGroup', + }; + const group = new ScheduleGroup(stack, 'TestGroup', props); + const resource = group.node.findChild('Resource') as CfnScheduleGroup; + expect(resource).toBeInstanceOf(CfnScheduleGroup); + expect(resource.name).toEqual(group.scheduleGroupName); + + Template.fromStack(stack).hasResource('AWS::Scheduler::ScheduleGroup', { + Properties: { + Name: `${props.scheduleGroupName}`, + }, + }); + }); + + test('creates a group from ARN', () => { + const groupArn = 'arn:aws:scheduler:region:account-id:schedule-group/group-name'; + const group = ScheduleGroup.fromScheduleGroupArn(stack, 'TestGroup', groupArn); + + expect(group.scheduleGroupArn).toBeDefined(); + expect(group.scheduleGroupName).toEqual('group-name'); + + const groups = Template.fromStack(stack).findResources('AWS::Scheduler::ScheduleGroup'); + expect(groups).toEqual({}); + }); + + test('creates a group from name', () => { + const groupName = 'MyGroup'; + const group = ScheduleGroup.fromScheduleGroupName(stack, 'TestGroup', groupName); + + expect(group.scheduleGroupArn).toBeDefined(); + expect(group.scheduleGroupName).toEqual(groupName); + + const groups = Template.fromStack(stack).findResources('AWS::Scheduler::ScheduleGroup'); + expect(groups).toEqual({}); + }); + + test('creates a group from default group', () => { + const group = ScheduleGroup.fromDefaultScheduleGroup(stack, 'DefaultGroup'); + + expect(group.scheduleGroupArn).toBeDefined(); + expect(group.scheduleGroupName).toEqual('default'); + + const groups = Template.fromStack(stack).findResources('AWS::Scheduler::ScheduleGroup'); + expect(groups).toEqual({}); + }); + + test('adds schedules to the group', () => { + const props: ScheduleGroupProps = { + scheduleGroupName: 'MyGroup', + }; + const group = new ScheduleGroup(stack, 'TestGroup', props); + const role = iam.Role.fromRoleArn(stack, 'ImportedRole', 'arn:aws:iam::123456789012:role/someRole'); + + const schedule1 = new Schedule(stack, 'MyScheduleDummy1', { + schedule: expr, + scheduleGroup: group, + target: new SomeLambdaTarget(func, role), + }); + const schedule2 = new Schedule(stack, 'MyScheduleDummy2', { + schedule: expr, + scheduleGroup: group, + target: new SomeLambdaTarget(func, role), + }); + + expect(schedule1.scheduleGroup).toBe(group); + expect(schedule2.scheduleGroup).toBe(group); + + Template.fromStack(stack).hasResource('AWS::Scheduler::Schedule', { + Properties: { + GroupName: `${props.scheduleGroupName}`, + }, + }); + }); + + test('adds schedules to the group with unspecified name', () => { + const group = new ScheduleGroup(stack, 'TestGroup', {}); + const role = iam.Role.fromRoleArn(stack, 'ImportedRole', 'arn:aws:iam::123456789012:role/someRole'); + + const schedule1 = new Schedule(stack, 'MyScheduleDummy1', { + schedule: expr, + scheduleGroup: group, + target: new SomeLambdaTarget(func, role), + }); + const schedule2 = new Schedule(stack, 'MyScheduleDummy2', { + schedule: expr, + scheduleGroup: group, + target: new SomeLambdaTarget(func, role), + }); + + expect(schedule1.scheduleGroup).toBe(group); + expect(schedule2.scheduleGroup).toBe(group); + + Template.fromStack(stack).hasResource('AWS::Scheduler::Schedule', { + Properties: { + GroupName: group.scheduleGroupName, + }, + }); + }); + + test('grantReadSchedules', () => { + // GIVEN + const props: ScheduleGroupProps = { + scheduleGroupName: 'MyGroup', + }; + const group = new ScheduleGroup(stack, 'TestGroup', props); + + const user = new iam.User(stack, 'User'); + + // WHEN + group.grantReadSchedules(user); + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'scheduler:GetSchedule', + 'scheduler:ListSchedules', + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':scheduler:us-east-1:123456789012:schedule/MyGroup/*', + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + }); + }); + + test('grantWriteSchedules', () => { + // GIVEN + const props: ScheduleGroupProps = { + scheduleGroupName: 'MyGroup', + }; + const group = new ScheduleGroup(stack, 'TestGroup', props); + + const user = new iam.User(stack, 'User'); + + // WHEN + group.grantWriteSchedules(user); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'scheduler:CreateSchedule', + 'scheduler:UpdateSchedule', + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':scheduler:us-east-1:123456789012:schedule/MyGroup/*', + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + }); + }); + + test('grantDeleteSchedules', () => { + // GIVEN + const props: ScheduleGroupProps = { + scheduleGroupName: 'MyGroup', + }; + const group = new ScheduleGroup(stack, 'TestGroup', props); + + const user = new iam.User(stack, 'User'); + + // WHEN + group.grantDeleteSchedules(user); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'scheduler:DeleteSchedule', + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':scheduler:us-east-1:123456789012:schedule/MyGroup/*', + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + }); + }); +}); + +describe('Schedule Group Metrics', () => { + test.each([ + ['metricTargetErrors', 'TargetErrorCount'], + ['metricThrottled', 'InvocationThrottleCount'], + ['metricAttempts', 'InvocationAttemptCount'], + ['metricTargetThrottled', 'TargetErrorThrottledCount'], + ['metricDropped', 'InvocationDroppedCount'], + ['metricSentToDLQ', 'InvocationsSentToDeadLetterCount'], + ['metricSentToDLQTruncated', 'InvocationsSentToDeadLetterCount_Truncated_MessageSizeExceeded'], + ])('calling %s creates alarm for %s metric', (metricMethodName, metricName) => { + // GIVEN + const app = new App(); + const props: ScheduleGroupProps = { + scheduleGroupName: 'MyGroup', + }; + const stack = new Stack(app, 'Stack', { env: { region: 'us-east-1', account: '123456789012' } }); + const group = new ScheduleGroup(stack, 'TestGroup', props); + + // WHEN + const metricMethod = (group as any)[metricMethodName].bind(group); // Get the method dynamically + const metricTargetErrors = metricMethod({ + period: Duration.minutes(1), + }); + + new cw.Alarm(stack, `Group${metricName}Alarm`, { + metric: metricTargetErrors, + evaluationPeriods: 1, + threshold: 1, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { + Dimensions: Match.arrayWith([ + Match.objectLike({ + Name: 'ScheduleGroup', + Value: 'MyGroup', + }), + ]), + MetricName: metricName, + Namespace: 'AWS/Scheduler', + }); + }); + + test('Invocations Failed to Deliver to DLQ Metrics', () => { + // GIVEN + const app = new App(); + const props: ScheduleGroupProps = { + scheduleGroupName: 'MyGroup', + }; + const stack = new Stack(app, 'Stack', { env: { region: 'us-east-1', account: '123456789012' } }); + const group = new ScheduleGroup(stack, 'TestGroup', props); + const errorCode = '403'; + + // WHEN + const metricFailedToBeSentToDLQ = group.metricFailedToBeSentToDLQ(errorCode, { + period: Duration.minutes(1), + }); + + new cw.Alarm(stack, 'GroupFailedInvocationsToDLQAlarm', { + metric: metricFailedToBeSentToDLQ, + evaluationPeriods: 1, + threshold: 1, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { + Dimensions: Match.arrayWith([ + Match.objectLike({ + Name: 'ScheduleGroup', + Value: 'MyGroup', + }), + ]), + MetricName: `InvocationsFailedToBeSentToDeadLetterCount_${errorCode}`, + Namespace: 'AWS/Scheduler', + }); + }); +}); diff --git a/packages/@aws-cdk/aws-scheduler-targets-alpha/lib/target.ts b/packages/@aws-cdk/aws-scheduler-targets-alpha/lib/target.ts index be72cc91a32a2..7800bb32ca899 100644 --- a/packages/@aws-cdk/aws-scheduler-targets-alpha/lib/target.ts +++ b/packages/@aws-cdk/aws-scheduler-targets-alpha/lib/target.ts @@ -114,12 +114,12 @@ export abstract class ScheduleTargetBase { conditions: { StringEquals: { 'aws:SourceAccount': schedule.env.account, - 'aws:SourceArn': schedule.group?.groupArn ?? Stack.of(schedule).formatArn({ + 'aws:SourceArn': schedule.scheduleGroup?.scheduleGroupArn ?? schedule.group?.groupArn ?? Stack.of(schedule).formatArn({ service: 'scheduler', resource: 'schedule-group', region: schedule.env.region, account: schedule.env.account, - resourceName: schedule.group?.groupName ?? 'default', + resourceName: schedule.scheduleGroup?.scheduleGroupName ?? schedule.group?.groupName ?? 'default', }), }, }, diff --git a/packages/@aws-cdk/aws-scheduler-targets-alpha/test/universal.test.ts b/packages/@aws-cdk/aws-scheduler-targets-alpha/test/universal.test.ts index 48dac4c693d5e..50dfa41f0657c 100644 --- a/packages/@aws-cdk/aws-scheduler-targets-alpha/test/universal.test.ts +++ b/packages/@aws-cdk/aws-scheduler-targets-alpha/test/universal.test.ts @@ -1,5 +1,5 @@ import * as scheduler from '@aws-cdk/aws-scheduler-alpha'; -import { Group } from '@aws-cdk/aws-scheduler-alpha'; +import { ScheduleGroup } from '@aws-cdk/aws-scheduler-alpha'; import { App, Duration, Stack } from 'aws-cdk-lib'; import { Annotations, Template } from 'aws-cdk-lib/assertions'; import * as iam from 'aws-cdk-lib/aws-iam'; @@ -256,8 +256,8 @@ describe('Universal schedule target', () => { QueueName: 'my-queue', }), }); - const group = new Group(stack, 'Group', { - groupName: 'mygroup', + const scheduleGroup = new ScheduleGroup(stack, 'ScheduleGroup', { + scheduleGroupName: 'mygroup', }); new scheduler.Schedule(stack, 'Schedule1', { @@ -268,7 +268,7 @@ describe('Universal schedule target', () => { new scheduler.Schedule(stack, 'Schedule2', { schedule: scheduleExpression, target, - group, + scheduleGroup, }); const template = Template.fromStack(stack); @@ -312,7 +312,7 @@ describe('Universal schedule target', () => { 'aws:SourceAccount': '123456789012', 'aws:SourceArn': { 'Fn::GetAtt': [ - 'GroupC77FDACD', + 'ScheduleGroup4D377372', 'Arn', ], },