diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/schedule.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/schedule.ts index c576e17453557..cdeb00be9a426 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/lib/schedule.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/schedule.ts @@ -17,6 +17,13 @@ export abstract class Schedule { * Construct a schedule from an interval and a time unit */ public static rate(duration: Duration): Schedule { + const validDurationUnit = ['minute', 'minutes', 'hour', 'hours', 'day', 'days']; + if (!validDurationUnit.includes(duration.unitLabel())) { + throw new Error("Allowed unit for scheduling is: 'minute', 'minutes', 'hour', 'hours', 'day' or 'days'"); + } + if (duration.isUnresolved()) { + return new LiteralSchedule(`rate(${duration.formatTokenToNumber()})`); + } if (duration.toSeconds() === 0) { throw new Error('Duration cannot be 0'); } diff --git a/packages/@aws-cdk/aws-applicationautoscaling/test/test.cron.ts b/packages/@aws-cdk/aws-applicationautoscaling/test/test.cron.ts index 6af9c525c9bf9..ca0fb69812b10 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/test/test.cron.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/test/test.cron.ts @@ -1,4 +1,4 @@ -import { Duration } from '@aws-cdk/core'; +import { Duration, Stack, Lazy } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import * as appscaling from '../lib'; @@ -15,8 +15,15 @@ export = { 'rate must be whole number of minutes'(test: Test) { test.throws(() => { - appscaling.Schedule.rate(Duration.seconds(12345)); - }, /'12345 seconds' cannot be converted into a whole number of minutes/); + appscaling.Schedule.rate(Duration.minutes(0.13456)); + }, /'0.13456 minutes' cannot be converted into a whole number of seconds/); + test.done(); + }, + + 'rate must not be in seconds'(test: Test) { + test.throws(() => { + appscaling.Schedule.rate(Duration.seconds(1)); + }, /Allowed unit for scheduling is: 'minute', 'minutes', 'hour', 'hours', 'day' or 'days'/); test.done(); }, @@ -26,4 +33,18 @@ export = { }, /Duration cannot be 0/); test.done(); }, + + 'rate can be token'(test: Test) { + const stack = new Stack(); + const lazyDuration = Duration.minutes(Lazy.number({ produce: () => 5 })); + const rate = appscaling.Schedule.rate(lazyDuration); + test.equal('rate(5 minutes)', stack.resolve(rate).expressionString); + test.done(); + }, + + 'rate can be in allowed type hours'(test: Test) { + test.equal('rate(1 hour)', appscaling.Schedule.rate(Duration.hours(1)) + .expressionString); + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-certificatemanager/README.md b/packages/@aws-cdk/aws-certificatemanager/README.md index 320f22c91663d..24202429ac5df 100644 --- a/packages/@aws-cdk/aws-certificatemanager/README.md +++ b/packages/@aws-cdk/aws-certificatemanager/README.md @@ -68,7 +68,7 @@ When working with multiple domains, use the `CertificateValidation.fromDnsMultiZ const exampleCom = new route53.HostedZone(this, 'ExampleCom', { zoneName: 'example.com', }); -const exampleNet = new route53.HostedZone(this, 'ExampelNet', { +const exampleNet = new route53.HostedZone(this, 'ExampleNet', { zoneName: 'example.net', }); diff --git a/packages/@aws-cdk/aws-cloudfront/lib/cache-policy.ts b/packages/@aws-cdk/aws-cloudfront/lib/cache-policy.ts index 18924ade79748..e9dbf46901eda 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/cache-policy.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/cache-policy.ts @@ -231,6 +231,9 @@ export class CacheHeaderBehavior { if (headers.length === 0) { throw new Error('At least one header to allow must be provided'); } + if (headers.length > 10) { + throw new Error(`Maximum allowed headers in Cache Policy is 10; got ${headers.length}.`); + } return new CacheHeaderBehavior('whitelist', headers); } diff --git a/packages/@aws-cdk/aws-cloudfront/test/cache-policy.test.ts b/packages/@aws-cdk/aws-cloudfront/test/cache-policy.test.ts index 66b5fa80d5749..4f6d618c51621 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/cache-policy.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/cache-policy.test.ts @@ -96,6 +96,17 @@ describe('CachePolicy', () => { expect(() => new CachePolicy(stack, 'CachePolicy6', { cachePolicyName: 'My_Policy' })).not.toThrow(); }); + test('throws if more than 10 CacheHeaderBehavior headers are being passed', () => { + const errorMessage = /Maximum allowed headers in Cache Policy is 10; got (.*?)/; + expect(() => new CachePolicy(stack, 'CachePolicy1', { + headerBehavior: CacheHeaderBehavior.allowList('Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'do', 'eiusmod'), + })).toThrow(errorMessage); + + expect(() => new CachePolicy(stack, 'CachePolicy2', { + headerBehavior: CacheHeaderBehavior.allowList('Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'do'), + })).not.toThrow(); + }); + test('does not throw if cachePolicyName is a token', () => { expect(() => new CachePolicy(stack, 'CachePolicy', { cachePolicyName: Aws.STACK_NAME, diff --git a/packages/@aws-cdk/aws-ec2/lib/port.ts b/packages/@aws-cdk/aws-ec2/lib/port.ts index df8d671271b85..314c8d615b0dd 100644 --- a/packages/@aws-cdk/aws-ec2/lib/port.ts +++ b/packages/@aws-cdk/aws-ec2/lib/port.ts @@ -9,6 +9,8 @@ export enum Protocol { UDP = 'udp', ICMP = 'icmp', ICMPV6 = '58', + ESP = 'esp', + AH = 'ah', } /** @@ -171,6 +173,30 @@ export class Port { }); } + /** + * A single ESP port + */ + public static esp(): Port { + return new Port({ + protocol: Protocol.ESP, + fromPort: 50, + toPort: 50, + stringRepresentation: 'ESP 50', + }); + } + + /** + * A single AH port + */ + public static ah(): Port { + return new Port({ + protocol: Protocol.AH, + fromPort: 51, + toPort: 51, + stringRepresentation: 'AH 51', + }); + } + /** * Whether the rule containing this port range can be inlined into a securitygroup or not. */ diff --git a/packages/@aws-cdk/aws-ec2/lib/windows-versions.ts b/packages/@aws-cdk/aws-ec2/lib/windows-versions.ts index b14134eec2363..20e959420168f 100644 --- a/packages/@aws-cdk/aws-ec2/lib/windows-versions.ts +++ b/packages/@aws-cdk/aws-ec2/lib/windows-versions.ts @@ -11,11 +11,15 @@ export enum WindowsVersion { WINDOWS_SERVER_2012_R2_RTM_JAPANESE_64BIT_BASE = 'Windows_Server-2012-R2_RTM-Japanese-64Bit-Base', WINDOWS_SERVER_2016_ENGLISH_CORE_CONTAINERS = 'Windows_Server-2016-English-Core-Containers', WINDOWS_SERVER_2016_ENGLISH_CORE_SQL_2016_SP1_WEB = 'Windows_Server-2016-English-Core-SQL_2016_SP1_Web', + /** @deprecated - use WINDOWS_SERVER_2016_GERMAN_FULL_BASE */ WINDOWS_SERVER_2016_GERMAL_FULL_BASE = 'Windows_Server-2016-German-Full-Base', + WINDOWS_SERVER_2016_GERMAN_FULL_BASE = 'Windows_Server-2016-German-Full-Base', WINDOWS_SERVER_2003_R2_SP2_LANGUAGE_PACKS_32BIT_BASE = 'Windows_Server-2003-R2_SP2-Language_Packs-32Bit-Base', WINDOWS_SERVER_2008_R2_SP1_ENGLISH_64BIT_SQL_2008_R2_SP3_WEB = 'Windows_Server-2008-R2_SP1-English-64Bit-SQL_2008_R2_SP3_Web', WINDOWS_SERVER_2008_R2_SP1_ENGLISH_64BIT_SQL_2012_SP4_EXPRESS = 'Windows_Server-2008-R2_SP1-English-64Bit-SQL_2012_SP4_Express', + /** @deprecated - use WINDOWS_SERVER_2012_R2_SP1_PORTUGUESE_BRAZIL_64BIT_CORE*/ WINDOWS_SERVER_2012_R2_SP1_PORTUGESE_BRAZIL_64BIT_CORE = 'Windows_Server-2008-R2_SP1-Portuguese_Brazil-64Bit-Core', + WINDOWS_SERVER_2012_R2_SP1_PORTUGUESE_BRAZIL_64BIT_CORE = 'Windows_Server-2008-R2_SP1-Portuguese_Brazil-64Bit-Core', WINDOWS_SERVER_2012_R2_RTM_ENGLISH_64BIT_SQL_2016_SP2_STANDARD = 'Windows_Server-2012-R2_RTM-English-64Bit-SQL_2016_SP2_Standard', WINDOWS_SERVER_2012_RTM_ENGLISH_64BIT_SQL_2014_SP2_EXPRESS = 'Windows_Server-2012-RTM-English-64Bit-SQL_2014_SP2_Express', WINDOWS_SERVER_2012_RTM_ITALIAN_64BIT_BASE = 'Windows_Server-2012-RTM-Italian-64Bit-Base', @@ -28,7 +32,9 @@ export enum WindowsVersion { WINDOWS_SERVER_2016_JAPANESE_FULL_FQL_2016_SP2_WEB = 'Windows_Server-2016-Japanese-Full-SQL_2016_SP2_Web', WINDOWS_SERVER_2016_KOREAN_FULL_BASE = 'Windows_Server-2016-Korean-Full-Base', WINDOWS_SERVER_2016_KOREAN_FULL_SQL_2016_SP2_STANDARD = 'Windows_Server-2016-Korean-Full-SQL_2016_SP2_Standard', + /** @deprecated - use WINDOWS_SERVER_2016_PORTUGUESE_PORTUGAL_FULL_BASE */ WINDOWS_SERVER_2016_PORTUGESE_PORTUGAL_FULL_BASE = 'Windows_Server-2016-Portuguese_Portugal-Full-Base', + WINDOWS_SERVER_2016_PORTUGUESE_PORTUGAL_FULL_BASE = 'Windows_Server-2016-Portuguese_Portugal-Full-Base', WINDOWS_SERVER_2019_ENGLISH_FULL_SQL_2017_WEB = 'Windows_Server-2019-English-Full-SQL_2017_Web', WINDOWS_SERVER_2019_FRENCH_FULL_BASE = 'Windows_Server-2019-French-Full-Base', WINDOWS_SERVER_2019_KOREAN_FULL_BASE = 'Windows_Server-2019-Korean-Full-Base', @@ -90,8 +96,12 @@ export enum WindowsVersion { WINDOWS_SERVER_2012_R2_RTM_ENGLISH_64BIT_SQL_2014_SP2_EXPRESS = 'Windows_Server-2012-R2_RTM-English-64Bit-SQL_2014_SP2_Express', WINDOWS_SERVER_2012_R2_RTM_ENGLISH_64BIT_SQL_2014_SP3_WEB = 'Windows_Server-2012-R2_RTM-English-64Bit-SQL_2014_SP3_Web', WINDOWS_SERVER_2012_R2_RTM_JAPANESE_64BIT_SQL_2016_SP1_WEB = 'Windows_Server-2012-R2_RTM-Japanese-64Bit-SQL_2016_SP1_Web', + /** @deprecated - use WINDOWS_SERVER_2012_R2_RTM_PORTUGUESE_BRAZIL_64BIT_BASE */ WINDOWS_SERVER_2012_R2_RTM_PORTUGESE_BRAZIL_64BIT_BASE = 'Windows_Server-2012-R2_RTM-Portuguese_Brazil-64Bit-Base', + WINDOWS_SERVER_2012_R2_RTM_PORTUGUESE_BRAZIL_64BIT_BASE = 'Windows_Server-2012-R2_RTM-Portuguese_Brazil-64Bit-Base', + /** @deprecated - use WINDOWS_SERVER_2012_R2_RTM_PORTUGUESE_PORTUGAL_64BIT_BASE*/ WINDOWS_SERVER_2012_R2_RTM_PORTUGESE_PORTUGAL_64BIT_BASE = 'Windows_Server-2012-R2_RTM-Portuguese_Portugal-64Bit-Base', + WINDOWS_SERVER_2012_R2_RTM_PORTUGUESE_PORTUGAL_64BIT_BASE = 'Windows_Server-2012-R2_RTM-Portuguese_Portugal-64Bit-Base', WINDOWS_SERVER_2012_R2_RTM_SWEDISH_64BIT_BASE = 'Windows_Server-2012-R2_RTM-Swedish-64Bit-Base', WINDOWS_SERVER_2016_ENGLISH_FULL_SQL_2016_SP1_EXPRESS = 'Windows_Server-2016-English-Full-SQL_2016_SP1_Express', WINDOWS_SERVER_2016_ITALIAN_FULL_BASE = 'Windows_Server-2016-Italian-Full-Base', @@ -103,7 +113,9 @@ export enum WindowsVersion { WINDOWS_SERVER_2012_RTM_ENGLISH_64BIT_SQL_2007_R2_SP3_WEB = 'Windows_Server-2012-RTM-English-64Bit-SQL_2008_R2_SP3_Web', WINDOWS_SERVER_2012_RTM_JAPANESE_64BIT_SQL_2014_SP2_WEB = 'Windows_Server-2012-RTM-Japanese-64Bit-SQL_2014_SP2_Web', WINDOWS_SERVER_2016_ENGLISH_CORE_SQL_2016_SP2_ENTERPRISE = 'Windows_Server-2016-English-Core-SQL_2016_SP2_Enterprise', + /** @deprecated - use WINDOWS_SERVER_2016_PORTUGUESE_BRAZIL_FULL_BASE */ WINDOWS_SERVER_2016_PORTUGESE_BRAZIL_FULL_BASE = 'Windows_Server-2016-Portuguese_Brazil-Full-Base', + WINDOWS_SERVER_2016_PORTUGUESE_BRAZIL_FULL_BASE = 'Windows_Server-2016-Portuguese_Brazil-Full-Base', WINDOWS_SERVER_2019_ENGLISH_FULL_BASE = 'Windows_Server-2019-English-Full-Base', WINDOWS_SERVER_2003_R2_SP2_ENGLISH_32BIT_BASE = 'Windows_Server-2003-R2_SP2-English-32Bit-Base', WINDOWS_SERVER_2012_R2_RTM_CZECH_64BIT_BASE = 'Windows_Server-2012-R2_RTM-Czech-64Bit-Base', @@ -128,7 +140,9 @@ export enum WindowsVersion { WINDOWS_SERVER_2003_R2_SP2_ENGLISH_64BIT_BASE = 'Windows_Server-2003-R2_SP2-English-64Bit-Base', WINDOWS_SERVER_2008_R2_SP1_ENGLISH_64BIT_BASE = 'Windows_Server-2008-R2_SP1-English-64Bit-Base', WINDOWS_SERVER_2008_R2_SP1_LANGUAGE_PACKS_64BIT_SQL_2008_R2_SP3_EXPRESS = 'Windows_Server-2008-R2_SP1-Language_Packs-64Bit-SQL_2008_R2_SP3_Express', + /** @deprecated - use WINDOWS_SERVER_2012_SP2_PORTUGUESE_BRAZIL_64BIT_BASE */ WINDOWS_SERVER_2012_SP2_PORTUGESE_BRAZIL_64BIT_BASE = 'Windows_Server-2008-SP2-Portuguese_Brazil-64Bit-Base', + WINDOWS_SERVER_2012_SP2_PORTUGUESE_BRAZIL_64BIT_BASE = 'Windows_Server-2008-SP2-Portuguese_Brazil-64Bit-Base', WINDOWS_SERVER_2012_R2_RTM_ENGLISH_64BIT_SQL_2016_SP1_WEB = 'Windows_Server-2012-R2_RTM-English-64Bit-SQL_2016_SP1_Web', WINDOWS_SERVER_2012_R2_RTM_JAPANESE_64BIT_SQL_2014_SP3_EXPRESS = 'Windows_Server-2012-R2_RTM-Japanese-64Bit-SQL_2014_SP3_Express', WINDOWS_SERVER_2012_R2_RTM_JAPANESE_64BIT_SQL_2016_SP2_ENTERPRISE = 'Windows_Server-2012-R2_RTM-Japanese-64Bit-SQL_2016_SP2_Enterprise', @@ -141,7 +155,9 @@ export enum WindowsVersion { WINDOWS_SERVER_2008_R2_SP1_JAPANESE_64BIT_BASE = 'Windows_Server-2008-R2_SP1-Japanese-64Bit-Base', WINDOWS_SERVER_2008_SP2_ENGLISH_64BIT_SQL_2008_SP4_STANDARD = 'Windows_Server-2008-SP2-English-64Bit-SQL_2008_SP4_Standard', WINDOWS_SERVER_2012_R2_RTM_ENGLISH_64BIT_BASE = 'Windows_Server-2012-R2_RTM-English-64Bit-Base', + /** @deprecated - use WINDOWS_SERVER_2012_RTM_PORTUGUESE_BRAZIL_64BIT_BASE */ WINDOWS_SERVER_2012_RTM_PORTUGESE_BRAZIL_64BIT_BASE = 'Windows_Server-2012-RTM-Portuguese_Brazil-64Bit-Base', + WINDOWS_SERVER_2012_RTM_PORTUGUESE_BRAZIL_64BIT_BASE = 'Windows_Server-2012-RTM-Portuguese_Brazil-64Bit-Base', WINDOWS_SERVER_2016_ENGLISH_FULL_SQL_2016_SP1_WEB = 'Windows_Server-2016-English-Full-SQL_2016_SP1_Web', WINDOWS_SERVER_2016_ENGLISH_P3 = 'Windows_Server-2016-English-P3', WINDOWS_SERVER_2016_JAPANESE_FULL_SQL_2016_SP1_ENTERPRISE = 'Windows_Server-2016-Japanese-Full-SQL_2016_SP1_Enterprise', @@ -165,7 +181,9 @@ export enum WindowsVersion { WINDOWS_SERVER_2016_CHINESE_SIMPLIFIED_FULL_BASE = 'Windows_Server-2016-Chinese_Simplified-Full-Base', WINDOWS_SERVER_2019_POLISH_FULL_BASE = 'Windows_Server-2019-Polish-Full-Base', WINDOWS_SERVER_2008_R2_SP1_JAPANESE_64BIT_SQL_2008_R2_SP3_WEB = 'Windows_Server-2008-R2_SP1-Japanese-64Bit-SQL_2008_R2_SP3_Web', + /** @deprecated - use WINDOWS_SERVER_2008_R2_SP1_PORTUGUESE_BRAZIL_64BIT_BASE */ WINDOWS_SERVER_2008_R2_SP1_PORTUGESE_BRAZIL_64BIT_BASE = 'Windows_Server-2008-R2_SP1-Portuguese_Brazil-64Bit-Base', + WINDOWS_SERVER_2008_R2_SP1_PORTUGUESE_BRAZIL_64BIT_BASE = 'Windows_Server-2008-R2_SP1-Portuguese_Brazil-64Bit-Base', WINDOWS_SERVER_2012_R2_RTM_JAPANESE_64BIT_SQL_2016_SP1_ENTERPRISE = 'Windows_Server-2012-R2_RTM-Japanese-64Bit-SQL_2016_SP1_Enterprise', WINDOWS_SERVER_2012_RTM_JAPANESE_64BIT_SQL_2016_SP2_EXPRESS = 'Windows_Server-2012-R2_RTM-Japanese-64Bit-SQL_2016_SP2_Express', WINDOWS_SERVER_2012_RTM_ENGLISH_64BIT_SQL_2014_SP3_EXPRESS = 'Windows_Server-2012-RTM-English-64Bit-SQL_2014_SP3_Express', @@ -196,10 +214,14 @@ export enum WindowsVersion { WINDOWS_SERVER_1709_ENGLISH_CORE_BASE = 'Windows_Server-1709-English-Core-Base', WINDOWS_SERVER_2008_R2_SP1_ENGLISH_61BIT_SQL_2012_RTM_SP2_ENTERPRISE = 'Windows_Server-2008-R2_SP1-English-64Bit-SQL_2012_RTM_SP2_Enterprise', WINDOWS_SERVER_2008_R2_SP1_ENGLISH_64BIT_SQL_2012_SP4_STANDARD = 'Windows_Server-2008-R2_SP1-English-64Bit-SQL_2012_SP4_Standard', + /** @deprecated - use WINDOWS_SERVER_2008_SP2_PORTUGUESE_BRAZIL_32BIT_BASE */ WINDOWS_SERVER_2008_SP2_PORTUGESE_BRAZIL_32BIT_BASE = 'Windows_Server-2008-SP2-Portuguese_Brazil-32Bit-Base', + WINDOWS_SERVER_2008_SP2_PORTUGUESE_BRAZIL_32BIT_BASE = 'Windows_Server-2008-SP2-Portuguese_Brazil-32Bit-Base', WINDOWS_SERVER_2012_R2_RTM_JAPANESE_64BIT_SQL_2014_SP2_STANDARD = 'Windows_Server-2012-R2_RTM-Japanese-64Bit-SQL_2014_SP2_Standard', WINDOWS_SERVER_2012_RTM_JAPANESE_64BIT_SQL_2012_SP4_EXPRESS = 'Windows_Server-2012-RTM-Japanese-64Bit-SQL_2012_SP4_Express', + /** @deprecated - use WINDOWS_SERVER_2012_RTM_PORTUGUESE_PORTUGAL_64BIT_BASE */ WINDOWS_SERVER_2012_RTM_PORTUGESE_PORTUGAL_64BIT_BASE = 'Windows_Server-2012-RTM-Portuguese_Portugal-64Bit-Base', + WINDOWS_SERVER_2012_RTM_PORTUGUESE_PORTUGAL_64BIT_BASE = 'Windows_Server-2012-RTM-Portuguese_Portugal-64Bit-Base', WINDOWS_SERVER_2016_CZECH_FULL_BASE = 'Windows_Server-2016-Czech-Full-Base', WINDOWS_SERVER_2016_JAPANESE_FULL_SQL_2016_SP1_STANDARD = 'Windows_Server-2016-Japanese-Full-SQL_2016_SP1_Standard', WINDOWS_SERVER_2019_DUTCH_FULL_BASE = 'Windows_Server-2019-Dutch-Full-Base', @@ -212,7 +234,9 @@ export enum WindowsVersion { WINDOWS_SERVER_2016_ENGLISH_CORE_SQL_2016_SP2_EXPRESS = 'Windows_Server-2016-English-Core-SQL_2016_SP2_Express', WINDOWS_SERVER_2016_ENGLISH_CORE_SQL_2016_SP2_WEB = 'Windows_Server-2016-English-Core-SQL_2016_SP2_Web', WINDOWS_SERVER_2016_ENGLISH_FULL_SQL_2017_STANDARD = 'Windows_Server-2016-English-Full-SQL_2017_Standard', + /** @deprecated - use WINDOWS_SERVER_2019_PORTUGUESE_BRAZIL_FULL_BASE */ WINDOWS_SERVER_2019_PORTUGESE_BRAZIL_FULL_BASE = 'Windows_Server-2019-Portuguese_Brazil-Full-Base', + WINDOWS_SERVER_2019_PORTUGUESE_BRAZIL_FULL_BASE = 'Windows_Server-2019-Portuguese_Brazil-Full-Base', WINDOWS_SERVER_2008_R2_SP1_ENGLISH_64BIT_SQL_2008_R2_SP3_STANDARD = 'Windows_Server-2008-R2_SP1-English-64Bit-SQL_2008_R2_SP3_Standard', WINDOWS_SERVER_2008_R2_SP1_ENGLISH_64BIT_SHAREPOINT_2010_SP2_FOUNDATION = 'Windows_Server-2008-R2_SP1-English-64Bit-SharePoint_2010_SP2_Foundation', WINDOWS_SERVER_2012_R2_RTM_ENGLISH_P3 = 'Windows_Server-2012-R2_RTM-English-P3', @@ -221,7 +245,9 @@ export enum WindowsVersion { WINDOWS_SERVER_2012_RTM_JAPANESE_64BIT_SQL_2014_SP3_EXPRESS = 'Windows_Server-2012-RTM-Japanese-64Bit-SQL_2014_SP3_Express', WINDOWS_SERVER_2016_ENGLISH_CORE_SQL_2016_SP2_STANDARD = 'Windows_Server-2016-English-Core-SQL_2016_SP2_Standard', WINDOWS_SERVER_2016_JAPANESE_FULL_SQL_2016_SP2_STANDARD = 'Windows_Server-2016-Japanese-Full-SQL_2016_SP2_Standard', + /** @deprecated - use WINDOWS_SERVER_2019_PORTUGUESE_PORTUGAL_FULL_BASE */ WINDOWS_SERVER_2019_PORTUGESE_PORTUGAL_FULL_BASE = 'Windows_Server-2019-Portuguese_Portugal-Full-Base', + WINDOWS_SERVER_2019_PORTUGUESE_PORTUGAL_FULL_BASE = 'Windows_Server-2019-Portuguese_Portugal-Full-Base', WINDOWS_SERVER_2019_SWEDISH_FULL_BASE = 'Windows_Server-2019-Swedish-Full-Base', WINDOWS_SERVER_2012_R2_RTM_ENGLISH_64BIT_HYPERV = 'Windows_Server-2012-R2_RTM-English-64Bit-HyperV', WINDOWS_SERVER_2012_RTM_KOREAN_64BIT_BASE = 'Windows_Server-2012-RTM-Korean-64Bit-Base', diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index 801040d4c7385..c5d5d9be6a64e 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -140,6 +140,7 @@ "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_ENGLISH_64BIT_HYPERV", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2019_SWEDISH_FULL_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2019_PORTUGESE_PORTUGAL_FULL_BASE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2019_PORTUGUESE_PORTUGAL_FULL_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_JAPANESE_FULL_SQL_2016_SP2_STANDARD", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_ENGLISH_CORE_SQL_2016_SP2_STANDARD", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_RTM_JAPANESE_64BIT_SQL_2014_SP3_EXPRESS", @@ -149,6 +150,7 @@ "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_R2_SP1_ENGLISH_64BIT_SHAREPOINT_2010_SP2_FOUNDATION", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_R2_SP1_ENGLISH_64BIT_SQL_2008_R2_SP3_STANDARD", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2019_PORTUGESE_BRAZIL_FULL_BASE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2019_PORTUGUESE_BRAZIL_FULL_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_ENGLISH_FULL_SQL_2017_STANDARD", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_ENGLISH_CORE_SQL_2016_SP2_WEB", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_ENGLISH_CORE_SQL_2016_SP2_EXPRESS", @@ -162,9 +164,11 @@ "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_JAPANESE_FULL_SQL_2016_SP1_STANDARD", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_CZECH_FULL_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_RTM_PORTUGESE_PORTUGAL_64BIT_BASE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_RTM_PORTUGUESE_PORTUGAL_64BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_RTM_JAPANESE_64BIT_SQL_2012_SP4_EXPRESS", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_JAPANESE_64BIT_SQL_2014_SP2_STANDARD", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_SP2_PORTUGESE_BRAZIL_32BIT_BASE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_SP2_PORTUGUESE_BRAZIL_32BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_R2_SP1_ENGLISH_64BIT_SQL_2012_SP4_STANDARD", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_R2_SP1_ENGLISH_61BIT_SQL_2012_RTM_SP2_ENTERPRISE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_1709_ENGLISH_CORE_BASE", @@ -315,6 +319,8 @@ "docs-public-apis:@aws-cdk/aws-ec2.Protocol.UDP", "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ICMP", "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ICMPV6", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ESP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.AH", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_SP2_ENGLISH_64BIT_SQL_2008_SP4_EXPRESS", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_CHINESE_SIMPLIFIED_64BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_CHINESE_TRADITIONAL_64BIT_BASE", @@ -325,10 +331,12 @@ "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_ENGLISH_CORE_CONTAINERS", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_ENGLISH_CORE_SQL_2016_SP1_WEB", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_GERMAL_FULL_BASE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_GERMAN_FULL_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2003_R2_SP2_LANGUAGE_PACKS_32BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_R2_SP1_ENGLISH_64BIT_SQL_2008_R2_SP3_WEB", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_R2_SP1_ENGLISH_64BIT_SQL_2012_SP4_EXPRESS", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_SP1_PORTUGESE_BRAZIL_64BIT_CORE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_SP1_PORTUGUESE_BRAZIL_64BIT_CORE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_ENGLISH_64BIT_SQL_2016_SP2_STANDARD", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_RTM_ENGLISH_64BIT_SQL_2014_SP2_EXPRESS", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_RTM_ITALIAN_64BIT_BASE", @@ -342,6 +350,7 @@ "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_KOREAN_FULL_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_KOREAN_FULL_SQL_2016_SP2_STANDARD", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_PORTUGESE_PORTUGAL_FULL_BASE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_PORTUGUESE_PORTUGAL_FULL_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2019_ENGLISH_FULL_SQL_2017_WEB", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2019_FRENCH_FULL_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2019_KOREAN_FULL_BASE", @@ -404,7 +413,9 @@ "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_ENGLISH_64BIT_SQL_2014_SP3_WEB", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_JAPANESE_64BIT_SQL_2016_SP1_WEB", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_PORTUGESE_BRAZIL_64BIT_BASE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_PORTUGUESE_BRAZIL_64BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_PORTUGESE_PORTUGAL_64BIT_BASE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_PORTUGUESE_PORTUGAL_64BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_SWEDISH_64BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_ENGLISH_FULL_SQL_2016_SP1_EXPRESS", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_ITALIAN_FULL_BASE", @@ -417,6 +428,7 @@ "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_RTM_JAPANESE_64BIT_SQL_2014_SP2_WEB", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_ENGLISH_CORE_SQL_2016_SP2_ENTERPRISE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_PORTUGESE_BRAZIL_FULL_BASE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_PORTUGUESE_BRAZIL_FULL_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2019_ENGLISH_FULL_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2003_R2_SP2_ENGLISH_32BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_CZECH_64BIT_BASE", @@ -441,6 +453,7 @@ "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_R2_SP1_ENGLISH_64BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_R2_SP1_LANGUAGE_PACKS_64BIT_SQL_2008_R2_SP3_EXPRESS", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_SP2_PORTUGESE_BRAZIL_64BIT_BASE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_SP2_PORTUGUESE_BRAZIL_64BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_ENGLISH_64BIT_SQL_2016_SP1_WEB", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_JAPANESE_64BIT_SQL_2014_SP3_EXPRESS", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_JAPANESE_64BIT_SQL_2016_SP2_ENTERPRISE", @@ -454,6 +467,7 @@ "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_SP2_ENGLISH_64BIT_SQL_2008_SP4_STANDARD", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_ENGLISH_64BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_RTM_PORTUGESE_BRAZIL_64BIT_BASE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_RTM_PORTUGUESE_BRAZIL_64BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_ENGLISH_FULL_SQL_2016_SP1_WEB", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_ENGLISH_P3", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_JAPANESE_FULL_SQL_2016_SP1_ENTERPRISE", @@ -478,6 +492,7 @@ "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2019_POLISH_FULL_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_R2_SP1_JAPANESE_64BIT_SQL_2008_R2_SP3_WEB", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_R2_SP1_PORTUGESE_BRAZIL_64BIT_BASE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_R2_SP1_PORTUGUESE_BRAZIL_64BIT_BASE", "props-default-doc:@aws-cdk/aws-ec2.InterfaceVpcEndpointAttributes.securityGroupId", "props-default-doc:@aws-cdk/aws-ec2.InterfaceVpcEndpointAttributes.securityGroups", "props-physical-name:@aws-cdk/aws-ec2.VpnGatewayProps" diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpc.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.vpc.expected.json index 1586cc8bff5e7..641b97b4ddbd5 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.vpc.expected.json +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpc.expected.json @@ -567,6 +567,20 @@ "FromPort": 800, "IpProtocol": "udp", "ToPort": 801 + }, + { + "CidrIp": "0.0.0.0/0", + "Description": "from 0.0.0.0/0:ESP 50", + "FromPort": 50, + "IpProtocol": "esp", + "ToPort": 50 + }, + { + "CidrIp": "0.0.0.0/0", + "Description": "from 0.0.0.0/0:AH 51", + "FromPort": 51, + "IpProtocol": "ah", + "ToPort": 51 } ], "VpcId": { @@ -575,4 +589,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpc.ts b/packages/@aws-cdk/aws-ec2/test/integ.vpc.ts index 2ffd5653e33f4..88e4dacf9839a 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.vpc.ts +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpc.ts @@ -16,6 +16,8 @@ const rules = [ ec2.Port.allUdp(), ec2.Port.udp(123), ec2.Port.udpRange(800, 801), + ec2.Port.esp(), + ec2.Port.ah(), ]; for (const rule of rules) { diff --git a/packages/@aws-cdk/aws-ecr/README.md b/packages/@aws-cdk/aws-ecr/README.md index 6adb791ba7f76..03172cac6ff82 100644 --- a/packages/@aws-cdk/aws-ecr/README.md +++ b/packages/@aws-cdk/aws-ecr/README.md @@ -74,6 +74,14 @@ ecr.PublicGalleryAuthorizationToken.grantRead(user); This user can then proceed to login to the registry using one of the [authentication methods](https://docs.aws.amazon.com/AmazonECR/latest/public/public-registries.html#public-registry-auth). +### Image tag immutability + +You can set tag immutability on images in our repository using the `imageTagMutability` construct prop. + +```ts +new ecr.Repository(stack, 'Repo', { imageTagMutability: ecr.TagMutability.IMMUTABLE }); +``` + ## Automatically clean up repositories You can set life cycle rules to automatically clean up old images from your diff --git a/packages/@aws-cdk/aws-ecr/lib/repository.ts b/packages/@aws-cdk/aws-ecr/lib/repository.ts index 20f110c206428..3734db8176d36 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository.ts @@ -354,6 +354,13 @@ export interface RepositoryProps { * @default false */ readonly imageScanOnPush?: boolean; + + /** + * The tag mutability setting for the repository. If this parameter is omitted, the default setting of MUTABLE will be used which will allow image tags to be overwritten. + * + * @default TagMutability.MUTABLE + */ + readonly imageTagMutability?: TagMutability; } export interface RepositoryAttributes { @@ -452,6 +459,7 @@ export class Repository extends RepositoryBase { imageScanningConfiguration: !props.imageScanOnPush ? undefined : { ScanOnPush: true, }, + imageTagMutability: props.imageTagMutability || undefined, }); resource.applyRemovalPolicy(props.removalPolicy); @@ -610,3 +618,19 @@ const enum CountType { */ SINCE_IMAGE_PUSHED = 'sinceImagePushed', } + +/** + * The tag mutability setting for your repository. + */ +export enum TagMutability { + /** + * allow image tags to be overwritten. + */ + MUTABLE = 'MUTABLE', + + /** + * all image tags within the repository will be immutable which will prevent them from being overwritten. + */ + IMMUTABLE = 'IMMUTABLE', + +} diff --git a/packages/@aws-cdk/aws-ecr/package.json b/packages/@aws-cdk/aws-ecr/package.json index 95a78ae04c522..bc3671a7c5585 100644 --- a/packages/@aws-cdk/aws-ecr/package.json +++ b/packages/@aws-cdk/aws-ecr/package.json @@ -103,6 +103,7 @@ "import:@aws-cdk/aws-ecr.Repository", "construct-base-is-private:@aws-cdk/aws-ecr.RepositoryBase", "docs-public-apis:@aws-cdk/aws-ecr.Repository.fromRepositoryArn", + "docs-public-apis:@aws-cdk/aws-ecr.Repository.imageTagMutability", "docs-public-apis:@aws-cdk/aws-ecr.Repository.fromRepositoryName", "props-default-doc:@aws-cdk/aws-ecr.LifecycleRule.maxImageAge", "props-default-doc:@aws-cdk/aws-ecr.LifecycleRule.maxImageCount", diff --git a/packages/@aws-cdk/aws-ecr/test/test.repository.ts b/packages/@aws-cdk/aws-ecr/test/test.repository.ts index 8c8094be287e9..fe020462f7716 100644 --- a/packages/@aws-cdk/aws-ecr/test/test.repository.ts +++ b/packages/@aws-cdk/aws-ecr/test/test.repository.ts @@ -63,6 +63,20 @@ export = { test.done(); }, + + 'image tag mutability can be set'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + new ecr.Repository(stack, 'Repo', { imageTagMutability: ecr.TagMutability.IMMUTABLE }); + + // THEN + expect(stack).to(haveResource('AWS::ECR::Repository', { + ImageTagMutability: 'IMMUTABLE', + })); + + test.done(); + }, + 'add day-based lifecycle policy'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 8ac684943e244..cc4bd432e7dba 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -728,6 +728,20 @@ new ecs.Ec2Service(stack, 'Service', { }); ``` +### Associate With a Specific CloudMap Service + +You may associate an ECS service with a specific CloudMap service. To do +this, use the service's `associateCloudMapService` method: + +```ts +const cloudMapService = new cloudmap.Service(...); +const ecsService = new ecs.FargateService(...); + +ecsService.associateCloudMapService({ + service: cloudMapService, +}); +``` + ## Capacity Providers Currently, only `FARGATE` and `FARGATE_SPOT` capacity providers are supported. diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 1e1fdde585f1e..3ff3f20fd8acd 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -601,6 +601,27 @@ export abstract class BaseService extends Resource return cloudmapService; } + /** + * Associates this service with a CloudMap service + */ + public associateCloudMapService(options: AssociateCloudMapServiceOptions): void { + const service = options.service; + + const { containerName, containerPort } = determineContainerNameAndPort({ + taskDefinition: this.taskDefinition, + dnsRecordType: service.dnsRecordType, + container: options.container, + containerPort: options.containerPort, + }); + + // add Cloudmap service to the ECS Service's serviceRegistry + this.addServiceRegistry({ + arn: service.serviceArn, + containerName, + containerPort, + }); + } + /** * This method returns the specified CloudWatch metric name for this service. */ @@ -748,6 +769,10 @@ export abstract class BaseService extends Resource * Associate Service Discovery (Cloud Map) service */ private addServiceRegistry(registry: ServiceRegistry) { + if (this.serviceRegistries.length >= 1) { + throw new Error('Cannot associate with the given service discovery registry. ECS supports at most one service registry per service.'); + } + const sr = this.renderServiceRegistry(registry); this.serviceRegistries.push(sr); } @@ -816,6 +841,28 @@ export interface CloudMapOptions { readonly containerPort?: number; } +/** + * The options for using a cloudmap service. + */ +export interface AssociateCloudMapServiceOptions { + /** + * The cloudmap service to register with. + */ + readonly service: cloudmap.IService; + + /** + * The container to point to for a SRV record. + * @default - the task definition's default container + */ + readonly container?: ContainerDefinition; + + /** + * The port to point to for a SRV record. + * @default - the default port of the task definition's default container + */ + readonly containerPort?: number; +} + /** * Service Registry for ECS service */ diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts index c982920225214..5531b53413f46 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts @@ -262,6 +262,101 @@ nodeunitShim({ test.done(); }, + 'with user-provided cloudmap service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + + const container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + container.addPortMappings({ containerPort: 8000 }); + + const cloudMapNamespace = new cloudmap.PrivateDnsNamespace(stack, 'TestCloudMapNamespace', { + name: 'scorekeep.com', + vpc, + }); + + const cloudMapService = new cloudmap.Service(stack, 'Service', { + name: 'service-name', + namespace: cloudMapNamespace, + dnsRecordType: cloudmap.DnsRecordType.SRV, + }); + + const ecsService = new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + }); + + // WHEN + ecsService.associateCloudMapService({ + service: cloudMapService, + container: container, + containerPort: 8000, + }); + + // THEN + expect(stack).to(haveResource('AWS::ECS::Service', { + ServiceRegistries: [ + { + ContainerName: 'web', + ContainerPort: 8000, + RegistryArn: { 'Fn::GetAtt': ['ServiceDBC79909', 'Arn'] }, + }, + ], + })); + + test.done(); + }, + + 'errors when more than one service registry used'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + + const container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + container.addPortMappings({ containerPort: 8000 }); + + const cloudMapNamespace = new cloudmap.PrivateDnsNamespace(stack, 'TestCloudMapNamespace', { + name: 'scorekeep.com', + vpc, + }); + + const ecsService = new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + }); + + ecsService.enableCloudMap({ + cloudMapNamespace, + }); + + const cloudMapService = new cloudmap.Service(stack, 'Service', { + name: 'service-name', + namespace: cloudMapNamespace, + dnsRecordType: cloudmap.DnsRecordType.SRV, + }); + + // WHEN / THEN + test.throws(() => { + ecsService.associateCloudMapService({ + service: cloudMapService, + container: container, + containerPort: 8000, + }); + }, /at most one service registry/i); + + test.done(); + }, + 'with all properties set'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index b72151f81f2f8..3a13b8a3490a1 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -266,9 +266,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis // Only one certificate can be specified per resource, even though // `certificates` is of type Array for (let i = 0; i < additionalCerts.length; i++) { - // ids should look like: `id`, `id2`, `id3` (for backwards-compatibility) - const certId = (i > 0) ? `${id}${i + 1}` : id; - new ApplicationListenerCertificate(this, certId, { + new ApplicationListenerCertificate(this, `${id}${i + 1}`, { listener: this, certificates: [additionalCerts[i]], }); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts index b6e379a17e463..e29e5a7837895 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts @@ -162,7 +162,7 @@ describe('tests', () => { ], }); - expect(listener.node.tryFindChild('DefaultCertificates')).toBeDefined(); + expect(listener.node.tryFindChild('DefaultCertificates1')).toBeDefined(); expect(listener.node.tryFindChild('DefaultCertificates2')).toBeDefined(); expect(listener.node.tryFindChild('DefaultCertificates3')).not.toBeDefined(); diff --git a/packages/@aws-cdk/aws-events-targets/README.md b/packages/@aws-cdk/aws-events-targets/README.md index 4b795ce5f19b5..d49621b0199cd 100644 --- a/packages/@aws-cdk/aws-events-targets/README.md +++ b/packages/@aws-cdk/aws-events-targets/README.md @@ -90,6 +90,39 @@ const rule = new events.Rule(this, 'rule', { rule.addTarget(new targets.CloudWatchLogGroup(logGroup)); ``` +## Trigger a CodeBuild project + +Use the `CodeBuildProject` target to trigger a CodeBuild project. + +The code snippet below creates a CodeCommit repository that triggers a CodeBuild project +on commit to the master branch. You can optionally attach a +[dead letter queue](https://docs.aws.amazon.com/eventbridge/latest/userguide/rule-dlq.html). + +```ts +import * as codebuild from '@aws-sdk/aws-codebuild'; +import * as codecommit from '@aws-sdk/aws-codecommit'; +import * as sqs from '@aws-sdk/aws-sqs'; +import * as targets from "@aws-cdk/aws-events-targets"; + +const repo = new codecommit.Repository(this, 'MyRepo', { + repositoryName: 'aws-cdk-codebuild-events', +}); + +const project = new codebuild.Project(this, 'MyProject', { + source: codebuild.Source.codeCommit({ repository: repo }), +}); + +const deadLetterQueue = new sqs.Queue(this, 'DeadLetterQueue'); + +// trigger a build when a commit is pushed to the repo +const onCommitRule = repo.onCommit('OnCommit', { + target: new targets.CodeBuildProject(project, { + deadLetterQueue: deadLetterQueue, + }), + branches: ['master'], +}); +``` + ## Trigger a State Machine Use the `SfnStateMachine` target to trigger a State Machine. diff --git a/packages/@aws-cdk/aws-events-targets/lib/codebuild.ts b/packages/@aws-cdk/aws-events-targets/lib/codebuild.ts index 55eac4cadd32f..d5b5e753b3867 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/codebuild.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/codebuild.ts @@ -1,7 +1,8 @@ import * as codebuild from '@aws-cdk/aws-codebuild'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; -import { singletonEventRole } from './util'; +import * as sqs from '@aws-cdk/aws-sqs'; +import { addToDeadLetterQueueResourcePolicy, singletonEventRole } from './util'; /** * Customize the CodeBuild Event Target @@ -24,6 +25,18 @@ export interface CodeBuildProjectProps { * @default - the entire EventBridge event */ readonly event?: events.RuleTargetInput; + + /** + * The SQS queue to be used as deadLetterQueue. + * Check out the [considerations for using a dead-letter queue](https://docs.aws.amazon.com/eventbridge/latest/userguide/rule-dlq.html#dlq-considerations). + * + * The events not successfully delivered are automatically retried for a specified period of time, + * depending on the retry policy of the target. + * If an event is not delivered before all retry attempts are exhausted, it will be sent to the dead letter queue. + * + * @default - no dead-letter queue + */ + readonly deadLetterQueue?: sqs.IQueue; } /** @@ -39,9 +52,15 @@ export class CodeBuildProject implements events.IRuleTarget { * Allows using build projects as event rule targets. */ public bind(_rule: events.IRule, _id?: string): events.RuleTargetConfig { + + if (this.props.deadLetterQueue) { + addToDeadLetterQueueResourcePolicy(_rule, this.props.deadLetterQueue); + } + return { id: '', arn: this.project.projectArn, + deadLetterConfig: this.props.deadLetterQueue ? { arn: this.props.deadLetterQueue?.queueArn } : undefined, role: this.props.eventRole || singletonEventRole(this.project, [ new iam.PolicyStatement({ actions: ['codebuild:StartBuild'], diff --git a/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts b/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts index c7bb3d59b68e8..1eba641d7c8ce 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts @@ -2,6 +2,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; +import * as sqs from '@aws-cdk/aws-sqs'; import { CfnElement, Stack } from '@aws-cdk/core'; import * as targets from '../../lib'; @@ -120,4 +121,84 @@ describe('CodeBuild event target', () => { ], })); }); + + test('use a Dead Letter Queue for the rule target', () => { + // GIVEN + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 hour)'), + }); + + const queue = new sqs.Queue(stack, 'Queue'); + + // WHEN + const eventInput = { + buildspecOverride: 'buildspecs/hourly.yml', + }; + + rule.addTarget( + new targets.CodeBuildProject(project, { + event: events.RuleTargetInput.fromObject(eventInput), + deadLetterQueue: queue, + }), + ); + + // THEN + expect(stack).to(haveResource('AWS::Events::Rule', { + Targets: [ + { + Arn: projectArn, + Id: 'Target0', + DeadLetterConfig: { + Arn: { + 'Fn::GetAtt': [ + 'Queue4A7E3555', + 'Arn', + ], + }, + }, + Input: JSON.stringify(eventInput), + RoleArn: { + 'Fn::GetAtt': ['MyProjectEventsRole5B7D93F5', 'Arn'], + }, + }, + ], + })); + + expect(stack).to(haveResource('AWS::SQS::QueuePolicy', { + PolicyDocument: { + Statement: [ + { + Action: 'sqs:SendMessage', + Condition: { + ArnEquals: { + 'aws:SourceArn': { + 'Fn::GetAtt': [ + 'Rule4C995B7F', + 'Arn', + ], + }, + }, + }, + Effect: 'Allow', + Principal: { + Service: 'events.amazonaws.com', + }, + Resource: { + 'Fn::GetAtt': [ + 'Queue4A7E3555', + 'Arn', + ], + }, + Sid: 'AllowEventRuleRule', + }, + ], + Version: '2012-10-17', + }, + Queues: [ + { + Ref: 'Queue4A7E3555', + }, + ], + })); + }); }); diff --git a/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json b/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json index 007503de08383..aec41898a4622 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json @@ -43,6 +43,14 @@ "Arn" ] }, + "DeadLetterConfig": { + "Arn": { + "Fn::GetAtt": [ + "DeadLetterQueue9F481546", + "Arn" + ] + } + }, "Id": "Target0", "RoleArn": { "Fn::GetAtt": [ @@ -394,6 +402,50 @@ } } }, + "DeadLetterQueue9F481546": { + "Type": "AWS::SQS::Queue", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "DeadLetterQueuePolicyB1FB890C": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Fn::GetAtt": [ + "MyRepoOnCommit0E80B304", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": [ + "DeadLetterQueue9F481546", + "Arn" + ] + }, + "Sid": "AllowEventRuleawscdkcodebuildeventsMyRepoOnCommit0ED1137A" + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "DeadLetterQueue9F481546" + } + ] + } + }, "MyTopic86869434": { "Type": "AWS::SNS::Topic" }, diff --git a/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.ts b/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.ts index 6d17e0e01c688..7974a4188f969 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.ts +++ b/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.ts @@ -20,6 +20,7 @@ const project = new codebuild.Project(stack, 'MyProject', { }); const queue = new sqs.Queue(stack, 'MyQueue'); +const deadLetterQueue = new sqs.Queue(stack, 'DeadLetterQueue'); const topic = new sns.Topic(stack, 'MyTopic'); topic.addSubscription(new subs.SqsSubscription(queue)); @@ -39,7 +40,9 @@ project.onPhaseChange('PhaseChange', { // trigger a build when a commit is pushed to the repo const onCommitRule = repo.onCommit('OnCommit', { - target: new targets.CodeBuildProject(project), + target: new targets.CodeBuildProject(project, { + deadLetterQueue: deadLetterQueue, + }), branches: ['master'], }); diff --git a/packages/@aws-cdk/aws-events/README.md b/packages/@aws-cdk/aws-events/README.md index 565b0537d091d..bb0e434837131 100644 --- a/packages/@aws-cdk/aws-events/README.md +++ b/packages/@aws-cdk/aws-events/README.md @@ -83,6 +83,7 @@ onCommitRule.addTarget(new targets.SnsTopic(topic, { ## Scheduling You can configure a Rule to run on a schedule (cron or rate). +Rate must be specified in minutes, hours or days. The following example runs a task every day at 4am: @@ -186,3 +187,17 @@ bus.archive('MyArchive', { retention: cdk.Duration.days(365), }); ``` + +## Granting PutEvents to an existing EventBus + +To import an existing EventBus into your CDK application, use `EventBus.fromEventBusArn` or `EventBus.fromEventBusAttributes` +factory method. + +Then, you can use the `grantPutEventsTo` method to grant `event:PutEvents` to the eventBus. + +```ts +const eventBus = EventBus.fromEventBusArn(this, 'ImportedEventBus', 'arn:aws:events:us-east-1:111111111:event-bus/my-event-bus'); + +// now you can just call methods on the eventbus +eventBus.grantPutEventsTo(lambdaFunction); +``` diff --git a/packages/@aws-cdk/aws-events/lib/event-bus.ts b/packages/@aws-cdk/aws-events/lib/event-bus.ts index cd0c7f913cbf6..2ea9e2300163c 100644 --- a/packages/@aws-cdk/aws-events/lib/event-bus.ts +++ b/packages/@aws-cdk/aws-events/lib/event-bus.ts @@ -47,6 +47,14 @@ export interface IEventBus extends IResource { * @param props Properties of the archive */ archive(id: string, props: BaseArchiveProps): Archive; + + /** + * Grants an IAM Principal to send custom events to the eventBus + * so that they can be matched to rules. + * + * @param grantee The principal (no-op if undefined) + */ + grantPutEventsTo(grantee: iam.IGrantable): iam.Grant; } /** @@ -137,6 +145,14 @@ abstract class EventBusBase extends Resource implements IEventBus { archiveName: props.archiveName, }); } + + public grantPutEventsTo(grantee: iam.IGrantable): iam.Grant { + return iam.Grant.addToPrincipal({ + grantee, + actions: ['events:PutEvents'], + resourceArns: [this.eventBusArn], + }); + } } /** @@ -177,6 +193,7 @@ export class EventBus extends EventBusBase { * so that they can be matched to rules. * * @param grantee The principal (no-op if undefined) + * @deprecated use grantAllPutEvents instead */ public static grantPutEvents(grantee: iam.IGrantable): iam.Grant { // It's currently not possible to restrict PutEvents to specific resources. @@ -188,6 +205,20 @@ export class EventBus extends EventBusBase { }); } + /** + * Permits an IAM Principal to send custom events to EventBridge + * so that they can be matched to rules. + * + * @param grantee The principal (no-op if undefined) + */ + public static grantAllPutEvents(grantee: iam.IGrantable): iam.Grant { + return iam.Grant.addToPrincipal({ + grantee, + actions: ['events:PutEvents'], + resourceArns: ['*'], + }); + } + private static eventBusProps(defaultEventBusName: string, props?: EventBusProps) { if (props) { const { eventBusName, eventSourceName } = props; @@ -282,7 +313,11 @@ class ImportedEventBus extends EventBusBase { public readonly eventBusPolicy: string; public readonly eventSourceName?: string; constructor(scope: Construct, id: string, attrs: EventBusAttributes) { - super(scope, id); + const arnParts = Stack.of(scope).parseArn(attrs.eventBusArn); + super(scope, id, { + account: arnParts.account, + region: arnParts.region, + }); this.eventBusArn = attrs.eventBusArn; this.eventBusName = attrs.eventBusName; diff --git a/packages/@aws-cdk/aws-events/lib/schedule.ts b/packages/@aws-cdk/aws-events/lib/schedule.ts index 8c2c04481c0d2..c12afde30aa01 100644 --- a/packages/@aws-cdk/aws-events/lib/schedule.ts +++ b/packages/@aws-cdk/aws-events/lib/schedule.ts @@ -17,6 +17,13 @@ export abstract class Schedule { * Construct a schedule from an interval and a time unit */ public static rate(duration: Duration): Schedule { + const validDurationUnit = ['minute', 'minutes', 'hour', 'hours', 'day', 'days']; + if (validDurationUnit.indexOf(duration.unitLabel()) === -1) { + throw new Error('Allowed unit for scheduling is: \'minute\', \'minutes\', \'hour\', \'hours\', \'day\', \'days\''); + } + if (duration.isUnresolved()) { + return new LiteralSchedule(`rate(${duration.formatTokenToNumber()})`); + } if (duration.toSeconds() === 0) { throw new Error('Duration cannot be 0'); } diff --git a/packages/@aws-cdk/aws-events/test/test.event-bus.ts b/packages/@aws-cdk/aws-events/test/test.event-bus.ts index 2e8434e147bb1..2babb3f9b659e 100644 --- a/packages/@aws-cdk/aws-events/test/test.event-bus.ts +++ b/packages/@aws-cdk/aws-events/test/test.event-bus.ts @@ -1,6 +1,6 @@ import { expect, haveResource } from '@aws-cdk/assert'; import * as iam from '@aws-cdk/aws-iam'; -import { Aws, CfnResource, Stack } from '@aws-cdk/core'; +import { Aws, CfnResource, Stack, Arn } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import { EventBus } from '../lib'; @@ -55,6 +55,65 @@ export = { test.done(); }, + 'imported event bus'(test: Test) { + const stack = new Stack(); + + const eventBus = new EventBus(stack, 'Bus'); + + const importEB = EventBus.fromEventBusArn(stack, 'ImportBus', eventBus.eventBusArn); + + // WHEN + new CfnResource(stack, 'Res', { + type: 'Test::Resource', + properties: { + EventBusArn1: eventBus.eventBusArn, + EventBusArn2: importEB.eventBusArn, + }, + }); + + expect(stack).to(haveResource('Test::Resource', { + EventBusArn1: { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }, + EventBusArn2: { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }, + })); + + test.done(); + }, + + 'same account imported event bus has right resource env'(test: Test) { + const stack = new Stack(); + + const eventBus = new EventBus(stack, 'Bus'); + + const importEB = EventBus.fromEventBusArn(stack, 'ImportBus', eventBus.eventBusArn); + + // WHEN + test.deepEqual(stack.resolve(importEB.env.account), { 'Fn::Select': [4, { 'Fn::Split': [':', { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }] }] }); + test.deepEqual(stack.resolve(importEB.env.region), { 'Fn::Select': [3, { 'Fn::Split': [':', { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }] }] }); + + test.done(); + }, + + 'cross account imported event bus has right resource env'(test: Test) { + const stack = new Stack(); + + const arnParts = { + resource: 'bus', + service: 'events', + account: 'myAccount', + region: 'us-west-1', + }; + + const arn = Arn.format(arnParts, stack); + + const importEB = EventBus.fromEventBusArn(stack, 'ImportBus', arn); + + // WHEN + test.deepEqual(importEB.env.account, arnParts.account); + test.deepEqual(importEB.env.region, arnParts.region); + + test.done(); + }, + 'can get bus name'(test: Test) { // GIVEN const stack = new Stack(); @@ -247,6 +306,76 @@ export = { test.done(); }, + + 'can grant PutEvents using grantAllPutEvents'(test: Test) { + // GIVEN + const stack = new Stack(); + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + + // WHEN + EventBus.grantAllPutEvents(role); + + // THEN + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'events:PutEvents', + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + Roles: [ + { + Ref: 'Role1ABCC5F0', + }, + ], + })); + + test.done(); + }, + 'can grant PutEvents to a specific event bus'(test: Test) { + // GIVEN + const stack = new Stack(); + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + + const eventBus = new EventBus(stack, 'EventBus'); + + // WHEN + eventBus.grantPutEventsTo(role); + + // THEN + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'events:PutEvents', + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'EventBus7B8748AA', + 'Arn', + ], + }, + }, + ], + Version: '2012-10-17', + }, + Roles: [ + { + Ref: 'Role1ABCC5F0', + }, + ], + })); + + test.done(); + }, 'can archive events'(test: Test) { // GIVEN const stack = new Stack(); diff --git a/packages/@aws-cdk/aws-events/test/test.rule.ts b/packages/@aws-cdk/aws-events/test/test.rule.ts index 80cf91948f5fa..72fe340242924 100644 --- a/packages/@aws-cdk/aws-events/test/test.rule.ts +++ b/packages/@aws-cdk/aws-events/test/test.rule.ts @@ -49,6 +49,39 @@ export = { test.done(); }, + 'get rate as token'(test: Test) { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'MyScheduledStack'); + const lazyDuration = cdk.Duration.minutes(cdk.Lazy.number({ produce: () => 5 })); + + new Rule(stack, 'MyScheduledRule', { + ruleName: 'rateInMinutes', + schedule: Schedule.rate(lazyDuration), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::Events::Rule', { + 'Name': 'rateInMinutes', + 'ScheduleExpression': 'rate(5 minutes)', + })); + + test.done(); + }, + + 'Seconds is not an allowed value for Schedule rate'(test: Test) { + const lazyDuration = cdk.Duration.seconds(cdk.Lazy.number({ produce: () => 5 })); + test.throws(() => Schedule.rate(lazyDuration), /Allowed unit for scheduling is: 'minute', 'minutes', 'hour', 'hours', 'day', 'days'/); + test.done(); + }, + + 'Millis is not an allowed value for Schedule rate'(test: Test) { + const lazyDuration = cdk.Duration.millis(cdk.Lazy.number({ produce: () => 5 })); + + // THEN + test.throws(() => Schedule.rate(lazyDuration), /Allowed unit for scheduling is: 'minute', 'minutes', 'hour', 'hours', 'day', 'days'/); + test.done(); + }, + 'rule with physical name'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-events/test/test.schedule.ts b/packages/@aws-cdk/aws-events/test/test.schedule.ts index d2e045199360f..e9c24a51c92b2 100644 --- a/packages/@aws-cdk/aws-events/test/test.schedule.ts +++ b/packages/@aws-cdk/aws-events/test/test.schedule.ts @@ -1,4 +1,4 @@ -import { Duration } from '@aws-cdk/core'; +import { Duration, Stack, Lazy } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import * as events from '../lib'; @@ -33,8 +33,15 @@ export = { 'rate must be whole number of minutes'(test: Test) { test.throws(() => { - events.Schedule.rate(Duration.seconds(12345)); - }, /'12345 seconds' cannot be converted into a whole number of minutes/); + events.Schedule.rate(Duration.minutes(0.13456)); + }, /'0.13456 minutes' cannot be converted into a whole number of seconds/); + test.done(); + }, + + 'rate must be whole number'(test: Test) { + test.throws(() => { + events.Schedule.rate(Duration.minutes(1/8)); + }, /'0.125 minutes' cannot be converted into a whole number of seconds/); test.done(); }, @@ -44,4 +51,33 @@ export = { }, /Duration cannot be 0/); test.done(); }, + + 'rate can be from a token'(test: Test) { + const stack = new Stack(); + const lazyDuration = Duration.minutes(Lazy.number({ produce: () => 5 })); + const rate = events.Schedule.rate(lazyDuration); + test.equal('rate(5 minutes)', stack.resolve(rate).expressionString); + test.done(); + }, + + 'rate can be in minutes'(test: Test) { + test.equal('rate(10 minutes)', + events.Schedule.rate(Duration.minutes(10)) + .expressionString); + test.done(); + }, + + 'rate can be in days'(test: Test) { + test.equal('rate(10 days)', + events.Schedule.rate(Duration.days(10)) + .expressionString); + test.done(); + }, + + 'rate can be in hours'(test: Test) { + test.equal('rate(10 hours)', + events.Schedule.rate(Duration.hours(10)) + .expressionString); + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-lambda-destinations/lib/event-bridge.ts b/packages/@aws-cdk/aws-lambda-destinations/lib/event-bridge.ts index f61d8409da7bd..9cdcc5c86a83b 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/lib/event-bridge.ts +++ b/packages/@aws-cdk/aws-lambda-destinations/lib/event-bridge.ts @@ -22,15 +22,25 @@ export class EventBridgeDestination implements lambda.IDestination { * Returns a destination configuration */ public bind(_scope: Construct, fn: lambda.IFunction, _options?: lambda.DestinationOptions): lambda.DestinationConfig { - // deduplicated automatically - events.EventBus.grantPutEvents(fn); // Cannot restrict to a specific resource + if (this.eventBus) { + this.eventBus.grantPutEventsTo(fn); + + return { + destination: this.eventBus.eventBusArn, + }; + } + + const existingDefaultEventBus = _scope.node.tryFindChild('DefaultEventBus'); + let eventBus = (existingDefaultEventBus as events.EventBus) || events.EventBus.fromEventBusArn(_scope, 'DefaultEventBus', Stack.of(fn).formatArn({ + service: 'events', + resource: 'event-bus', + resourceName: 'default', + })); + + eventBus.grantPutEventsTo(fn); return { - destination: this.eventBus && this.eventBus.eventBusArn || Stack.of(fn).formatArn({ - service: 'events', - resource: 'event-bus', - resourceName: 'default', - }), + destination: eventBus.eventBusArn, }; } } diff --git a/packages/@aws-cdk/aws-lambda-destinations/test/destinations.test.ts b/packages/@aws-cdk/aws-lambda-destinations/test/destinations.test.ts index 0f8f7d4c85254..70dda9ef43893 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/test/destinations.test.ts +++ b/packages/@aws-cdk/aws-lambda-destinations/test/destinations.test.ts @@ -47,56 +47,12 @@ test('event bus as destination', () => { { Action: 'events:PutEvents', Effect: 'Allow', - Resource: '*', - }, - ], - Version: '2012-10-17', - }, - }); -}); - -test('event bus as destination defaults to default event bus', () => { - // WHEN - new lambda.Function(stack, 'Function', { - ...lambdaProps, - onSuccess: new destinations.EventBridgeDestination(), - }); - - // THEN - expect(stack).toHaveResource('AWS::Lambda::EventInvokeConfig', { - DestinationConfig: { - OnSuccess: { - Destination: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':events:', - { - Ref: 'AWS::Region', - }, - ':', - { - Ref: 'AWS::AccountId', - }, - ':event-bus/default', + Resource: { + 'Fn::GetAtt': [ + 'EventBus7B8748AA', + 'Arn', ], - ], - }, - }, - }, - }); - - expect(stack).toHaveResource('AWS::IAM::Policy', { - PolicyDocument: { - Statement: [ - { - Action: 'events:PutEvents', - Effect: 'Allow', - Resource: '*', + }, }, ], Version: '2012-10-17', @@ -215,7 +171,26 @@ test('lambda payload as destination', () => { { Action: 'events:PutEvents', Effect: 'Allow', - Resource: '*', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':events:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':event-bus/default', + ], + ], + }, }, ], Version: '2012-10-17', diff --git a/packages/@aws-cdk/aws-lambda-destinations/test/integ.destinations.expected.json b/packages/@aws-cdk/aws-lambda-destinations/test/integ.destinations.expected.json index 510fe8cef21a8..009327c46da7e 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/test/integ.destinations.expected.json +++ b/packages/@aws-cdk/aws-lambda-destinations/test/integ.destinations.expected.json @@ -219,7 +219,26 @@ { "Action": "events:PutEvents", "Effect": "Allow", - "Resource": "*" + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":events:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":event-bus/default" + ] + ] + } }, { "Action": "lambda:InvokeFunction", diff --git a/packages/@aws-cdk/aws-lambda-destinations/test/integ.lambda-chain.expected.json b/packages/@aws-cdk/aws-lambda-destinations/test/integ.lambda-chain.expected.json index 5fc64df8417f3..5fc2d65b80387 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/test/integ.lambda-chain.expected.json +++ b/packages/@aws-cdk/aws-lambda-destinations/test/integ.lambda-chain.expected.json @@ -39,7 +39,26 @@ { "Action": "events:PutEvents", "Effect": "Allow", - "Resource": "*" + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":events:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":event-bus/default" + ] + ] + } } ], "Version": "2012-10-17" @@ -289,7 +308,26 @@ { "Action": "events:PutEvents", "Effect": "Allow", - "Resource": "*" + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":events:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":event-bus/default" + ] + ] + } } ], "Version": "2012-10-17" diff --git a/packages/@aws-cdk/aws-neptune/README.md b/packages/@aws-cdk/aws-neptune/README.md index 1be41b88a1022..57ecabab058be 100644 --- a/packages/@aws-cdk/aws-neptune/README.md +++ b/packages/@aws-cdk/aws-neptune/README.md @@ -67,13 +67,13 @@ versions and limitations. The following example shows enabling IAM authentication for a database cluster and granting connection access to an IAM role. ```ts -const cluster = new rds.DatabaseCluster(stack, 'Cluster', { +const cluster = new neptune.DatabaseCluster(this, 'Cluster', { vpc, instanceType: neptune.InstanceType.R5_LARGE, iamAuthentication: true, // Optional - will be automatically set if you call grantConnect(). }); -const role = new Role(stack, 'DBRole', { assumedBy: new AccountPrincipal(stack.account) }); -instance.grantConnect(role); // Grant the role connection access to the DB. +const role = new iam.Role(this, 'DBRole', { assumedBy: new iam.AccountPrincipal(this.account) }); +cluster.grantConnect(role); // Grant the role connection access to the DB. ``` ## Customizing parameters diff --git a/packages/@aws-cdk/aws-neptune/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-neptune/rosetta/default.ts-fixture index 2e687290371fa..63e17311b5dc5 100644 --- a/packages/@aws-cdk/aws-neptune/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/aws-neptune/rosetta/default.ts-fixture @@ -1,5 +1,6 @@ import { Duration, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; +import * as iam from '@aws-cdk/aws-iam'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as neptune from '@aws-cdk/aws-neptune'; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index d7c2d1498394d..69d97936aabcb 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -102,8 +102,8 @@ The following example provides the field named `input` as the input to the `Task state that runs a Lambda function. ```ts -const submitJob = new tasks.LambdaInvoke(stack, 'Invoke Handler', { - lambdaFunction: submitJobLambda, +const submitJob = new tasks.LambdaInvoke(this, 'Invoke Handler', { + lambdaFunction: fn, inputPath: '$.input' }); ``` @@ -122,8 +122,8 @@ as well as other metadata. The following example assigns the output from the Task to a field named `result` ```ts -const submitJob = new tasks.LambdaInvoke(stack, 'Invoke Handler', { - lambdaFunction: submitJobLambda, +const submitJob = new tasks.LambdaInvoke(this, 'Invoke Handler', { + lambdaFunction: fn, outputPath: '$.Payload.result' }); ``` @@ -140,9 +140,11 @@ The following example adds the item from calling DynamoDB's `getItem` API to the input and passes it to the next state. ```ts -new tasks.DynamoGetItem(this, 'PutItem', { - item: { MessageId: { s: '12345'} }, - tableName: 'my-table', +new tasks.DynamoPutItem(this, 'PutItem', { + item: { + MessageId: tasks.DynamoAttributeValue.fromString('message-id') + }, + table: myTable, resultPath: `$.Item`, }); ``` @@ -163,10 +165,10 @@ The following example provides the field named `input` as the input to the Lambd and invokes it asynchronously. ```ts -const submitJob = new tasks.LambdaInvoke(stack, 'Invoke Handler', { - lambdaFunction: submitJobLambda, - payload: sfn.JsonPath.StringAt('$.input'), - invocationType: tasks.InvocationType.EVENT, +const submitJob = new tasks.LambdaInvoke(this, 'Invoke Handler', { + lambdaFunction: fn, + payload: sfn.TaskInput.fromDataAt('$.input'), + invocationType: tasks.LambdaInvocationType.EVENT, }); ``` @@ -194,7 +196,7 @@ const createMessage = new tasks.EvaluateExpression(this, 'Create message', { }); const publishMessage = new tasks.SnsPublish(this, 'Publish message', { - topic, + topic: new sns.Topic(this, 'cool-topic'), message: sfn.TaskInput.fromDataAt('$.message'), resultPath: '$.sns', }); @@ -224,19 +226,19 @@ Step Functions supports [Athena](https://docs.aws.amazon.com/step-functions/late The [StartQueryExecution](https://docs.aws.amazon.com/athena/latest/APIReference/API_StartQueryExecution.html) API runs the SQL query statement. ```ts -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as tasks from `@aws-cdk/aws-stepfunctions-tasks`; - -const startQueryExecutionJob = new tasks.AthenaStartQueryExecution(stack, 'Start Athena Query', { +const startQueryExecutionJob = new tasks.AthenaStartQueryExecution(this, 'Start Athena Query', { queryString: sfn.JsonPath.stringAt('$.queryString'), queryExecutionContext: { - database: 'mydatabase', + databaseName: 'mydatabase', }, resultConfiguration: { encryptionConfiguration: { encryptionOption: tasks.EncryptionOption.S3_MANAGED, }, - outputLocation: sfn.JsonPath.stringAt('$.outputLocation'), + outputLocation: { + bucketName: 'query-results-bucket', + objectKey: 'folder', + }, }, }); ``` @@ -246,10 +248,7 @@ const startQueryExecutionJob = new tasks.AthenaStartQueryExecution(stack, 'Start The [GetQueryExecution](https://docs.aws.amazon.com/athena/latest/APIReference/API_GetQueryExecution.html) API gets information about a single execution of a query. ```ts -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as tasks from `@aws-cdk/aws-stepfunctions-tasks`; - -const getQueryExecutionJob = new tasks.AthenaGetQueryExecution(stack, 'Get Query Execution', { +const getQueryExecutionJob = new tasks.AthenaGetQueryExecution(this, 'Get Query Execution', { queryExecutionId: sfn.JsonPath.stringAt('$.QueryExecutionId'), }); ``` @@ -259,10 +258,7 @@ const getQueryExecutionJob = new tasks.AthenaGetQueryExecution(stack, 'Get Query The [GetQueryResults](https://docs.aws.amazon.com/athena/latest/APIReference/API_GetQueryResults.html) API that streams the results of a single query execution specified by QueryExecutionId from S3. ```ts -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as tasks from `@aws-cdk/aws-stepfunctions-tasks`; - -const getQueryResultsJob = new tasks.AthenaGetQueryResults(stack, 'Get Query Results', { +const getQueryResultsJob = new tasks.AthenaGetQueryResults(this, 'Get Query Results', { queryExecutionId: sfn.JsonPath.stringAt('$.QueryExecutionId'), }); ``` @@ -272,10 +268,7 @@ const getQueryResultsJob = new tasks.AthenaGetQueryResults(stack, 'Get Query Res The [StopQueryExecution](https://docs.aws.amazon.com/athena/latest/APIReference/API_StopQueryExecution.html) API that stops a query execution. ```ts -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as tasks from `@aws-cdk/aws-stepfunctions-tasks`; - -const stopQueryExecutionJob = new tasks.AthenaStopQueryExecution(stack, 'Stop Query Execution', { +const stopQueryExecutionJob = new tasks.AthenaStopQueryExecution(this, 'Stop Query Execution', { queryExecutionId: sfn.JsonPath.stringAt('$.QueryExecutionId'), }); ``` @@ -288,27 +281,7 @@ Step Functions supports [Batch](https://docs.aws.amazon.com/step-functions/lates The [SubmitJob](https://docs.aws.amazon.com/batch/latest/APIReference/API_SubmitJob.html) API submits an AWS Batch job from a job definition. -```ts -import * as batch from '@aws-cdk/aws-batch'; -import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; - -const batchQueue = new batch.JobQueue(this, 'JobQueue', { - computeEnvironments: [ - { - order: 1, - computeEnvironment: new batch.ComputeEnvironment(this, 'ComputeEnv', { - computeResources: { vpc }, - }), - }, - ], -}); - -const batchJobDefinition = new batch.JobDefinition(this, 'JobDefinition', { - container: { - image: ecs.ContainerImage.fromAsset(path.resolve(__dirname, 'batchjob-image')), - }, -}); - +```ts fixture=with-batch-job const task = new tasks.BatchSubmitJob(this, 'Submit Job', { jobDefinition: batchJobDefinition, jobName: 'MyJob', @@ -326,10 +299,8 @@ Step Functions supports [CodeBuild](https://docs.aws.amazon.com/step-functions/l ```ts import * as codebuild from '@aws-cdk/aws-codebuild'; -import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; -import * as sfn from '@aws-cdk/aws-stepfunctions'; -const codebuildProject = new codebuild.Project(stack, 'Project', { +const codebuildProject = new codebuild.Project(this, 'Project', { projectName: 'MyTestProject', buildSpec: codebuild.BuildSpec.fromObject({ version: '0.2', @@ -343,7 +314,7 @@ const codebuildProject = new codebuild.Project(stack, 'Project', { }), }); -const task = new tasks.CodeBuildStartBuild(stack, 'Task', { +const task = new tasks.CodeBuildStartBuild(this, 'Task', { project: codebuildProject, integrationPattern: sfn.IntegrationPattern.RUN_JOB, environmentVariablesOverride: { @@ -367,7 +338,7 @@ The [GetItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API ```ts new tasks.DynamoGetItem(this, 'Get Item', { key: { messageId: tasks.DynamoAttributeValue.fromString('message-007') }, - table, + table: myTable, }); ``` @@ -382,7 +353,7 @@ new tasks.DynamoPutItem(this, 'PutItem', { Text: tasks.DynamoAttributeValue.fromString(sfn.JsonPath.stringAt('$.bar')), TotalCount: tasks.DynamoAttributeValue.fromNumber(10), }, - table, + table: myTable, }); ``` @@ -391,12 +362,9 @@ new tasks.DynamoPutItem(this, 'PutItem', { The [DeleteItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html) operation deletes a single item in a table by primary key. ```ts -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; - new tasks.DynamoDeleteItem(this, 'DeleteItem', { key: { MessageId: tasks.DynamoAttributeValue.fromString('message-007') }, - table, + table: myTable, resultPath: sfn.JsonPath.DISCARD, }); ``` @@ -408,8 +376,10 @@ to the table if it does not already exist. ```ts new tasks.DynamoUpdateItem(this, 'UpdateItem', { - key: { MessageId: tasks.DynamoAttributeValue.fromString('message-007') }, - table, + key: { + MessageId: tasks.DynamoAttributeValue.fromString('message-007') + }, + table: myTable, expressionAttributeValues: { ':val': tasks.DynamoAttributeValue.numberFromString(sfn.JsonPath.stringAt('$.Item.TotalCount.N')), ':rand': tasks.DynamoAttributeValue.fromNumber(20), @@ -443,20 +413,18 @@ The following example runs a job from a task definition on EC2 ```ts import * as ecs from '@aws-cdk/aws-ecs'; -import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; -import * as sfn from '@aws-cdk/aws-stepfunctions'; -const vpc = ec2.Vpc.fromLookup(stack, 'Vpc', { +const vpc = ec2.Vpc.fromLookup(this, 'Vpc', { isDefault: true, }); -const cluster = new ecs.Cluster(stack, 'Ec2Cluster', { vpc }); +const cluster = new ecs.Cluster(this, 'Ec2Cluster', { vpc }); cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro'), vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, }); -const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { +const taskDefinition = new ecs.TaskDefinition(this, 'TD', { compatibility: ecs.Compatibility.EC2, }); @@ -465,7 +433,7 @@ taskDefinition.addContainer('TheContainer', { memoryLimitMiB: 256, }); -const runTask = new tasks.EcsRunTask(stack, 'Run', { +const runTask = new tasks.EcsRunTask(this, 'Run', { integrationPattern: sfn.IntegrationPattern.RUN_JOB, cluster, taskDefinition, @@ -500,16 +468,14 @@ The following example runs a job from a task definition on Fargate ```ts import * as ecs from '@aws-cdk/aws-ecs'; -import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; -import * as sfn from '@aws-cdk/aws-stepfunctions'; -const vpc = ec2.Vpc.fromLookup(stack, 'Vpc', { +const vpc = ec2.Vpc.fromLookup(this, 'Vpc', { isDefault: true, }); -const cluster = new ecs.Cluster(stack, 'FargateCluster', { vpc }); +const cluster = new ecs.Cluster(this, 'FargateCluster', { vpc }); -const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { +const taskDefinition = new ecs.TaskDefinition(this, 'TD', { memoryMiB: '512', cpu: '256', compatibility: ecs.Compatibility.FARGATE, @@ -520,7 +486,7 @@ const containerDefinition = taskDefinition.addContainer('TheContainer', { memoryLimitMiB: 256, }); -const runTask = new tasks.EcsRunTask(stack, 'RunFargate', { +const runTask = new tasks.EcsRunTask(this, 'RunFargate', { integrationPattern: sfn.IntegrationPattern.RUN_JOB, cluster, taskDefinition, @@ -548,15 +514,15 @@ Corresponds to the [`runJobFlow`](https://docs.aws.amazon.com/emr/latest/APIRefe ```ts -const clusterRole = new iam.Role(stack, 'ClusterRole', { +const clusterRole = new iam.Role(this, 'ClusterRole', { assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'), }); -const serviceRole = new iam.Role(stack, 'ServiceRole', { +const serviceRole = new iam.Role(this, 'ServiceRole', { assumedBy: new iam.ServicePrincipal('elasticmapreduce.amazonaws.com'), }); -const autoScalingRole = new iam.Role(stack, 'AutoScalingRole', { +const autoScalingRole = new iam.Role(this, 'AutoScalingRole', { assumedBy: new iam.ServicePrincipal('elasticmapreduce.amazonaws.com'), }); @@ -569,16 +535,15 @@ autoScalingRole.assumeRolePolicy?.addStatements( actions: [ 'sts:AssumeRole', ], - }); + })); ) -new tasks.EmrCreateCluster(stack, 'Create Cluster', { +new tasks.EmrCreateCluster(this, 'Create Cluster', { instances: {}, clusterRole, name: sfn.TaskInput.fromDataAt('$.ClusterName').value, serviceRole, autoScalingRole, - integrationPattern: sfn.ServiceIntegrationPattern.FIRE_AND_FORGET, }); ``` @@ -590,7 +555,7 @@ terminated by user intervention, an API call, or a job-flow error. Corresponds to the [`setTerminationProtection`](https://docs.aws.amazon.com/step-functions/latest/dg/connect-emr.html) API in EMR. ```ts -new tasks.EmrSetClusterTerminationProtection(stack, 'Task', { +new tasks.EmrSetClusterTerminationProtection(this, 'Task', { clusterId: 'ClusterId', terminationProtected: false, }); @@ -602,7 +567,7 @@ Shuts down a cluster (job flow). Corresponds to the [`terminateJobFlows`](https://docs.aws.amazon.com/emr/latest/APIReference/API_TerminateJobFlows.html) API in EMR. ```ts -new tasks.EmrTerminateCluster(stack, 'Task', { +new tasks.EmrTerminateCluster(this, 'Task', { clusterId: 'ClusterId' }); ``` @@ -613,7 +578,7 @@ Adds a new step to a running cluster. Corresponds to the [`addJobFlowSteps`](https://docs.aws.amazon.com/emr/latest/APIReference/API_AddJobFlowSteps.html) API in EMR. ```ts -new tasks.EmrAddStep(stack, 'Task', { +new tasks.EmrAddStep(this, 'Task', { clusterId: 'ClusterId', name: 'StepName', jar: 'Jar', @@ -627,7 +592,7 @@ Cancels a pending step in a running cluster. Corresponds to the [`cancelSteps`](https://docs.aws.amazon.com/emr/latest/APIReference/API_CancelSteps.html) API in EMR. ```ts -new tasks.EmrCancelStep(stack, 'Task', { +new tasks.EmrCancelStep(this, 'Task', { clusterId: 'ClusterId', stepId: 'StepId', }); @@ -641,7 +606,7 @@ fleet with the specified InstanceFleetName. Corresponds to the [`modifyInstanceFleet`](https://docs.aws.amazon.com/emr/latest/APIReference/API_ModifyInstanceFleet.html) API in EMR. ```ts -new sfn.EmrModifyInstanceFleetByName(stack, 'Task', { +new tasks.EmrModifyInstanceFleetByName(this, 'Task', { clusterId: 'ClusterId', instanceFleetName: 'InstanceFleetName', targetOnDemandCapacity: 2, @@ -656,7 +621,7 @@ Modifies the number of nodes and configuration settings of an instance group. Corresponds to the [`modifyInstanceGroups`](https://docs.aws.amazon.com/emr/latest/APIReference/API_ModifyInstanceGroups.html) API in EMR. ```ts -new tasks.EmrModifyInstanceGroupByName(stack, 'Task', { +new tasks.EmrModifyInstanceGroupByName(this, 'Task', { clusterId: 'ClusterId', instanceGroupName: sfn.JsonPath.stringAt('$.InstanceGroupName'), instanceGroup: { @@ -703,11 +668,11 @@ Step Functions supports [AWS Glue](https://docs.aws.amazon.com/step-functions/la You can call the [`StartJobRun`](https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-jobs-runs.html#aws-glue-api-jobs-runs-StartJobRun) API from a `Task` state. ```ts -new GlueStartJobRun(stack, 'Task', { +new tasks.GlueStartJobRun(this, 'Task', { glueJobName: 'my-glue-job', - arguments: { - key: 'value', - }, + arguments: sfn.TaskInput.fromObject({ + key: 'value', + }), timeout: cdk.Duration.minutes(30), notifyDelayAfter: cdk.Duration.minutes(5), }); @@ -720,8 +685,8 @@ Step Functions supports [AWS Glue DataBrew](https://docs.aws.amazon.com/step-fun You can call the [`StartJobRun`](https://docs.aws.amazon.com/databrew/latest/dg/API_StartJobRun.html) API from a `Task` state. ```ts -new GlueDataBrewStartJobRun(stack, 'Task', { - Name: 'databrew-job', +new tasks.GlueDataBrewStartJobRun(this, 'Task', { + name: 'databrew-job', }); ``` @@ -737,23 +702,8 @@ The following snippet invokes a Lambda Function with the state input as the payl by referencing the `$` path. ```ts -import * as lambda from '@aws-cdk/aws-lambda'; -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; - -const myLambda = new lambda.Function(this, 'my sample lambda', { - code: Code.fromInline(`exports.handler = async () => { - return { - statusCode: '200', - body: 'hello, world!' - }; - };`), - runtime: Runtime.NODEJS_12_X, - handler: 'index.handler', -}); - new tasks.LambdaInvoke(this, 'Invoke with state input', { - lambdaFunction: myLambda, + lambdaFunction: fn, }); ``` @@ -768,13 +718,13 @@ to reference the output of a Lambda executed before it. ```ts new tasks.LambdaInvoke(this, 'Invoke with empty object as payload', { - lambdaFunction: myLambda, + lambdaFunction: fn, payload: sfn.TaskInput.fromObject({}), }); -// use the output of myLambda as input +// use the output of fn as input new tasks.LambdaInvoke(this, 'Invoke with payload field in the state input', { - lambdaFunction: myOtherLambda, + lambdaFunction: fn, payload: sfn.TaskInput.fromDataAt('$.Payload'), }); ``` @@ -784,8 +734,7 @@ the Lambda function response. ```ts new tasks.LambdaInvoke(this, 'Invoke and set function response as task output', { - lambdaFunction: myLambda, - payload: sfn.TaskInput.fromDataAt('$'), + lambdaFunction: fn, outputPath: '$.Payload', }); ``` @@ -797,9 +746,9 @@ integrationPattern, invocationType, clientContext, and qualifier properties. ```ts new tasks.LambdaInvoke(this, 'Invoke and combine function response with task input', { - lambdaFunction: myLambda, + lambdaFunction: fn, payloadResponseOnly: true, - resultPath: '$.myLambda', + resultPath: '$.fn', }); ``` @@ -814,8 +763,8 @@ The following snippet invokes a Lambda with the task token as part of the input to the Lambda. ```ts -new tasks.LambdaInvoke(stack, 'Invoke with callback', { - lambdaFunction: myLambda, +new tasks.LambdaInvoke(this, 'Invoke with callback', { + lambdaFunction: fn, integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, payload: sfn.TaskInput.fromObject({ token: sfn.JsonPath.taskToken, @@ -830,7 +779,7 @@ Token](https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource. AWS Lambda can occasionally experience transient service errors. In this case, invoking Lambda results in a 500 error, such as `ServiceException`, `AWSLambdaException`, or `SdkClientException`. -As a best practive, the `LambdaInvoke` task will retry on those errors with an interval of 2 seconds, +As a best practice, the `LambdaInvoke` task will retry on those errors with an interval of 2 seconds, a back-off rate of 2 and 6 maximum attempts. Set the `retryOnServiceExceptions` prop to `false` to disable this behavior. @@ -843,9 +792,8 @@ Step Functions supports [AWS SageMaker](https://docs.aws.amazon.com/step-functio You can call the [`CreateTrainingJob`](https://docs.aws.amazon.com/sagemaker/latest/dg/API_CreateTrainingJob.html) API from a `Task` state. ```ts -new sfn.SageMakerCreateTrainingJob(this, 'TrainSagemaker', { +new tasks.SageMakerCreateTrainingJob(this, 'TrainSagemaker', { trainingJobName: sfn.JsonPath.stringAt('$.JobName'), - role, algorithmSpecification: { algorithmName: 'BlazingText', trainingInputMode: tasks.InputMode.FILE, @@ -860,16 +808,16 @@ new sfn.SageMakerCreateTrainingJob(this, 'TrainSagemaker', { }, }], outputDataConfig: { - s3OutputLocation: tasks.S3Location.fromBucket(s3.Bucket.fromBucketName(stack, 'Bucket', 'mybucket'), 'myoutputpath'), + s3OutputLocation: tasks.S3Location.fromBucket(s3.Bucket.fromBucketName(this, 'Bucket', 'mybucket'), 'myoutputpath'), }, resourceConfig: { instanceCount: 1, instanceType: ec2.InstanceType.of(ec2.InstanceClass.P3, ec2.InstanceSize.XLARGE2), volumeSize: cdk.Size.gibibytes(50), - }, + }, // optional: default is 1 instance of EC2 `M4.XLarge` with `10GB` volume stoppingCondition: { - maxRuntime: cdk.Duration.hours(1), - }, + maxRuntime: cdk.Duration.hours(2), + }, // optional: default is 1 hour }); ``` @@ -878,19 +826,18 @@ new sfn.SageMakerCreateTrainingJob(this, 'TrainSagemaker', { You can call the [`CreateTransformJob`](https://docs.aws.amazon.com/sagemaker/latest/dg/API_CreateTransformJob.html) API from a `Task` state. ```ts -new sfn.SageMakerCreateTransformJob(this, 'Batch Inference', { +new tasks.SageMakerCreateTransformJob(this, 'Batch Inference', { transformJobName: 'MyTransformJob', modelName: 'MyModelName', modelClientOptions: { - invocationMaxRetries: 3, // default is 0 - invocationTimeout: cdk.Duration.minutes(5), // default is 60 seconds - } - role, + invocationsMaxRetries: 3, // default is 0 + invocationsTimeout: cdk.Duration.minutes(5), // default is 60 seconds + }, transformInput: { transformDataSource: { s3DataSource: { s3Uri: 's3://inputbucket/train', - s3DataType: S3DataType.S3Prefix, + s3DataType: tasks.S3DataType.S3_PREFIX, } } }, @@ -899,7 +846,7 @@ new sfn.SageMakerCreateTransformJob(this, 'Batch Inference', { }, transformResources: { instanceCount: 1, - instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.XLarge), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.XLARGE), } }); @@ -910,7 +857,7 @@ new sfn.SageMakerCreateTransformJob(this, 'Batch Inference', { You can call the [`CreateEndpoint`](https://docs.aws.amazon.com/sagemaker/latest/APIReference/API_CreateEndpoint.html) API from a `Task` state. ```ts -new sfn.SageMakerCreateEndpoint(this, 'SagemakerEndpoint', { +new tasks.SageMakerCreateEndpoint(this, 'SagemakerEndpoint', { endpointName: sfn.JsonPath.stringAt('$.EndpointName'), endpointConfigName: sfn.JsonPath.stringAt('$.EndpointConfigName'), }); @@ -921,7 +868,7 @@ new sfn.SageMakerCreateEndpoint(this, 'SagemakerEndpoint', { You can call the [`CreateEndpointConfig`](https://docs.aws.amazon.com/sagemaker/latest/APIReference/API_CreateEndpointConfig.html) API from a `Task` state. ```ts -new sfn.SageMakerCreateEndpointConfig(this, 'SagemakerEndpointConfig', { +new tasks.SageMakerCreateEndpointConfig(this, 'SagemakerEndpointConfig', { endpointConfigName: 'MyEndpointConfig', productionVariants: [{ initialInstanceCount: 2, @@ -937,7 +884,7 @@ new sfn.SageMakerCreateEndpointConfig(this, 'SagemakerEndpointConfig', { You can call the [`CreateModel`](https://docs.aws.amazon.com/sagemaker/latest/APIReference/API_CreateModel.html) API from a `Task` state. ```ts -new sfn.SageMakerCreateModel(this, 'Sagemaker', { +new tasks.SageMakerCreateModel(this, 'Sagemaker', { modelName: 'MyModel', primaryContainer: new tasks.ContainerDefinition({ image: tasks.DockerImage.fromJsonExpression(sfn.JsonPath.stringAt('$.Model.imageName')), @@ -952,7 +899,7 @@ new sfn.SageMakerCreateModel(this, 'Sagemaker', { You can call the [`UpdateEndpoint`](https://docs.aws.amazon.com/sagemaker/latest/APIReference/API_UpdateEndpoint.html) API from a `Task` state. ```ts -new sfn.SageMakerUpdateEndpoint(this, 'SagemakerEndpoint', { +new tasks.SageMakerUpdateEndpoint(this, 'SagemakerEndpoint', { endpointName: sfn.JsonPath.stringAt('$.Endpoint.Name'), endpointConfigName: sfn.JsonPath.stringAt('$.Endpoint.EndpointConfig'), }); @@ -965,12 +912,6 @@ Step Functions supports [Amazon SNS](https://docs.aws.amazon.com/step-functions/ You can call the [`Publish`](https://docs.aws.amazon.com/sns/latest/api/API_Publish.html) API from a `Task` state to publish to an SNS topic. ```ts -import * as sns from '@aws-cdk/aws-sns'; -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; - -// ... - const topic = new sns.Topic(this, 'Topic'); // Use a field from the execution data as message. @@ -1001,12 +942,12 @@ AWS Step Functions supports it's own [`StartExecution`](https://docs.aws.amazon. ```ts // Define a state machine with one Pass state -const child = new sfn.StateMachine(stack, 'ChildStateMachine', { - definition: sfn.Chain.start(new sfn.Pass(stack, 'PassState')), +const child = new sfn.StateMachine(this, 'ChildStateMachine', { + definition: sfn.Chain.start(new sfn.Pass(this, 'PassState')), }); // Include the state machine in a Task state with callback pattern -const task = new StepFunctionsStartExecution(stack, 'ChildTask', { +const task = new tasks.StepFunctionsStartExecution(this, 'ChildTask', { stateMachine: child, integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, input: sfn.TaskInput.fromObject({ @@ -1017,7 +958,7 @@ const task = new StepFunctionsStartExecution(stack, 'ChildTask', { }); // Define a second state machine with the Task state above -new sfn.StateMachine(stack, 'ParentStateMachine', { +new sfn.StateMachine(this, 'ParentStateMachine', { definition: task }); ``` @@ -1057,12 +998,6 @@ You can call the [`SendMessage`](https://docs.aws.amazon.com/AWSSimpleQueueServi to send a message to an SQS queue. ```ts -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; -import * as sqs from '@aws-cdk/aws-sqs'; - -// ... - const queue = new sqs.Queue(this, 'Queue'); // Use a field from the execution data as message. diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-stepfunctions-tasks/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..11558a599e5f4 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/rosetta/default.ts-fixture @@ -0,0 +1,37 @@ +// Fixture with packages imported, but nothing else +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as ddb from '@aws-cdk/aws-dynamodb'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as sns from '@aws-cdk/aws-sns'; +import * as sqs from '@aws-cdk/aws-sqs'; +import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; + +class Fixture extends cdk.Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const fn = new lambda.Function(this, 'lambdaFunction', { + code: lambda.Code.fromInline(`exports.handler = async () => { + return { "hello world"}; + `), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + }); + + const myTable = new ddb.Table(this, 'Messages', { + tableName: 'my-table', + partitionKey: { + name: 'MessageId', + type: ddb.AttributeType.STRING, + }, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/rosetta/with-batch-job.ts-fixture b/packages/@aws-cdk/aws-stepfunctions-tasks/rosetta/with-batch-job.ts-fixture new file mode 100644 index 0000000000000..47672ba140841 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/rosetta/with-batch-job.ts-fixture @@ -0,0 +1,38 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; +import * as batch from '@aws-cdk/aws-batch'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as path from 'path'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const vpc = ec2.Vpc.fromLookup(this, 'Vpc', { + isDefault: true, + }); + + const batchQueue = new batch.JobQueue(this, 'JobQueue', { + computeEnvironments: [ + { + order: 1, + computeEnvironment: new batch.ComputeEnvironment(this, 'ComputeEnv', { + computeResources: { vpc }, + }), + }, + ], + }); + + const batchJobDefinition = new batch.JobDefinition(this, 'JobDefinition', { + container: { + image: ecs.ContainerImage.fromAsset(path.resolve(__dirname, 'batchjob-image')), + }, + }); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts index cced1d4519660..fa97eccad71f4 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts @@ -46,7 +46,7 @@ export enum LogLevel { /** * Log all errors */ - ERROR= 'ERROR', + ERROR = 'ERROR', /** * Log fatal errors */ @@ -379,6 +379,10 @@ export class StateMachine extends StateMachineBase { physicalName: props.stateMachineName, }); + if (props.stateMachineName != undefined) { + this.validateStateMachineName(props.stateMachineName); + } + this.role = props.role || new iam.Role(this, 'Role', { assumedBy: new iam.ServicePrincipal('states.amazonaws.com'), }); @@ -426,6 +430,15 @@ export class StateMachine extends StateMachineBase { this.role.addToPrincipalPolicy(statement); } + private validateStateMachineName(stateMachineName: string) { + if (stateMachineName.length < 1 || stateMachineName.length > 80) { + throw new Error(`State Machine name must be between 1 and 80 characters. Received: ${stateMachineName}`); + } + if (!stateMachineName.match('^[0-9a-zA-Z+!@._-]+$')) { + throw new Error(`State Machine name must match "^[0-9a-zA-Z+!@._-]+$". Received: ${stateMachineName}`); + } + } + private buildLoggingConfiguration(logOptions: LogOptions): CfnStateMachine.LoggingConfigurationProperty { // https://docs.aws.amazon.com/step-functions/latest/dg/cw-logs.html#cloudwatch-iam-policy this.addToRolePolicy(new iam.PolicyStatement({ diff --git a/packages/@aws-cdk/aws-stepfunctions/test/state-machine.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/state-machine.test.ts index e721b460a5358..1c26947659a54 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/state-machine.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/state-machine.test.ts @@ -62,6 +62,36 @@ describe('State Machine', () => { }), + test('State Machine with invalid name', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const createStateMachine = (name: string) => { + new stepfunctions.StateMachine(stack, name + 'StateMachine', { + stateMachineName: name, + definition: stepfunctions.Chain.start(new stepfunctions.Pass(stack, name + 'Pass')), + stateMachineType: stepfunctions.StateMachineType.EXPRESS, + }); + }; + const tooShortName = ''; + const tooLongName = 'M'.repeat(81); + const invalidCharactersName = '*'; + + // THEN + expect(() => { + createStateMachine(tooShortName); + }).toThrow(`State Machine name must be between 1 and 80 characters. Received: ${tooShortName}`); + + expect(() => { + createStateMachine(tooLongName); + }).toThrow(`State Machine name must be between 1 and 80 characters. Received: ${tooLongName}`); + + expect(() => { + createStateMachine(invalidCharactersName); + }).toThrow(`State Machine name must match "^[0-9a-zA-Z+!@._-]+$". Received: ${invalidCharactersName}`); + }); + test('log configuration', () => { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/core/lib/duration.ts b/packages/@aws-cdk/core/lib/duration.ts index 098744af2d0b8..b1c675d3ce722 100644 --- a/packages/@aws-cdk/core/lib/duration.ts +++ b/packages/@aws-cdk/core/lib/duration.ts @@ -1,4 +1,4 @@ -import { Token } from './token'; +import { Token, Tokenization } from './token'; /** * Represents a length of time. @@ -252,6 +252,28 @@ export class Duration { } return ret; } + + /** + * Checks if duration is a token or a resolvable object + */ + public isUnresolved() { + return Token.isUnresolved(this.amount); + } + + /** + * Returns unit of the duration + */ + public unitLabel() { + return this.unit.label; + } + + /** + * Returns stringified number of duration + */ + public formatTokenToNumber(): string { + const number = Tokenization.stringifyNumber(this.amount); + return `${number} ${this.unit.label}`; + } } /** diff --git a/packages/@aws-cdk/core/test/duration.test.ts b/packages/@aws-cdk/core/test/duration.test.ts index 70b7cb786e344..75b92c76924c2 100644 --- a/packages/@aws-cdk/core/test/duration.test.ts +++ b/packages/@aws-cdk/core/test/duration.test.ts @@ -168,6 +168,32 @@ nodeunitShim({ test.done(); }, + + 'get unit label from duration'(test: Test) { + test.equal(Duration.minutes(Lazy.number({ produce: () => 10 })).unitLabel(), 'minutes'); + test.equal(Duration.minutes(62).unitLabel(), 'minutes'); + test.equal(Duration.seconds(10).unitLabel(), 'seconds'); + test.equal(Duration.millis(1).unitLabel(), 'millis'); + test.equal(Duration.hours(1000).unitLabel(), 'hours'); + test.equal(Duration.days(2).unitLabel(), 'days'); + test.done(); + }, + + 'format number token to number'(test: Test) { + const stack = new Stack(); + const lazyDuration = Duration.minutes(Lazy.number({ produce: () => 10 })); + test.equal(stack.resolve(lazyDuration.formatTokenToNumber()), '10 minutes'); + test.equal(Duration.hours(10).formatTokenToNumber(), '10 hours'); + test.equal(Duration.days(5).formatTokenToNumber(), '5 days'); + test.done(); + }, + + 'duration is unresolved'(test: Test) { + const lazyDuration = Duration.minutes(Lazy.number({ produce: () => 10 })); + test.equal(lazyDuration.isUnresolved(), true); + test.equal(Duration.hours(10).isUnresolved(), false); + test.done(); + }, }); function floatEqual(test: Test, actual: number, expected: number) {