Skip to content

Commit

Permalink
feat(scheduler): ScheduleTargetInput (#25663)
Browse files Browse the repository at this point in the history
This PR contains implementation of ScheduleTargetInput. While a schedule is the main resource in Amazon EventBridge Scheduler, this PR adds ScheduleTargetInput on which ScheduleTargetBase depends.

Every Schedule has a target that determines what extra information is sent to the target when the schedule is triggered. Also 4 ContextAttributes can be used that will be resolved at trigger-time.

To be able to create sensible unit tests, also the a start is made to add the `Schedule` and the `LambdaInvoke` target as described in the RFC.

Implementation is based on RFC: https://github.com/aws/aws-cdk-rfcs/blob/master/text/0474-event-bridge-scheduler-l2.md

Also added a small fix to 2 of the unit tests of the previous PR for this module.

Advances #23394

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
Jacco authored Jun 29, 2023
1 parent 1ccfc78 commit bc9f3de
Show file tree
Hide file tree
Showing 9 changed files with 425 additions and 4 deletions.
33 changes: 31 additions & 2 deletions packages/@aws-cdk/aws-scheduler-alpha/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,15 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aw
## Defining a schedule

TODO: Schedule 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)
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)

Only an L2 class is created that wraps the L1 class and handles the following properties:

- schedule
- target (only LambdaInvoke is supported for now)
- flexibleTimeWindow will be set to `{ mode: 'OFF' }`

### Schedule Expressions

Expand Down Expand Up @@ -95,10 +103,31 @@ TODO: Group is not yet implemented. See section in [L2 Event Bridge Scheduler RF

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.

### Input

TODO: Target Input 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)
Target can be invoked with a custom input. Class `ScheduleTargetInput` supports free form text input and JSON-formatted object input:

```ts
const input = ScheduleTargetInput.fromObject({
'QueueName': 'MyQueue'
});
```

