Skip to content

Commit 885d0c2

Browse files
chore(aws-events): add warning when minute is not defined (#19197)
Per issue #17881, the default value for CRON schedule configuration parameters are which runs every minute/hour/day. This change adds a warning to be emitted upon synthesis and deployment, if minute is empty, that the CRON job will run every minute. Closes #17881. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent afc70bd commit 885d0c2

File tree

15 files changed

+587
-271
lines changed

15 files changed

+587
-271
lines changed

packages/@aws-cdk/aws-applicationautoscaling/lib/scalable-target.ts

+4
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ export class ScalableTarget extends Resource implements IScalableTarget {
148148
if (action.minCapacity === undefined && action.maxCapacity === undefined) {
149149
throw new Error(`You must supply at least one of minCapacity or maxCapacity, got ${JSON.stringify(action)}`);
150150
}
151+
152+
// add a warning on synth when minute is not defined in a cron schedule
153+
action.schedule._bind(this);
154+
151155
this.actions.push({
152156
scheduledActionName: id,
153157
schedule: action.schedule.expressionString,

packages/@aws-cdk/aws-applicationautoscaling/lib/schedule.ts

+20-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Duration } from '@aws-cdk/core';
1+
import { Annotations, Duration } from '@aws-cdk/core';
2+
import { Construct } from 'constructs';
23

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

61-
return new LiteralSchedule(`cron(${minute} ${hour} ${day} ${month} ${weekDay} ${year})`);
62+
return new class extends Schedule {
63+
public readonly expressionString: string = `cron(${minute} ${hour} ${day} ${month} ${weekDay} ${year})`;
64+
public _bind(scope: Construct) {
65+
if (!options.minute) {
66+
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.');
67+
}
68+
return new LiteralSchedule(this.expressionString);
69+
}
70+
};
6271
}
6372

6473
/**
6574
* Retrieve the expression for this schedule
6675
*/
6776
public abstract readonly expressionString: string;
6877

69-
protected constructor() {
70-
}
78+
protected constructor() {}
79+
80+
/**
81+
*
82+
* @internal
83+
*/
84+
public abstract _bind(scope: Construct): void;
7185
}
7286

7387
/**
@@ -126,6 +140,8 @@ class LiteralSchedule extends Schedule {
126140
constructor(public readonly expressionString: string) {
127141
super();
128142
}
143+
144+
public _bind() {}
129145
}
130146

131147
function fallback<T>(x: T | undefined, def: T): T {

packages/@aws-cdk/aws-applicationautoscaling/test/scalable-target.test.ts

+48-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Match, Template } from '@aws-cdk/assertions';
1+
import { Annotations, Match, Template } from '@aws-cdk/assertions';
22
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
33
import * as iam from '@aws-cdk/aws-iam';
44
import * as cdk from '@aws-cdk/core';
@@ -79,6 +79,53 @@ describe('scalable target', () => {
7979
});
8080
});
8181

82+
test('scheduled scaling shows warning when minute is not defined in cron', () => {
83+
// GIVEN
84+
const stack = new cdk.Stack();
85+
const target = createScalableTarget(stack);
86+
87+
// WHEN
88+
target.scaleOnSchedule('ScaleUp', {
89+
schedule: appscaling.Schedule.cron({
90+
hour: '8',
91+
day: '1',
92+
}),
93+
maxCapacity: 50,
94+
minCapacity: 1,
95+
});
96+
97+
// THEN
98+
expect(target.node.metadataEntry).toEqual([{
99+
type: 'aws:cdk:warning',
100+
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.',
101+
trace: undefined,
102+
}]);
103+
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.");
104+
105+
});
106+
107+
test('scheduled scaling shows no warning when minute is * in cron', () => {
108+
// GIVEN
109+
const stack = new cdk.Stack();
110+
const target = createScalableTarget(stack);
111+
112+
// WHEN
113+
target.scaleOnSchedule('ScaleUp', {
114+
schedule: appscaling.Schedule.cron({
115+
hour: '8',
116+
day: '1',
117+
minute: '*',
118+
}),
119+
maxCapacity: 50,
120+
minCapacity: 1,
121+
});
122+
123+
// THEN
124+
expect(target.node.metadataEntry).toEqual([]);
125+
const annotations = Annotations.fromStack(stack).findWarning('*', Match.anyValue());
126+
expect(annotations.length).toBe(0);
127+
});
128+
82129
test('step scaling on MathExpression', () => {
83130
// GIVEN
84131
const stack = new cdk.Stack();

packages/@aws-cdk/aws-autoscaling/lib/schedule.ts

+21-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { Annotations } from '@aws-cdk/core';
2+
import { Construct } from 'constructs';
3+
14
/**
25
* Schedule for scheduled scaling actions
36
*/
@@ -26,16 +29,29 @@ export abstract class Schedule {
2629
const day = fallback(options.day, '*');
2730
const weekDay = fallback(options.weekDay, '*');
2831

29-
return new LiteralSchedule(`${minute} ${hour} ${day} ${month} ${weekDay}`);
32+
return new class extends Schedule {
33+
public readonly expressionString: string = `${minute} ${hour} ${day} ${month} ${weekDay}`;
34+
public _bind(scope: Construct) {
35+
if (!options.minute) {
36+
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.');
37+
}
38+
return new LiteralSchedule(this.expressionString);
39+
}
40+
};
3041
}
3142

3243
/**
3344
* Retrieve the expression for this schedule
3445
*/
3546
public abstract readonly expressionString: string;
3647

37-
protected constructor() {
38-
}
48+
protected constructor() {}
49+
50+
/**
51+
*
52+
* @internal
53+
*/
54+
public abstract _bind(scope: Construct): void;
3955
}
4056

