Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(scheduler): ScheduleGroup #26196

Merged
merged 10 commits into from
Jul 24, 2023
363 changes: 363 additions & 0 deletions packages/@aws-cdk/aws-scheduler-alpha/lib/group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,363 @@
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, PhysicalName, RemovalPolicy, Resource, Stack } from 'aws-cdk-lib/core';
import { Construct } from 'constructs';
import { Schedule } from './schedule';

export interface GroupProps {
/**
* 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 groupName?: string;

/**
* The removal policy for the group. If the group is removed also all schedules are removed.
*
* @default RemovalPolicy.RETAIN
*/
readonly removalPolicy?: RemovalPolicy;
}

export interface IGroup extends IResource {
/**
* The name of the schedule group
*
* @attribute
*/
readonly groupName: string;

/**
* The arn of the schedule group
*
* @attribute
*/
readonly groupArn: string;

addSchedules(...schedules: Schedule[]): void;

/**
* 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
*/
metricSentToDLQTrunacted(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;
Copy link
Contributor Author

@filletofish filletofish Jul 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compared to RFC I have updated the method names for granting permissions. From grantRead to grantReadSchedules - to be more precise that the method grants permissions to manage schedules in the group, not the permissions for the group itself.

There are IAM permissions for the schedules and for the groups.

/**
* 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 GroupBase extends Resource implements IGroup {
/**
* The name of the schedule group
*
* @attribute
*/
public abstract readonly groupName: string;

/**
* The arn of the schedule group
*
* @attribute
*/
public abstract readonly groupArn: string;

addSchedules(...schedules: Schedule[]): void {
schedules.forEach(schedule => {
schedule.group = this;
});
}

/**
* Return the given named metric for this group schedules
*
* @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.groupName },
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
*/
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
*/
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
*/
metricSentToDLQTrunacted(props?: cloudwatch.MetricOptions): cloudwatch.Metric {
return this.metric('InvocationsSentToDeadLetterCount_Truncated_MessageSizeExceeded', props);
}

/**
* Grant the indicated permissions on this group to the given principal
*/
grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant {
return iam.Grant.addToPrincipal({
grantee,
actions,
resourceArns: [this.groupArn],
scope: this,
});
}

arnForScheduleInGroup(scheduleName: string): string {
return Arn.format({
region: this.env.region,
account: this.env.account,
partition: Aws.PARTITION,
service: 'scheduler',
resource: 'schedule',
resourceName: this.groupName + '/' + scheduleName,
});
}
filletofish marked this conversation as resolved.
Show resolved Hide resolved

/**
* Grant list and get schedule permissions for schedules in this group to the given principal
*/
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
*/
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
*/
grantDeleteSchedules(identity: iam.IGrantable): iam.Grant {
return iam.Grant.addToPrincipal({
grantee: identity,
actions: ['scheduler:DeleteSchedule'],
resourceArns: [this.arnForScheduleInGroup('*')],
scope: this,
});
}
}

export class Group extends GroupBase {
/**
* Import an external group by ARN.
*
* @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`)
*/
public static fromGroupArn(scope: Construct, id: string, groupArn: string): IGroup {
const arnComponents = Stack.of(scope).splitArn(groupArn, ArnFormat.SLASH_RESOURCE_NAME);
const groupName = arnComponents.resourceName!;
class Import extends GroupBase {
public groupName = groupName;
public groupArn = groupArn;
}
return new Import(scope, id);
}

/**
* Import a default schedule group.
*
* @param scope construct scope
* @param id construct id
*/
public static fromDefaultGroup(scope: Construct, id: string): IGroup {
return Group.fromGroupName(scope, id, 'default');
}

/**
* Import an existing group with a given name.
*
* @param scope construct scope
* @param id construct id
* @param groupName the name of the existing group to import
*/
static fromGroupName(scope: Construct, id: string, groupName: string): IGroup {
const groupArn = Stack.of(scope).formatArn({
service: 'scheduler',
resource: 'schedule-group',
resourceName: groupName,
});
return Group.fromGroupArn(scope, id, groupArn);
}

public readonly groupName: string;
public readonly groupArn: string;

constructor(scope: Construct, id: string, props: GroupProps) {
super(scope, id, {
physicalName: props.groupName ?? PhysicalName.GENERATE_IF_NEEDED,
});

const group = new CfnScheduleGroup(this, 'Resource', {
name: this.physicalName,
});

this.groupArn = this.getResourceArnAttribute(group.attrArn, {
service: 'scheduler',
resource: 'schedule-group',
resourceName: this.physicalName,
});
this.groupName = this.physicalName;
}
Copy link
Contributor

@Jacco Jacco Jul 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a call to applyRemovalPolicy is missing here? This means the removalPolicy from the props will not be emitted to CloudFormation. (+ that needs a test)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I have actually missed that.

@default RemovalPolicy.RETAIN

Are we sure that we want the default to be RETAIN?

}
3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-scheduler-alpha/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './schedule-expression';
export * from './input';
export * from './schedule';
export * from './schedule';
export * from './group';
7 changes: 6 additions & 1 deletion packages/@aws-cdk/aws-scheduler-alpha/lib/schedule.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { IResource } from 'aws-cdk-lib';
import { IResource, Resource } from 'aws-cdk-lib';
import { IGroup } from './group';

/**
* Interface representing a created or an imported `Schedule`.
*/
export interface ISchedule extends IResource {
group?: IGroup;
}

export class Schedule extends Resource implements ISchedule {
group?: IGroup;
}
Loading
Loading