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

chore(aws-events): add warning when minute is not defined #19197

Merged
merged 3 commits into from
Mar 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ export class ScalableTarget extends Resource implements IScalableTarget {
if (action.minCapacity === undefined && action.maxCapacity === undefined) {
throw new Error(`You must supply at least one of minCapacity or maxCapacity, got ${JSON.stringify(action)}`);
}

// add a warning on synth when minute is not defined in a cron schedule
action.schedule._bind(this);

this.actions.push({
scheduledActionName: id,
schedule: action.schedule.expressionString,
Expand Down
24 changes: 20 additions & 4 deletions packages/@aws-cdk/aws-applicationautoscaling/lib/schedule.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Duration } from '@aws-cdk/core';
import { Annotations, Duration } from '@aws-cdk/core';
import { Construct } from 'constructs';

/**
* Schedule for scheduled scaling actions
Expand Down Expand Up @@ -58,16 +59,29 @@ export abstract class Schedule {
const day = fallback(options.day, options.weekDay !== undefined ? '?' : '*');
const weekDay = fallback(options.weekDay, '?');

return new LiteralSchedule(`cron(${minute} ${hour} ${day} ${month} ${weekDay} ${year})`);
return new class extends Schedule {
public readonly expressionString: string = `cron(${minute} ${hour} ${day} ${month} ${weekDay} ${year})`;
public _bind(scope: Construct) {
if (!options.minute) {
Annotations.of(scope).addWarning('cron: If you don\'t pass \'minute\', by default the event runs every minute. Pass \'minute: \'*\'\' if that\'s what you intend, or \'minute: 0\' to run once per hour instead.');
}
return new LiteralSchedule(this.expressionString);
}
};
}

/**
* Retrieve the expression for this schedule
*/
public abstract readonly expressionString: string;

protected constructor() {
}
protected constructor() {}

/**
*
* @internal
*/
public abstract _bind(scope: Construct): void;
}

/**
Expand Down Expand Up @@ -126,6 +140,8 @@ class LiteralSchedule extends Schedule {
constructor(public readonly expressionString: string) {
super();
}

public _bind() {}
}

function fallback<T>(x: T | undefined, def: T): T {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Match, Template } from '@aws-cdk/assertions';
import { Annotations, Match, Template } from '@aws-cdk/assertions';
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
import * as iam from '@aws-cdk/aws-iam';
import * as cdk from '@aws-cdk/core';
Expand Down Expand Up @@ -79,6 +79,53 @@ describe('scalable target', () => {
});
});

test('scheduled scaling shows warning when minute is not defined in cron', () => {
// GIVEN
const stack = new cdk.Stack();
const target = createScalableTarget(stack);

// WHEN
target.scaleOnSchedule('ScaleUp', {
schedule: appscaling.Schedule.cron({
hour: '8',
day: '1',
}),
maxCapacity: 50,
minCapacity: 1,
});

// THEN
expect(target.node.metadataEntry).toEqual([{
type: 'aws:cdk:warning',
data: 'cron: If you don\'t pass \'minute\', by default the event runs every minute. Pass \'minute: \'*\'\' if that\'s what you intend, or \'minute: 0\' to run once per hour instead.',
trace: undefined,
}]);
Annotations.fromStack(stack).hasWarning('/Default/Target', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead.");

});

test('scheduled scaling shows no warning when minute is * in cron', () => {
// GIVEN
const stack = new cdk.Stack();
const target = createScalableTarget(stack);

// WHEN
target.scaleOnSchedule('ScaleUp', {
schedule: appscaling.Schedule.cron({
hour: '8',
day: '1',
minute: '*',
}),
maxCapacity: 50,
minCapacity: 1,
});

// THEN
expect(target.node.metadataEntry).toEqual([]);
const annotations = Annotations.fromStack(stack).findWarning('*', Match.anyValue());
expect(annotations.length).toBe(0);
});

test('step scaling on MathExpression', () => {
// GIVEN
const stack = new cdk.Stack();
Expand Down
24 changes: 21 additions & 3 deletions packages/@aws-cdk/aws-autoscaling/lib/schedule.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { Annotations } from '@aws-cdk/core';
import { Construct } from 'constructs';

/**
* Schedule for scheduled scaling actions
*/
Expand Down Expand Up @@ -26,16 +29,29 @@ export abstract class Schedule {
const day = fallback(options.day, '*');
const weekDay = fallback(options.weekDay, '*');

return new LiteralSchedule(`${minute} ${hour} ${day} ${month} ${weekDay}`);
return new class extends Schedule {
public readonly expressionString: string = `${minute} ${hour} ${day} ${month} ${weekDay}`;
public _bind(scope: Construct) {
if (!options.minute) {
Annotations.of(scope).addWarning('cron: If you don\'t pass \'minute\', by default the event runs every minute. Pass \'minute: \'*\'\' if that\'s what you intend, or \'minute: 0\' to run once per hour instead.');
}
return new LiteralSchedule(this.expressionString);
}
};
}

/**
* Retrieve the expression for this schedule
*/
public abstract readonly expressionString: string;

protected constructor() {
}
protected constructor() {}

/**
*
* @internal
*/
public abstract _bind(scope: Construct): void;
}