4157
/**
@@ -87,6 +103,8 @@ class LiteralSchedule extends Schedule {
87103
constructor(public readonly expressionString: string) {
88104
super();
89105
}
106+
107+
public _bind(): void {}
90108
}
91109

92110
function fallback<T>(x: T | undefined, def: T): T {

packages/@aws-cdk/aws-autoscaling/lib/scheduled-action.ts

+3
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ export class ScheduledAction extends Resource {
9797
throw new Error('At least one of minCapacity, maxCapacity, or desiredCapacity is required');
9898
}
9999

100+
// add a warning on synth when minute is not defined in a cron schedule
101+
props.schedule._bind(this);
102+
100103
new CfnScheduledAction(this, 'Resource', {
101104
autoScalingGroupName: props.autoScalingGroup.autoScalingGroupName,
102105
startTime: formatISO(props.startTime),

packages/@aws-cdk/aws-autoscaling/test/scheduled-action.test.ts

+35-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Template } from '@aws-cdk/assertions';
1+
import { Annotations, Match, Template } from '@aws-cdk/assertions';
22
import * as ec2 from '@aws-cdk/aws-ec2';
33
import { describeDeprecated } from '@aws-cdk/cdk-build-tools';
44
import * as cdk from '@aws-cdk/core';
@@ -120,6 +120,40 @@ describeDeprecated('scheduled action', () => {
120120
},
121121
});
122122
});
123+
124+
test('scheduled scaling shows warning when minute is not defined in cron', () => {
125+
// GIVEN
126+
const stack = new cdk.Stack();
127+
const asg = makeAutoScalingGroup(stack);
128+
129+
// WHEN
130+
asg.scaleOnSchedule('ScaleOutInTheMorning', {
131+
schedule: autoscaling.Schedule.cron({ hour: '8' }),
132+
minCapacity: 10,
133+
});
134+
135+
// THEN
136+
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.");
137+
});
138+
139+
test('scheduled scaling shows no warning when minute is * in cron', () => {
140+
// GIVEN
141+
const stack = new cdk.Stack();
142+
const asg = makeAutoScalingGroup(stack);
143+
144+
// WHEN
145+
asg.scaleOnSchedule('ScaleOutInTheMorning', {
146+
schedule: autoscaling.Schedule.cron({
147+
hour: '8',
148+
minute: '*',
149+
}),
150+
minCapacity: 10,
151+
});
152+
153+
// THEN
154+
const annotations = Annotations.fromStack(stack).findWarning('*', Match.anyValue());
155+
expect(annotations.length).toBe(0);
156+
});
123157
});
124158

125159
function makeAutoScalingGroup(scope: constructs.Construct) {

packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts

+42-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Match, Template } from '@aws-cdk/assertions';
1+
import { Annotations, Match, Template } from '@aws-cdk/assertions';
22
import * as appscaling from '@aws-cdk/aws-applicationautoscaling';
33
import * as iam from '@aws-cdk/aws-iam';
44
import * as kinesis from '@aws-cdk/aws-kinesis';
@@ -1622,6 +1622,47 @@ test('can autoscale on a schedule', () => {
16221622
});
16231623
});
16241624

1625+
test('scheduled scaling shows warning when minute is not defined in cron', () => {
1626+
// GIVEN
1627+
const stack = new Stack();
1628+
const table = new Table(stack, CONSTRUCT_NAME, {
1629+
readCapacity: 42,
1630+
writeCapacity: 1337,
1631+
partitionKey: { name: 'Hash', type: AttributeType.STRING },
1632+
});
1633+
1634+
// WHEN
1635+
const scaling = table.autoScaleReadCapacity({ minCapacity: 1, maxCapacity: 100 });
1636+
scaling.scaleOnSchedule('SaveMoneyByNotScalingUp', {
1637+
schedule: appscaling.Schedule.cron({}),
1638+
maxCapacity: 10,
1639+
});
1640+
1641+
// THEN
1642+
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.");
1643+
});
1644+
1645+
test('scheduled scaling shows no warning when minute is * in cron', () => {
1646+
// GIVEN
1647+
const stack = new Stack();
1648+
const table = new Table(stack, CONSTRUCT_NAME, {
1649+
readCapacity: 42,
1650+
writeCapacity: 1337,
1651+
partitionKey: { name: 'Hash', type: AttributeType.STRING },
1652+
});
1653+
1654+
// WHEN
1655+
const scaling = table.autoScaleReadCapacity({ minCapacity: 1, maxCapacity: 100 });
1656+
scaling.scaleOnSchedule('SaveMoneyByNotScalingUp', {
1657+
schedule: appscaling.Schedule.cron({ minute: '*' }),
1658+
maxCapacity: 10,
1659+
});
1660+
1661+
// THEN
1662+
const annotations = Annotations.fromStack(stack).findWarning('*', Match.anyValue());
1663+
expect(annotations.length).toBe(0);
1664+
});
1665+
16251666
describe('metrics', () => {
16261667
test('Can use metricConsumedReadCapacityUnits on a Dynamodb Table', () => {
16271668
// GIVEN

packages/@aws-cdk/aws-ecs-patterns/lib/base/scheduled-task-base.ts

+3
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@ export abstract class ScheduledTaskBase extends CoreConstruct {
170170
ruleName: props.ruleName,
171171
enabled: props.enabled,
172172
});
173+
174+
// add a warning on synth when minute is not defined in a cron schedule
175+
props.schedule._bind(scope);
173176
}
174177

175178
/**

packages/@aws-cdk/aws-ecs-patterns/test/ec2/scheduled-ecs-task.test.ts

+46-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Template } from '@aws-cdk/assertions';
1+
import { Annotations, Match, Template } from '@aws-cdk/assertions';
22
import { AutoScalingGroup } from '@aws-cdk/aws-autoscaling';
33
import * as ec2 from '@aws-cdk/aws-ec2';
44
import { MachineImage } from '@aws-cdk/aws-ec2';
@@ -347,3 +347,48 @@ test('Scheduled Ec2 Task - exposes ECS Task', () => {
347347
// THEN
348348
expect(scheduledEc2Task.task).toBeDefined();
349349
});
350+
351+
test('Scheduled Ec2 Task shows warning when minute is not defined in cron', () => {
352+
// GIVEN
353+
const stack = new cdk.Stack();
354+
const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 });
355+
const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc });
356+
357+
new ScheduledEc2Task(stack, 'ScheduledEc2Task', {
358+
cluster,
359+
scheduledEc2TaskImageOptions: {
360+
image: ecs.ContainerImage.fromRegistry('henk'),
361+
memoryLimitMiB: 512,
362+
},
363+
schedule: events.Schedule.cron({}),
364+
});
365+
366+
// THEN
367+
expect(stack.node.metadataEntry).toEqual([{
368+
type: 'aws:cdk:warning',
369+
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.',
370+
trace: undefined,
371+
}]);
372+
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.");
373+
});
374+
375+
test('Scheduled Ec2 Task shows no warning when minute is * in cron', () => {
376+
// GIVEN
377+
const stack = new cdk.Stack();
378+
const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 });
379+
const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc });
380+
381+
new ScheduledEc2Task(stack, 'ScheduledEc2Task', {
382+
cluster,
383+
scheduledEc2TaskImageOptions: {
384+
image: ecs.ContainerImage.fromRegistry('henk'),
385+
memoryLimitMiB: 512,
386+
},
387+
schedule: events.Schedule.cron({ minute: '*' }),
388+
});
389+
390+
// THEN
391+
expect(stack.node.metadataEntry).toEqual([]);
392+
const annotations = Annotations.fromStack(stack).findWarning('*', Match.anyValue());
393+
expect(annotations.length).toBe(0);
394+
});

packages/@aws-cdk/aws-ecs-patterns/test/fargate/scheduled-fargate-task.test.ts

+46-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Match, Template } from '@aws-cdk/assertions';
1+
import { Annotations, Match, Template } from '@aws-cdk/assertions';
22
import * as ec2 from '@aws-cdk/aws-ec2';
33
import * as ecs from '@aws-cdk/aws-ecs';
44
import * as events from '@aws-cdk/aws-events';
@@ -410,3 +410,48 @@ test('Scheduled Fargate Task - exposes ECS Task', () => {
410410
// THEN
411411
expect(scheduledFargateTask.task).toBeDefined();
412412
});
413+
414+
test('Scheduled Fargate Task shows warning when minute is not defined in cron', () => {
415+
// GIVEN
416+
const stack = new cdk.Stack();
417+
const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 });
418+
const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc });
419+
420+
new ScheduledFargateTask(stack, 'ScheduledFargateTask', {
421+
cluster,
422+
scheduledFargateTaskImageOptions: {
423+
image: ecs.ContainerImage.fromRegistry('henk'),
424+
memoryLimitMiB: 512,
425+
},
426+
schedule: events.Schedule.cron({}),
427+
});
428+
429+
// THEN
430+
expect(stack.node.metadataEntry).toEqual([{
431+
type: 'aws:cdk:warning',
432+
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.',
433+
trace: undefined,
434+
}]);
435+
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.");
436+
});
437+
438+
test('Scheduled Fargate Task shows no warning when minute is * in cron', () => {
439+
// GIVEN
440+
const stack = new cdk.Stack();
441+
const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 });
442+
const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc });
443+
444+
new ScheduledFargateTask(stack, 'ScheduledFargateTask', {
445+
cluster,
446+
scheduledFargateTaskImageOptions: {
447+
image: ecs.ContainerImage.fromRegistry('henk'),
448+
memoryLimitMiB: 512,
449+
},
450+
schedule: events.Schedule.cron({ minute: '*' }),
451+
});
452+
453+
// THEN
454+
expect(stack.node.metadataEntry).toEqual([]);
455+
const annotations = Annotations.fromStack(stack).findWarning('*', Match.anyValue());
456+
expect(annotations.length).toBe(0);
457+
});

0 commit comments

Comments
 (0)