diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/cron.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/cron.ts deleted file mode 100644 index 9aa4369930a82..0000000000000 --- a/packages/@aws-cdk/aws-applicationautoscaling/lib/cron.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Helper class to generate Cron expressions - */ -export class Cron { - - /** - * Return a cron expression to run every day at a particular time - * - * The time is specified in UTC. - * - * @param hour The hour in UTC to schedule this action - * @param minute The minute in the our to schedule this action (defaults to 0) - */ - public static dailyUtc(hour: number, minute?: number) { - minute = minute || 0; - // 3rd and 5th expression are mutually exclusive, one of them should be ? - return `cron(${minute} ${hour} * * ?)`; - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/index.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/index.ts index 843ea1a4a1d53..83f294cd7e744 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/lib/index.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/index.ts @@ -2,7 +2,7 @@ export * from './applicationautoscaling.generated'; export * from './base-scalable-attribute'; -export * from './cron'; +export * from './schedule'; export * from './scalable-target'; export * from './step-scaling-policy'; export * from './step-scaling-action'; diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/scalable-target.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/scalable-target.ts index 4e5cad53bcb8d..23cd71a20e33a 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/lib/scalable-target.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/scalable-target.ts @@ -1,6 +1,7 @@ import iam = require('@aws-cdk/aws-iam'); import { Construct, IResource, Resource } from '@aws-cdk/cdk'; import { CfnScalableTarget } from './applicationautoscaling.generated'; +import { Schedule } from './schedule'; import { BasicStepScalingPolicyProps, StepScalingPolicy } from './step-scaling-policy'; import { BasicTargetTrackingScalingPolicyProps, TargetTrackingScalingPolicy } from './target-tracking-scaling-policy'; @@ -138,7 +139,7 @@ export class ScalableTarget extends Resource implements IScalableTarget { } this.actions.push({ scheduledActionName: id, - schedule: action.schedule, + schedule: action.schedule.expressionString, startTime: action.startTime, endTime: action.endTime, scalableTargetAction: { @@ -169,23 +170,8 @@ export class ScalableTarget extends Resource implements IScalableTarget { export interface ScalingSchedule { /** * When to perform this action. - * - * Support formats: - * - at(yyyy-mm-ddThh:mm:ss) - * - rate(value unit) - * - cron(fields) - * - * "At" expressions are useful for one-time schedules. Specify the time in - * UTC. - * - * For "rate" expressions, value is a positive integer, and unit is minute, - * minutes, hour, hours, day, or days. - * - * For more information about cron expressions, see https://en.wikipedia.org/wiki/Cron. - * - * @example rate(12 hours) */ - readonly schedule: string; + readonly schedule: Schedule; /** * When this scheduled action becomes active. diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/schedule.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/schedule.ts new file mode 100644 index 0000000000000..59c62f9a0f54b --- /dev/null +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/schedule.ts @@ -0,0 +1,157 @@ +/** + * Schedule for scheduled scaling actions + */ +export abstract class Schedule { + /** + * Construct a schedule from a literal schedule expression + * + * @param expression The expression to use. Must be in a format that Application AutoScaling will recognize + */ + public static expression(expression: string): Schedule { + return new LiteralSchedule(expression); + } + + /** + * Construct a schedule from an interval and a time unit + */ + public static rate(interval: number, unit: TimeUnit): Schedule { + const unitStr = interval !== 1 ? `${unit}s` : unit; + + return new LiteralSchedule(`rate(${interval} ${unitStr})`); + } + + /** + * Construct a Schedule from a moment in time + */ + public static at(moment: Date): Schedule { + return new LiteralSchedule(`at(${formatISO(moment)})`); + } + + /** + * Create a schedule from a set of cron fields + */ + public static cron(options: CronOptions): Schedule { + if (options.weekDay !== undefined && options.day !== undefined) { + throw new Error(`Cannot supply both 'day' and 'weekDay', use at most one`); + } + + const minute = fallback(options.minute, '*'); + const hour = fallback(options.hour, '*'); + const month = fallback(options.month, '*'); + const year = fallback(options.year, '*'); + + // Weekday defaults to '?' if not supplied. If it is supplied, day must become '?' + const day = fallback(options.day, options.weekDay !== undefined ? '?' : '*'); + const weekDay = fallback(options.weekDay, '?'); + + return new LiteralSchedule(`cron(${minute} ${hour} ${day} ${month} ${weekDay} ${year})`); + } + + /** + * Retrieve the expression for this schedule + */ + public abstract readonly expressionString: string; + + protected constructor() { + } +} + +/** + * What unit to interpret the rate in + */ +export enum TimeUnit { + /** + * The rate is in minutes + */ + Minute = 'minute', + + /** + * The rate is in hours + */ + Hour = 'hour', + + /** + * The rate is in days + */ + Day = 'day' +} + +/** + * Options to configure a cron expression + * + * All fields are strings so you can use complex expresions. Absence of + * a field implies '*' or '?', whichever one is appropriate. + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html#CronExpressions + */ +export interface CronOptions { + /** + * The minute to run this rule at + * + * @default - Every minute + */ + readonly minute?: string; + + /** + * The hour to run this rule at + * + * @default - Every hour + */ + readonly hour?: string; + + /** + * The day of the month to run this rule at + * + * @default - Every day of the month + */ + readonly day?: string; + + /** + * The month to run this rule at + * + * @default - Every month + */ + readonly month?: string; + + /** + * The year to run this rule at + * + * @default - Every year + */ + readonly year?: string; + + /** + * The day of the week to run this rule at + * + * @default - Any day of the week + */ + readonly weekDay?: string; +} + +class LiteralSchedule extends Schedule { + constructor(public readonly expressionString: string) { + super(); + } +} + +function fallback(x: T | undefined, def: T): T { + return x === undefined ? def : x; +} + +function formatISO(date?: Date) { + if (!date) { return undefined; } + + return date.getUTCFullYear() + + '-' + pad(date.getUTCMonth() + 1) + + '-' + pad(date.getUTCDate()) + + 'T' + pad(date.getUTCHours()) + + ':' + pad(date.getUTCMinutes()) + + ':' + pad(date.getUTCSeconds()); + + function pad(num: number) { + if (num < 10) { + return '0' + num; + } + return num; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-applicationautoscaling/test/test.cron.ts b/packages/@aws-cdk/aws-applicationautoscaling/test/test.cron.ts index 3697c9affe4f8..7c4968267695e 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/test/test.cron.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/test/test.cron.ts @@ -3,12 +3,12 @@ import appscaling = require('../lib'); export = { 'test utc cron, hour only'(test: Test) { - test.equals(appscaling.Cron.dailyUtc(18), 'cron(0 18 * * ?)'); + test.equals(appscaling.Schedule.cron({ hour: '18', minute: '0' }).expressionString, 'cron(0 18 * * ? *)'); test.done(); }, 'test utc cron, hour and minute'(test: Test) { - test.equals(appscaling.Cron.dailyUtc(18, 24), 'cron(24 18 * * ?)'); + test.equals(appscaling.Schedule.cron({ hour: '18', minute: '24' }).expressionString, 'cron(24 18 * * ? *)'); test.done(); } }; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-applicationautoscaling/test/test.scalable-target.ts b/packages/@aws-cdk/aws-applicationautoscaling/test/test.scalable-target.ts index 1afe47ee0031f..c798f5d2f5d7a 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/test/test.scalable-target.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/test/test.scalable-target.ts @@ -37,7 +37,7 @@ export = { // WHEN target.scaleOnSchedule('ScaleUp', { - schedule: 'rate(1 second)', + schedule: appscaling.Schedule.rate(1, appscaling.TimeUnit.Minute), maxCapacity: 50, minCapacity: 1, }); @@ -50,7 +50,7 @@ export = { MaxCapacity: 50, MinCapacity: 1 }, - Schedule: "rate(1 second)", + Schedule: "rate(1 minute)", ScheduledActionName: "ScaleUp" } ] diff --git a/packages/@aws-cdk/aws-autoscaling/lib/cron.ts b/packages/@aws-cdk/aws-autoscaling/lib/cron.ts deleted file mode 100644 index c5758dd6edf38..0000000000000 --- a/packages/@aws-cdk/aws-autoscaling/lib/cron.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Helper class to generate Cron expressions - */ -export class Cron { - - /** - * Return a cron expression to run every day at a particular time - * - * The time is specified in UTC. - * - * @param hour The hour in UTC to schedule this action - * @param minute The minute in the our to schedule this action (defaults to 0) - */ - public static dailyUtc(hour: number, minute?: number) { - minute = minute || 0; - // In this cron flavor, 3rd and 5th expression can both be * - return `${minute} ${hour} * * *`; - } -} diff --git a/packages/@aws-cdk/aws-autoscaling/lib/index.ts b/packages/@aws-cdk/aws-autoscaling/lib/index.ts index 834f1a16e0107..43e18b96f71c4 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/index.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/index.ts @@ -1,5 +1,5 @@ export * from './auto-scaling-group'; -export * from './cron'; +export * from './schedule'; export * from './lifecycle-hook'; export * from './lifecycle-hook-target'; export * from './scheduled-action'; diff --git a/packages/@aws-cdk/aws-autoscaling/lib/schedule.ts b/packages/@aws-cdk/aws-autoscaling/lib/schedule.ts new file mode 100644 index 0000000000000..551947825910b --- /dev/null +++ b/packages/@aws-cdk/aws-autoscaling/lib/schedule.ts @@ -0,0 +1,94 @@ +/** + * Schedule for scheduled scaling actions + */ +export abstract class Schedule { + /** + * Construct a schedule from a literal schedule expression + * + * @param expression The expression to use. Must be in a format that AutoScaling will recognize + * @see http://crontab.org/ + */ + public static expression(expression: string): Schedule { + return new LiteralSchedule(expression); + } + + /** + * Create a schedule from a set of cron fields + */ + public static cron(options: CronOptions): Schedule { + if (options.weekDay !== undefined && options.day !== undefined) { + throw new Error(`Cannot supply both 'day' and 'weekDay', use at most one`); + } + + const minute = fallback(options.minute, '*'); + const hour = fallback(options.hour, '*'); + const month = fallback(options.month, '*'); + const day = fallback(options.day, '*'); + const weekDay = fallback(options.weekDay, '*'); + + return new LiteralSchedule(`${minute} ${hour} ${day} ${month} ${weekDay}`); + } + + /** + * Retrieve the expression for this schedule + */ + public abstract readonly expressionString: string; + + protected constructor() { + } +} + +/** + * Options to configure a cron expression + * + * All fields are strings so you can use complex expresions. Absence of + * a field implies '*' or '?', whichever one is appropriate. + * + * @see http://crontab.org/ + */ +export interface CronOptions { + /** + * The minute to run this rule at + * + * @default - Every minute + */ + readonly minute?: string; + + /** + * The hour to run this rule at + * + * @default - Every hour + */ + readonly hour?: string; + + /** + * The day of the month to run this rule at + * + * @default - Every day of the month + */ + readonly day?: string; + + /** + * The month to run this rule at + * + * @default - Every month + */ + readonly month?: string; + + /** + * The day of the week to run this rule at + * + * @default - Any day of the week + */ + readonly weekDay?: string; +} + +class LiteralSchedule extends Schedule { + constructor(public readonly expressionString: string) { + super(); + } +} + +function fallback(x: T | undefined, def: T): T { + return x === undefined ? def : x; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-autoscaling/lib/scheduled-action.ts b/packages/@aws-cdk/aws-autoscaling/lib/scheduled-action.ts index a447fa61ebbc0..5e8443da9b6d5 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/scheduled-action.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/scheduled-action.ts @@ -1,6 +1,7 @@ import { Construct, Resource } from '@aws-cdk/cdk'; import { IAutoScalingGroup } from './auto-scaling-group'; import { CfnScheduledAction } from './autoscaling.generated'; +import { Schedule } from './schedule'; /** * Properties for a scheduled scaling action @@ -15,7 +16,7 @@ export interface BasicScheduledActionProps { * * @example 0 8 * * ? */ - readonly schedule: string; + readonly schedule: Schedule; /** * When this scheduled action becomes active. @@ -75,10 +76,6 @@ export interface ScheduledActionProps extends BasicScheduledActionProps { readonly autoScalingGroup: IAutoScalingGroup; } -const CRON_PART = '(\\*|\\?|[0-9]+)'; - -const CRON_EXPRESSION = new RegExp('^' + [CRON_PART, CRON_PART, CRON_PART, CRON_PART, CRON_PART].join('\\s+') + '$'); - /** * Define a scheduled scaling action */ @@ -86,10 +83,6 @@ export class ScheduledAction extends Resource { constructor(scope: Construct, id: string, props: ScheduledActionProps) { super(scope, id); - if (!CRON_EXPRESSION.exec(props.schedule)) { - throw new Error(`Input to ScheduledAction should be a cron expression, got: ${props.schedule}`); - } - if (props.minCapacity === undefined && props.maxCapacity === undefined && props.desiredCapacity === undefined) { throw new Error('At least one of minCapacity, maxCapacity, or desiredCapacity is required'); } @@ -101,7 +94,7 @@ export class ScheduledAction extends Resource { minSize: props.minCapacity, maxSize: props.maxCapacity, desiredCapacity: props.desiredCapacity, - recurrence: props.schedule, + recurrence: props.schedule.expressionString, }); } } diff --git a/packages/@aws-cdk/aws-autoscaling/test/integ.custom-scaling.ts b/packages/@aws-cdk/aws-autoscaling/test/integ.custom-scaling.ts index ab7d14ef6cdc0..fcadadbe0d545 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/integ.custom-scaling.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/integ.custom-scaling.ts @@ -17,12 +17,12 @@ const asg = new autoscaling.AutoScalingGroup(stack, 'Fleet', { }); asg.scaleOnSchedule('ScaleUpInTheMorning', { - schedule: autoscaling.Cron.dailyUtc(8), + schedule: autoscaling.Schedule.cron({ hour: '8', minute: '0' }), minCapacity: 5 }); asg.scaleOnSchedule('ScaleDownAtNight', { - schedule: autoscaling.Cron.dailyUtc(20), + schedule: autoscaling.Schedule.cron({ hour: '20', minute: '0' }), maxCapacity: 2 }); diff --git a/packages/@aws-cdk/aws-autoscaling/test/test.cron.ts b/packages/@aws-cdk/aws-autoscaling/test/test.cron.ts index 0ae1deb74a35f..88b4709d6363f 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/test.cron.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/test.cron.ts @@ -3,12 +3,12 @@ import autoscaling = require('../lib'); export = { 'test utc cron, hour only'(test: Test) { - test.equals(autoscaling.Cron.dailyUtc(18), '0 18 * * *'); + test.equals(autoscaling.Schedule.cron({ hour: '18', minute: '0' }).expressionString, '0 18 * * *'); test.done(); }, 'test utc cron, hour and minute'(test: Test) { - test.equals(autoscaling.Cron.dailyUtc(18, 24), '24 18 * * *'); + test.equals(autoscaling.Schedule.cron({ hour: '18', minute: '24' }).expressionString, '24 18 * * *'); test.done(); } }; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-autoscaling/test/test.scheduled-action.ts b/packages/@aws-cdk/aws-autoscaling/test/test.scheduled-action.ts index 79677691e55c4..49b576145f214 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/test.scheduled-action.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/test.scheduled-action.ts @@ -12,7 +12,7 @@ export = { // WHEN asg.scaleOnSchedule('ScaleOutInTheMorning', { - schedule: autoscaling.Cron.dailyUtc(8), + schedule: autoscaling.Schedule.cron({ hour: '8', minute: '0' }), minCapacity: 10, }); @@ -32,7 +32,7 @@ export = { // WHEN asg.scaleOnSchedule('ScaleOutInTheMorning', { - schedule: autoscaling.Cron.dailyUtc(8), + schedule: autoscaling.Schedule.cron({ hour: '8' }), startTime: new Date(Date.UTC(2033, 8, 10, 12, 0, 0)), // JavaScript's Date is a little silly. minCapacity: 11, }); @@ -52,7 +52,7 @@ export = { // WHEN asg.scaleOnSchedule('ScaleOutInTheMorning', { - schedule: autoscaling.Cron.dailyUtc(8), + schedule: autoscaling.Schedule.cron({ hour: '8' }), minCapacity: 10, }); diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.autoscaling.lit.expected.json b/packages/@aws-cdk/aws-dynamodb/test/integ.autoscaling.lit.expected.json index 9590004579403..ddc2c38d6ebdb 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/integ.autoscaling.lit.expected.json +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.autoscaling.lit.expected.json @@ -64,14 +64,14 @@ "ScalableTargetAction": { "MinCapacity": 20 }, - "Schedule": "cron(0 8 * * ?)", + "Schedule": "cron(0 8 * * ? *)", "ScheduledActionName": "ScaleUpInTheMorning" }, { "ScalableTargetAction": { "MaxCapacity": 20 }, - "Schedule": "cron(0 20 * * ?)", + "Schedule": "cron(0 20 * * ? *)", "ScheduledActionName": "ScaleDownAtNight" } ] diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.autoscaling.lit.ts b/packages/@aws-cdk/aws-dynamodb/test/integ.autoscaling.lit.ts index b7f4fae16e119..7173832bfbde7 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/integ.autoscaling.lit.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.autoscaling.lit.ts @@ -17,12 +17,12 @@ readScaling.scaleOnUtilization({ }); readScaling.scaleOnSchedule('ScaleUpInTheMorning', { - schedule: appscaling.Cron.dailyUtc(8), + schedule: appscaling.Schedule.cron({ hour: '8', minute: '0' }), minCapacity: 20, }); readScaling.scaleOnSchedule('ScaleDownAtNight', { - schedule: appscaling.Cron.dailyUtc(20), + schedule: appscaling.Schedule.cron({ hour: '20', minute: '0' }), maxCapacity: 20 }); /// !hide diff --git a/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts b/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts index b92d3057c17de..e10e2db488bcf 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts @@ -1,4 +1,5 @@ import { expect, haveResource } from '@aws-cdk/assert'; +import appscaling = require('@aws-cdk/aws-applicationautoscaling'); import iam = require('@aws-cdk/aws-iam'); import { ConstructNode, Stack, Tag } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; @@ -1101,7 +1102,7 @@ export = { // WHEN const scaling = table.autoScaleReadCapacity({ minCapacity: 1, maxCapacity: 100 }); scaling.scaleOnSchedule('SaveMoneyByNotScalingUp', { - schedule: 'cron(* * ? * * )', + schedule: appscaling.Schedule.cron({}), maxCapacity: 10 }); @@ -1110,7 +1111,7 @@ export = { ScheduledActions: [ { ScalableTargetAction: { "MaxCapacity": 10 }, - Schedule: "cron(* * ? * * )", + Schedule: "cron(* * * * ? *)", ScheduledActionName: "SaveMoneyByNotScalingUp" } ]