/**
Expand Down Expand Up @@ -87,6 +103,8 @@ class LiteralSchedule extends Schedule {
constructor(public readonly expressionString: string) {
super();
}

public _bind(): void {}
}

function fallback<T>(x: T | undefined, def: T): T {
Expand Down
3 changes: 3 additions & 0 deletions packages/@aws-cdk/aws-autoscaling/lib/scheduled-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ export class ScheduledAction extends Resource {
throw new Error('At least one of minCapacity, maxCapacity, or desiredCapacity is required');
}

// add a warning on synth when minute is not defined in a cron schedule
props.schedule._bind(this);

new CfnScheduledAction(this, 'Resource', {
autoScalingGroupName: props.autoScalingGroup.autoScalingGroupName,
startTime: formatISO(props.startTime),
Expand Down
36 changes: 35 additions & 1 deletion packages/@aws-cdk/aws-autoscaling/test/scheduled-action.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Template } from '@aws-cdk/assertions';
import { Annotations, Match, Template } from '@aws-cdk/assertions';
import * as ec2 from '@aws-cdk/aws-ec2';
import { describeDeprecated } from '@aws-cdk/cdk-build-tools';
import * as cdk from '@aws-cdk/core';
Expand Down Expand Up @@ -120,6 +120,40 @@ describeDeprecated('scheduled action', () => {
},
});
});

test('scheduled scaling shows warning when minute is not defined in cron', () => {
// GIVEN
const stack = new cdk.Stack();
const asg = makeAutoScalingGroup(stack);

// WHEN
asg.scaleOnSchedule('ScaleOutInTheMorning', {
schedule: autoscaling.Schedule.cron({ hour: '8' }),
minCapacity: 10,
});

// THEN
Annotations.fromStack(stack).hasWarning('/Default/ASG/ScheduledActionScaleOutInTheMorning', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead.");
});

test('scheduled scaling shows no warning when minute is * in cron', () => {
// GIVEN
const stack = new cdk.Stack();
const asg = makeAutoScalingGroup(stack);

// WHEN
asg.scaleOnSchedule('ScaleOutInTheMorning', {
schedule: autoscaling.Schedule.cron({
hour: '8',
minute: '*',
}),
minCapacity: 10,
});

// THEN
const annotations = Annotations.fromStack(stack).findWarning('*', Match.anyValue());
expect(annotations.length).toBe(0);
});
});

function makeAutoScalingGroup(scope: constructs.Construct) {
Expand Down
43 changes: 42 additions & 1 deletion packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Match, Template } from '@aws-cdk/assertions';
import { Annotations, Match, Template } from '@aws-cdk/assertions';
import * as appscaling from '@aws-cdk/aws-applicationautoscaling';
import * as iam from '@aws-cdk/aws-iam';
import * as kinesis from '@aws-cdk/aws-kinesis';
Expand Down Expand Up @@ -1622,6 +1622,47 @@ test('can autoscale on a schedule', () => {
});
});

test('scheduled scaling shows warning when minute is not defined in cron', () => {
// GIVEN
const stack = new Stack();
const table = new Table(stack, CONSTRUCT_NAME, {
readCapacity: 42,
writeCapacity: 1337,
partitionKey: { name: 'Hash', type: AttributeType.STRING },
});

// WHEN
const scaling = table.autoScaleReadCapacity({ minCapacity: 1, maxCapacity: 100 });
scaling.scaleOnSchedule('SaveMoneyByNotScalingUp', {
schedule: appscaling.Schedule.cron({}),
maxCapacity: 10,
});

// THEN
Annotations.fromStack(stack).hasWarning('/Default/MyTable/ReadScaling/Target', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead.");
});

test('scheduled scaling shows no warning when minute is * in cron', () => {
// GIVEN
const stack = new Stack();
const table = new Table(stack, CONSTRUCT_NAME, {
readCapacity: 42,
writeCapacity: 1337,
partitionKey: { name: 'Hash', type: AttributeType.STRING },
});

// WHEN
const scaling = table.autoScaleReadCapacity({ minCapacity: 1, maxCapacity: 100 });
scaling.scaleOnSchedule('SaveMoneyByNotScalingUp', {
schedule: appscaling.Schedule.cron({ minute: '*' }),
maxCapacity: 10,
});

// THEN
const annotations = Annotations.fromStack(stack).findWarning('*', Match.anyValue());
expect(annotations.length).toBe(0);
});

describe('metrics', () => {
test('Can use metricConsumedReadCapacityUnits on a Dynamodb Table', () => {
// GIVEN
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ export abstract class ScheduledTaskBase extends CoreConstruct {
ruleName: props.ruleName,
enabled: props.enabled,
});

// add a warning on synth when minute is not defined in a cron schedule
props.schedule._bind(scope);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Template } from '@aws-cdk/assertions';
import { Annotations, Match, Template } from '@aws-cdk/assertions';
import { AutoScalingGroup } from '@aws-cdk/aws-autoscaling';
import * as ec2 from '@aws-cdk/aws-ec2';
import { MachineImage } from '@aws-cdk/aws-ec2';
Expand Down Expand Up @@ -347,3 +347,48 @@ test('Scheduled Ec2 Task - exposes ECS Task', () => {
// THEN
expect(scheduledEc2Task.task).toBeDefined();
});

test('Scheduled Ec2 Task shows warning when minute is not defined in cron', () => {
// GIVEN
const stack = new cdk.Stack();
const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 });
const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc });

new ScheduledEc2Task(stack, 'ScheduledEc2Task', {
cluster,
scheduledEc2TaskImageOptions: {
image: ecs.ContainerImage.fromRegistry('henk'),
memoryLimitMiB: 512,
},
schedule: events.Schedule.cron({}),
});

// THEN
expect(stack.node.metadataEntry).toEqual([{
type: 'aws:cdk:warning',
data: 'cron: If you don\'t pass \'minute\', by default the event runs every minute. Pass \'minute: \'*\'\' if that\'s what you intend, or \'minute: 0\' to run once per hour instead.',
trace: undefined,
}]);
Annotations.fromStack(stack).hasWarning('/Default', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead.");
});

test('Scheduled Ec2 Task shows no warning when minute is * in cron', () => {
// GIVEN
const stack = new cdk.Stack();
const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 });
const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc });

new ScheduledEc2Task(stack, 'ScheduledEc2Task', {
cluster,
scheduledEc2TaskImageOptions: {
image: ecs.ContainerImage.fromRegistry('henk'),
memoryLimitMiB: 512,
},
schedule: events.Schedule.cron({ minute: '*' }),
});

// THEN
expect(stack.node.metadataEntry).toEqual([]);
const annotations = Annotations.fromStack(stack).findWarning('*', Match.anyValue());
expect(annotations.length).toBe(0);
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Match, Template } from '@aws-cdk/assertions';
import { Annotations, Match, Template } from '@aws-cdk/assertions';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as ecs from '@aws-cdk/aws-ecs';
import * as events from '@aws-cdk/aws-events';
Expand Down Expand Up @@ -410,3 +410,48 @@ test('Scheduled Fargate Task - exposes ECS Task', () => {
// THEN
expect(scheduledFargateTask.task).toBeDefined();
});

test('Scheduled Fargate Task shows warning when minute is not defined in cron', () => {
// GIVEN
const stack = new cdk.Stack();
const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 });
const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc });

new ScheduledFargateTask(stack, 'ScheduledFargateTask', {
cluster,
scheduledFargateTaskImageOptions: {
image: ecs.ContainerImage.fromRegistry('henk'),
memoryLimitMiB: 512,
},
schedule: events.Schedule.cron({}),
});

// THEN
expect(stack.node.metadataEntry).toEqual([{
type: 'aws:cdk:warning',
data: 'cron: If you don\'t pass \'minute\', by default the event runs every minute. Pass \'minute: \'*\'\' if that\'s what you intend, or \'minute: 0\' to run once per hour instead.',
trace: undefined,
}]);
Annotations.fromStack(stack).hasWarning('/Default', "cron: If you don't pass 'minute', by default the event runs every minute. Pass 'minute: '*'' if that's what you intend, or 'minute: 0' to run once per hour instead.");
});

test('Scheduled Fargate Task shows no warning when minute is * in cron', () => {
// GIVEN
const stack = new cdk.Stack();
const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 });
const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc });

new ScheduledFargateTask(stack, 'ScheduledFargateTask', {
cluster,
scheduledFargateTaskImageOptions: {
image: ecs.ContainerImage.fromRegistry('henk'),
memoryLimitMiB: 512,
},
schedule: events.Schedule.cron({ minute: '*' }),
});

// THEN
expect(stack.node.metadataEntry).toEqual([]);
const annotations = Annotations.fromStack(stack).findWarning('*', Match.anyValue());
expect(annotations.length).toBe(0);
});
Loading