You can include context attributes in your target payload. EventBridge Scheduler will replace each keyword with
its respective value and deliver it to the target. See
[full list of supported context attributes](https://docs.aws.amazon.com/scheduler/latest/UserGuide/managing-schedule-context-attributes.html):

1. `ContextAttribute.scheduleArn()` – The ARN of the schedule.
2. `ContextAttribute.scheduledTime()` – The time you specified for the schedule to invoke its target, for example, 2022-03-22T18:59:43Z.
3. `ContextAttribute.executionId()` – The unique ID that EventBridge Scheduler assigns for each attempted invocation of a target, for example, d32c5kddcf5bb8c3.
4. `ContextAttribute.attemptNumber()` – A counter that identifies the attempt number for the current invocation, for example, 1.

```ts
const text = `Attempt number: ${ContextAttribute.attemptNumber}`;
const input = ScheduleTargetInput.fromText(text);
```

### Specifying Execution Role

Expand Down
4 changes: 3 additions & 1 deletion packages/@aws-cdk/aws-scheduler-alpha/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './schedule-expression';
export * from './schedule-expression';
export * from './input';
export * from './schedule';
122 changes: 122 additions & 0 deletions packages/@aws-cdk/aws-scheduler-alpha/lib/input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { DefaultTokenResolver, IResolveContext, Stack, StringConcat, Token, Tokenization } from 'aws-cdk-lib';
import { ISchedule } from './schedule';

/**
* The text, or well-formed JSON, passed to the target of the schedule.
*/
export abstract class ScheduleTargetInput {
/**
* Pass text to the target, it is possible to embed `ContextAttributes`
* that will be resolved to actual values while the CloudFormation is
* deployed or cdk Tokens that will be resolved when the CloudFormation
* templates are generated by CDK.
*
* The target input value will be a single string that you pass.
* For passing complex values like JSON object to a target use method
* `ScheduleTargetInput.fromObject()` instead.
*
* @param text Text to use as the input for the target
*/
public static fromText(text: string): ScheduleTargetInput {
return new FieldAwareEventInput(text);
}

/**
* Pass a JSON object to the target, it is possible to embed `ContextAttributes` and other
* cdk references.
*
* @param obj object to use to convert to JSON to use as input for the target
*/
public static fromObject(obj: any): ScheduleTargetInput {
return new FieldAwareEventInput(obj);
}

protected constructor() {
}

/**
* Return the input properties for this input object
*/
public abstract bind(schedule: ISchedule): string;
}

class FieldAwareEventInput extends ScheduleTargetInput {
constructor(private readonly input: any) {
super();
}

public bind(schedule: ISchedule): string {
class Replacer extends DefaultTokenResolver {
constructor() {
super(new StringConcat());
}

public resolveToken(t: Token, _context: IResolveContext) {
return Token.asString(t);
}
}

const stack = Stack.of(schedule);
return stack.toJsonString(Tokenization.resolve(this.input, {
scope: schedule,
resolver: new Replacer(),
}));
}
}

/**
* Represents a field in the event pattern
*
* @see https://docs.aws.amazon.com/scheduler/latest/UserGuide/managing-schedule-context-attributes.html
*/
export class ContextAttribute {
/**
* The ARN of the schedule.
*/
public static get scheduleArn(): string {
return this.fromName('schedule-arn');
}

/**
* The time you specified for the schedule to invoke its target, for example,
* 2022-03-22T18:59:43Z.
*/
public static get scheduledTime(): string {
return this.fromName('scheduled-time');
}

/**
* The unique ID that EventBridge Scheduler assigns for each attempted invocation of
* a target, for example, d32c5kddcf5bb8c3.
*/
public static get executionId(): string {
return this.fromName('execution-id');
}

/**
* A counter that identifies the attempt number for the current invocation, for
* example, 1.
*/
public static get attemptNumber(): string {
return this.fromName('attempt-number');
}

/**
* Escape hatch for other ContextAttribute that might be resolved in future.
*
* @param name - name will replace xxx in <aws.scheduler.xxx>
*/
public static fromName(name: string): string {
return new ContextAttribute(name).toString();
}

private constructor(public readonly name: string) {
}

/**
* Convert the path to the field in the event pattern to JSON
*/
public toString() {
return `<aws.scheduler.${this.name}>`;
}
}
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-scheduler-alpha/lib/private/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './schedule';
export * from './targets';
57 changes: 57 additions & 0 deletions packages/@aws-cdk/aws-scheduler-alpha/lib/private/schedule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Resource } from 'aws-cdk-lib';
import { CfnSchedule } from 'aws-cdk-lib/aws-scheduler';
import { Construct } from 'constructs';
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;
}

/**
* An EventBridge Schedule
*/
export class Schedule extends Resource implements ISchedule {
constructor(scope: Construct, id: string, props: ScheduleProps) {
super(scope, id);

new CfnSchedule(this, 'Resource', {
flexibleTimeWindow: { mode: 'OFF' },
scheduleExpression: props.schedule.expressionString,
scheduleExpressionTimezone: props.schedule.timeZone?.timezoneName,
target: {
...props.target.bind(this),
},
});
}
}
58 changes: 58 additions & 0 deletions packages/@aws-cdk/aws-scheduler-alpha/lib/private/targets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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);
}
}
}
8 changes: 8 additions & 0 deletions packages/@aws-cdk/aws-scheduler-alpha/lib/schedule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { IResource } from 'aws-cdk-lib';

/**
* Interface representing a created or an imported `Schedule`.
*/
export interface ISchedule extends IResource {

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ 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 { App, Stack, TimeZone, Duration } from 'aws-cdk-lib';
import { ScheduleExpression } from '@aws-cdk/aws-scheduler-alpha';
import { ScheduleExpression, ScheduleTargetInput, ContextAttribute } from '@aws-cdk/aws-scheduler-alpha';

class Fixture extends cdk.Stack {
constructor(scope: Construct, id: string) {
Expand Down
Loading

0 comments on commit bc9f3de

Please sign in to comment.