From fdf612a45b3d3c0f8c972bd2736cb9f93e2e7057 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 24 Aug 2021 11:26:24 +0200 Subject: [PATCH 01/91] docs(pipelines): should use 'lookup' tag instead of 'deploy' tag (#15984) The code example was using the wrong condition. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/pipelines/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index f17a35e3a9990..bab41842c2316 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -955,7 +955,7 @@ new CodePipeline(this, 'Pipeline', { resources: ['*'], conditions: { StringEquals: { - 'iam:ResourceTag/aws-cdk:bootstrap-role': 'deploy', + 'iam:ResourceTag/aws-cdk:bootstrap-role': 'lookup', }, }, }), From 0d0db38e3cdb557b4a641c5993068400847cc7df Mon Sep 17 00:00:00 2001 From: Yihui Han <53243835+readybuilderone@users.noreply.github.com> Date: Tue, 24 Aug 2021 18:09:03 +0800 Subject: [PATCH 02/91] fix: (aws-ec2): fix vpc endpoint incorrect issue in China region (#16139) fix:(aws-ec2): fix vpc endpoint incorrect issue in China region This PR fix the issue that can't create interface vpc endpoint for 40+ services like ecr, ec2, athena etc Closes: #9864 ---- # Considerations 1. In cn-north-1 region, there are: - 2 services whose endpoint service begin with "aws.sagemaker" prefix; - 41 services whose endpoint service begin with "cn.com.amazonaws" prefix; - 21 services whose endpoint service begin with "com.amazonaws" prefix; Details: https://gist.github.com/readybuilderone/d355f9f8f0f2b66379a10742b3c67cc7 2. in cn-northwest-1 region, there are: - 2 services whose endpoint service begin with "aws.sagemaker" prefix; - 44 services whose endpoint service begin with "cn.com.amazonaws" prefix; - 21 services whose endpoint service begin with "com.amazonaws" prefix; Details: https://gist.github.com/readybuilderone/a79f2c5e6fa02aae1699bf674b08be7c So, In cn-north-1 and cn-northwest-1, the vpc endpoint prefix is both region and service related. At first, I found the vpc endpoint prefix could be fetched via [AWS API](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeVpcEndpoints.html) , however the javascript sdk supports only asynchronous call, currently I didn't find any pattern in CDK to make live calls in object construction. Since the exception rules won't ever change ( only be added to), and the rate of change should be small in the grand scheme, I just kept the exception prefix lists in a map as fact data. # Known Issues Since the interface vpc endpoints prefixs are not region agnostic, it requires to set the {region} in the environment to create stacks in cn-north-1, and cn-northwest-1. The methods could be found in the [offical guide](https://docs.aws.amazon.com/cdk/latest/guide/environments.html). *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts | 63 +++++++++- .../aws-ec2/test/vpc-endpoint.test.ts | 119 ++++++++++++++++++ 2 files changed, 181 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts index dd28d74e84ea1..69ad4aab88404 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts @@ -326,9 +326,70 @@ export class InterfaceVpcEndpointAwsService implements IInterfaceVpcEndpointServ const region = Lazy.uncachedString({ produce: (context) => Stack.of(context.scope).region, }); - this.name = `${prefix || 'com.amazonaws'}.${region}.${name}`; + const defaultEndpointPrefix = Lazy.uncachedString({ + produce: (context) => { + const regionName = Stack.of(context.scope).region; + return this.getDefaultEndpointPrefix(name, regionName); + }, + }); + const defaultEndpointSuffix = Lazy.uncachedString({ + produce: (context) => { + const regionName = Stack.of(context.scope).region; + return this.getDefaultEndpointSuffix(name, regionName); + }, + }); + + this.name = `${prefix || defaultEndpointPrefix}.${region}.${name}${defaultEndpointSuffix}`; this.port = port || 443; } + + /** + * Get the endpoint prefix for the service in the specified region + * because the prefix for some of the services in cn-north-1 and cn-northwest-1 are different + * + * For future maintenance, the vpc endpoint services could be fetched using AWS CLI Commmand: + * aws ec2 describe-vpc-endpoint-services + */ + private getDefaultEndpointPrefix(name: string, region: string) { + const VPC_ENDPOINT_SERVICE_EXCEPTIONS: { [region: string]: string[] } = { + 'cn-north-1': ['application-autoscaling', 'athena', 'autoscaling', 'awsconnector', 'cassandra', + 'cloudformation', 'codedeploy-commands-secure', 'databrew', 'dms', 'ebs', 'ec2', 'ecr.api', 'ecr.dkr', + 'elasticbeanstalk', 'elasticfilesystem', 'elasticfilesystem-fips', 'execute-api', 'imagebuilder', + 'iotsitewise.api', 'iotsitewise.data', 'kinesis-streams', 'lambda', 'license-manager', 'monitoring', + 'rds', 'redshift', 'redshift-data', 's3', 'sagemaker.api', 'sagemaker.featurestore-runtime', + 'sagemaker.runtime', 'servicecatalog', 'sms', 'sqs', 'states', 'sts', 'synthetics', 'transcribe', + 'transcribestreaming', 'transfer', 'xray'], + 'cn-northwest-1': ['application-autoscaling', 'athena', 'autoscaling', 'awsconnector', 'cassandra', + 'cloudformation', 'codedeploy-commands-secure', 'databrew', 'dms', 'ebs', 'ec2', 'ecr.api', 'ecr.dkr', + 'elasticbeanstalk', 'elasticfilesystem', 'elasticfilesystem-fips', 'execute-api', 'imagebuilder', + 'kinesis-streams', 'lambda', 'license-manager', 'monitoring', 'rds', 'redshift', 'redshift-data', 's3', + 'sagemaker.api', 'sagemaker.featurestore-runtime', 'sagemaker.runtime', 'servicecatalog', 'sms', 'sqs', + 'states', 'sts', 'synthetics', 'transcribe', 'transcribestreaming', 'transfer', 'workspaces', 'xray'], + }; + if (VPC_ENDPOINT_SERVICE_EXCEPTIONS[region]?.includes(name)) { + return 'cn.com.amazonaws'; + } else { + return 'com.amazonaws'; + } + } + + /** + * Get the endpoint suffix for the service in the specified region. + * In cn-north-1 and cn-northwest-1, the vpc endpoint of transcribe is: + * cn.com.amazonaws.cn-north-1.transcribe.cn + * cn.com.amazonaws.cn-northwest-1.transcribe.cn + * so suffix '.cn' should be return in these scenarios. + * + * For future maintenance, the vpc endpoint services could be fetched using AWS CLI Commmand: + * aws ec2 describe-vpc-endpoint-services + */ + private getDefaultEndpointSuffix(name: string, region: string) { + const VPC_ENDPOINT_SERVICE_EXCEPTIONS: { [region: string]: string[] } = { + 'cn-north-1': ['transcribe'], + 'cn-northwest-1': ['transcribe'], + }; + return VPC_ENDPOINT_SERVICE_EXCEPTIONS[region]?.includes(name) ? '.cn' : ''; + } } /** diff --git a/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts b/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts index a59dfbbca07eb..267c48a9e4145 100644 --- a/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts @@ -607,5 +607,124 @@ nodeunitShim({ })); test.done(); }, + 'test vpc interface endpoint with cn.com.amazonaws prefix can be created correctly in cn-north-1'(test: Test) { + //GIVEN + const stack = new Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'cn-north-1' } }); + const vpc = new Vpc(stack, 'VPC'); + + //WHEN + vpc.addInterfaceEndpoint('ECR Endpoint', { + service: InterfaceVpcEndpointAwsService.ECR, + }); + + //THEN + expect(stack).to(haveResource('AWS::EC2::VPCEndpoint', { + ServiceName: 'cn.com.amazonaws.cn-north-1.ecr.api', + })); + + test.done(); + }, + 'test vpc interface endpoint with cn.com.amazonaws prefix can be created correctly in cn-northwest-1'(test: Test) { + //GIVEN + const stack = new Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'cn-northwest-1' } }); + const vpc = new Vpc(stack, 'VPC'); + + //WHEN + vpc.addInterfaceEndpoint('Lambda Endpoint', { + service: InterfaceVpcEndpointAwsService.LAMBDA, + }); + + //THEN + expect(stack).to(haveResource('AWS::EC2::VPCEndpoint', { + ServiceName: 'cn.com.amazonaws.cn-northwest-1.lambda', + })); + + test.done(); + }, + 'test vpc interface endpoint without cn.com.amazonaws prefix can be created correctly in cn-north-1'(test: Test) { + //GIVEN + const stack = new Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'cn-north-1' } }); + const vpc = new Vpc(stack, 'VPC'); + + //WHEN + vpc.addInterfaceEndpoint('ECS Endpoint', { + service: InterfaceVpcEndpointAwsService.ECS, + }); + + //THEN + expect(stack).to(haveResource('AWS::EC2::VPCEndpoint', { + ServiceName: 'com.amazonaws.cn-north-1.ecs', + })); + + test.done(); + }, + 'test vpc interface endpoint without cn.com.amazonaws prefix can be created correctly in cn-northwest-1'(test: Test) { + //GIVEN + const stack = new Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'cn-northwest-1' } }); + const vpc = new Vpc(stack, 'VPC'); + + //WHEN + vpc.addInterfaceEndpoint('Glue Endpoint', { + service: InterfaceVpcEndpointAwsService.GLUE, + }); + + //THEN + expect(stack).to(haveResource('AWS::EC2::VPCEndpoint', { + ServiceName: 'com.amazonaws.cn-northwest-1.glue', + })); + + test.done(); + }, + 'test vpc interface endpoint for transcribe can be created correctly in non-china regions'(test: Test) { + //GIVEN + const stack = new Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' } }); + const vpc = new Vpc(stack, 'VPC'); + + //WHEN + vpc.addInterfaceEndpoint('Transcribe Endpoint', { + service: InterfaceVpcEndpointAwsService.TRANSCRIBE, + }); + + //THEN + expect(stack).to(haveResource('AWS::EC2::VPCEndpoint', { + ServiceName: 'com.amazonaws.us-east-1.transcribe', + })); + + test.done(); + }, + 'test vpc interface endpoint for transcribe can be created correctly in cn-north-1'(test: Test) { + //GIVEN + const stack = new Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'cn-north-1' } }); + const vpc = new Vpc(stack, 'VPC'); + + //WHEN + vpc.addInterfaceEndpoint('Transcribe Endpoint', { + service: InterfaceVpcEndpointAwsService.TRANSCRIBE, + }); + + //THEN + expect(stack).to(haveResource('AWS::EC2::VPCEndpoint', { + ServiceName: 'cn.com.amazonaws.cn-north-1.transcribe.cn', + })); + + test.done(); + }, + 'test vpc interface endpoint for transcribe can be created correctly in cn-northwest-1'(test: Test) { + //GIVEN + const stack = new Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'cn-northwest-1' } }); + const vpc = new Vpc(stack, 'VPC'); + + //WHEN + vpc.addInterfaceEndpoint('Transcribe Endpoint', { + service: InterfaceVpcEndpointAwsService.TRANSCRIBE, + }); + + //THEN + expect(stack).to(haveResource('AWS::EC2::VPCEndpoint', { + ServiceName: 'cn.com.amazonaws.cn-northwest-1.transcribe.cn', + })); + + test.done(); + }, }, }); From 1be373c14350e2fade0874579f6196384dc8d3d2 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 24 Aug 2021 12:54:25 +0200 Subject: [PATCH 03/91] =?UTF-8?q?chore:=20`publishInParallel=3Dfalse`=20wi?= =?UTF-8?q?th=20tokens=20produces=20useless=20error=20m=E2=80=A6=20(#16196?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …essage When a buildspec is written to disk at synth time and the BuildSpec contains CFN references, the error produced is: ``` The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received an instance of Object ``` Because it's trying to write the `{ Fn::Join }` instead of a plain string. This error message is pretty useless. Supporting the feature correctly is a lot more complicated, but at least we can detect this situation and give a more readable error message. Relates to #16164. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/codepipeline/_codebuild-factory.ts | 10 +++++++- .../pipelines/test/docker-credentials.test.ts | 24 +++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts index ac125aee29f0d..59abcbe8e287d 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts @@ -241,7 +241,15 @@ export class CodeBuildFactory implements ICodePipelineActionFactory { // Write to disk and replace with a reference const relativeSpecFile = `buildspec-${Node.of(scope).addr}-${this.constructId}.yaml`; const absSpecFile = path.join(cloudAssemblyBuildSpecDir(scope), relativeSpecFile); - fs.writeFileSync(absSpecFile, Stack.of(scope).resolve(actualBuildSpec.toBuildSpec()), { encoding: 'utf-8' }); + + // This should resolve to a pure JSON string. If it resolves to an object, it's a CFN + // expression, and we can't support that yet. Maybe someday if we think really hard about it. + const fileContents = Stack.of(scope).resolve(actualBuildSpec.toBuildSpec()); + + if (typeof fileContents !== 'string') { + throw new Error(`This BuildSpec contains CloudFormation references and is supported by publishInParallel=false: ${JSON.stringify(fileContents, undefined, 2)}`); + } + fs.writeFileSync(absSpecFile, fileContents, { encoding: 'utf-8' }); projectBuildSpec = codebuild.BuildSpec.fromSourceFilename(relativeSpecFile); } else { projectBuildSpec = actualBuildSpec; diff --git a/packages/@aws-cdk/pipelines/test/docker-credentials.test.ts b/packages/@aws-cdk/pipelines/test/docker-credentials.test.ts index de9d7efa54852..9e3559242e04c 100644 --- a/packages/@aws-cdk/pipelines/test/docker-credentials.test.ts +++ b/packages/@aws-cdk/pipelines/test/docker-credentials.test.ts @@ -6,12 +6,14 @@ import * as iam from '@aws-cdk/aws-iam'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import * as cdk from '@aws-cdk/core'; import * as cdkp from '../lib'; +import { ShellStep } from '../lib'; +import { DockerAssetApp, TestApp } from './testhelpers'; let app: cdk.App; let stack: cdk.Stack; beforeEach(() => { - app = new cdk.App(); + app = new TestApp(); stack = new cdk.Stack(app, 'Stack', { env: { account: '0123456789012', region: 'eu-west-1' }, }); @@ -299,9 +301,27 @@ describe('EcrDockerCredential', () => { expect(stack).not.toHaveResource('AWS::IAM::Policy'); }); - }); + // This test doesn't actually work yet. See https://github.com/aws/aws-cdk/issues/16164 + // eslint-disable-next-line jest/no-disabled-tests + test.skip('with non-parallel publishing', () => { + const pipelines = new cdkp.CodePipeline(stack, 'Pipeline', { + synth: new ShellStep('Build', { + input: cdkp.CodePipelineSource.gitHub('test/test', 'test'), + commands: ['cdk synth'], + }), + + publishAssetsInParallel: false, + dockerCredentials: [ + cdkp.DockerCredential.ecr([repo]), + ], + }); + pipelines.addStage(new DockerAssetApp(stack, 'AssetApp')); + + // Should not throw + app.synth(); + }); }); describe('dockerCredentialsInstallCommands', () => { From 9c39bcb970fc791e94d199b962cc006fca1a3320 Mon Sep 17 00:00:00 2001 From: Sam Wilson Date: Tue, 24 Aug 2021 06:56:44 -0500 Subject: [PATCH 04/91] fix(apigatewayv2): http api - disallow empty string as domain name (#16044) Currently, empty strings are allowed for custom domain name values despite other tools such as the AWS CLI not allowing empty string values. This effort brings the `DomainName` construct up to date with other tooling. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-apigatewayv2/lib/common/domain-name.ts | 4 ++++ .../test/http/domain-name.test.ts | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/domain-name.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/domain-name.ts index 94d284c7f16c6..dca1a60bd4548 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/domain-name.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/domain-name.ts @@ -84,6 +84,10 @@ export class DomainName extends Resource implements IDomainName { constructor(scope: Construct, id: string, props: DomainNameProps) { super(scope, id); + if (props.domainName === '') { + throw new Error('empty string for domainName not allowed'); + } + const domainNameProps: CfnDomainNameProps = { domainName: props.domainName, domainNameConfigurations: [ diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/domain-name.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/domain-name.test.ts index dc64fbf5bf7c9..30c981a1da1d5 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/domain-name.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/domain-name.test.ts @@ -29,6 +29,22 @@ describe('DomainName', () => { }); }); + test('throws when domainName is empty string', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const t = () => { + new DomainName(stack, 'DomainName', { + domainName: '', + certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), + }); + }; + + // THEN + expect(t).toThrow(/empty string for domainName not allowed/); + }); + test('import domain name correctly', () => { // GIVEN const stack = new Stack(); From 305f683e86cca221705c0138572faa38043396eb Mon Sep 17 00:00:00 2001 From: Giedrius Kaskonas <57504623+gkaskonas@users.noreply.github.com> Date: Tue, 24 Aug 2021 15:47:49 +0100 Subject: [PATCH 05/91] feat(lambda): nodejs14.x supports inline code (#16131) Cloudformation now supports Inline code for NodeJS14 runtime. Updating CDK to reflect that ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-lambda/lib/runtime.ts | 2 +- .../integ.runtime.inlinecode.expected.json | 55 +++++++++++++++++++ .../test/integ.runtime.inlinecode.ts | 7 +++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-lambda/lib/runtime.ts b/packages/@aws-cdk/aws-lambda/lib/runtime.ts index e354e5862cc6e..ae26ca066e1c9 100644 --- a/packages/@aws-cdk/aws-lambda/lib/runtime.ts +++ b/packages/@aws-cdk/aws-lambda/lib/runtime.ts @@ -77,7 +77,7 @@ export class Runtime { /** * The NodeJS 14.x runtime (nodejs14.x) */ - public static readonly NODEJS_14_X = new Runtime('nodejs14.x', RuntimeFamily.NODEJS, { supportsInlineCode: false }); + public static readonly NODEJS_14_X = new Runtime('nodejs14.x', RuntimeFamily.NODEJS, { supportsInlineCode: true }); /** * The Python 2.7 runtime (python2.7) diff --git a/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.expected.json index 30d39828cc39d..59961a0755b84 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.expected.json +++ b/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.expected.json @@ -299,6 +299,56 @@ "DependsOn": [ "PYTHON38ServiceRole3EA86BBE" ] + }, + "NODEJS14XServiceRole4523ECDB": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "NODEJS14X930214A3": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event) { return \"success\" }" + }, + "Role": { + "Fn::GetAtt": [ + "NODEJS14XServiceRole4523ECDB", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs14.x" + }, + "DependsOn": [ + "NODEJS14XServiceRole4523ECDB" + ] } }, "Outputs": { @@ -331,6 +381,11 @@ "Value": { "Ref": "PYTHON38A180AE47" } + }, + "NODEJS14XfunctionName": { + "Value": { + "Ref": "NODEJS14X930214A3" + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.ts b/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.ts index 56f5bd27f7746..3bde19e5cd22b 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.ts +++ b/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.ts @@ -57,4 +57,11 @@ const python38 = new Function(stack, 'PYTHON_3_8', { }); new CfnOutput(stack, 'PYTHON_3_8-functionName', { value: python38.functionName }); +const node14xfn = new Function(stack, 'NODEJS_14_X', { + code: new InlineCode('exports.handler = async function(event) { return "success" }'), + handler: 'index.handler', + runtime: Runtime.NODEJS_14_X, +}); +new CfnOutput(stack, 'NODEJS_14_X-functionName', { value: node14xfn.functionName }); + app.synth(); From 64019bbf090e156261feb626a5a4bd7ff4f26545 Mon Sep 17 00:00:00 2001 From: Emil Rowland Date: Tue, 24 Aug 2021 17:28:27 +0200 Subject: [PATCH 06/91] feat(cognito): user pools - device tracking (#16055) fixes #15013 This pull request adds the support of device configuration for Cognito user pools. [DeviceConfiguration](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-userpool-deviceconfiguration.html) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-cognito/README.md | 18 +++++++++++++ .../@aws-cdk/aws-cognito/lib/user-pool.ts | 27 +++++++++++++++++++ .../aws-cognito/test/user-pool.test.ts | 21 +++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index bdb93fc24d566..b015607589652 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -45,6 +45,7 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aw - [Multi-factor Authentication](#multi-factor-authentication-mfa) - [Account Recovery Settings](#account-recovery-settings) - [Emails](#emails) + - [Device Tracking](#device-tracking) - [Lambda Triggers](#lambda-triggers) - [Trigger Permissions](#trigger-permissions) - [Import](#importing-user-pools) @@ -337,6 +338,23 @@ layer](https://docs.aws.amazon.com/cdk/latest/guide/cfn_layer.html) to configure If an email address contains non-ASCII characters, it will be encoded using the [punycode encoding](https://en.wikipedia.org/wiki/Punycode) when generating the template for Cloudformation. +### Device Tracking + +User pools can be configured to track devices that users have logged in to. +Read more at [Device Tracking](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-device-tracking.html) + +```ts +new cognito.UserPool(this, 'myuserpool', { + // ... + deviceTracking: { + challengeRequiredOnNewDevice: true, + deviceOnlyRememberedOnUserPrompt: true, + }, +}); +``` + +The default is to not track devices. + ### Lambda Triggers User pools can be configured such that AWS Lambda functions can be triggered when certain user operations or actions diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index 9f598761ee843..6277ee0f467ee 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -429,6 +429,26 @@ export enum AccountRecovery { NONE, } +/** + * Device tracking settings + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-device-tracking.html + */ +export interface DeviceTracking { + /** + * Indicates whether a challenge is required on a new device. Only applicable to a new device. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-device-tracking.html + * @default false + */ + readonly challengeRequiredOnNewDevice: boolean; + + /** + * If true, a device is only remembered on user prompt. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-device-tracking.html + * @default false + */ + readonly deviceOnlyRememberedOnUserPrompt: boolean; +} + /** * Props for the UserPool construct */ @@ -581,6 +601,12 @@ export interface UserPoolProps { * @default RemovalPolicy.RETAIN */ readonly removalPolicy?: RemovalPolicy; + + /** + * Device tracking settings + * @default - see defaults on each property of DeviceTracking. + */ + readonly deviceTracking?: DeviceTracking; } /** @@ -787,6 +813,7 @@ export class UserPool extends UserPoolBase { caseSensitive: props.signInCaseSensitive, }), accountRecoverySetting: this.accountRecovery(props), + deviceConfiguration: props.deviceTracking, }); userPool.applyRemovalPolicy(props.removalPolicy); diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts index 75ed3dcfa1a2d..0c61312222355 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts @@ -1390,6 +1390,27 @@ describe('User Pool', () => { }); }); +test('device tracking is configured correctly', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', { + deviceTracking: { + challengeRequiredOnNewDevice: true, + deviceOnlyRememberedOnUserPrompt: true, + }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { + DeviceConfiguration: { + ChallengeRequiredOnNewDevice: true, + DeviceOnlyRememberedOnUserPrompt: true, + }, + }); +}); + function fooFunction(scope: Construct, name: string): lambda.IFunction { return new lambda.Function(scope, name, { From 90f95e10f4dd9e4992731d6262dcfc767b65ab3f Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Tue, 24 Aug 2021 17:08:35 +0100 Subject: [PATCH 07/91] feat(assertions): queries and assertions against the Outputs and Mappings sections (#15892) Introduce APIs `hasOutput()`, `findOutputs()`, `hasMapping()` and `findMappings()` to assert the `Mappings` and `Outputs` section of the CloudFormation template. Also, refactored the implementation of `hasResource()` to increase re-usability of its implementation across these new APIs. Migrated the modules `aws-kinesisfirehose` and `aws-neptune` that use these new APIs. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/assertions/README.md | 18 +- packages/@aws-cdk/assertions/lib/match.ts | 12 +- packages/@aws-cdk/assertions/lib/matcher.ts | 8 + .../assertions/lib/private/mappings.ts | 31 ++++ .../assertions/lib/private/outputs.ts | 31 ++++ .../assertions/lib/private/resource.ts | 82 --------- .../assertions/lib/private/resources.ts | 42 +++++ .../assertions/lib/private/section.ts | 58 ++++++ packages/@aws-cdk/assertions/lib/template.ts | 50 +++++- .../assertions/test/private/section.test.ts | 43 +++++ .../@aws-cdk/assertions/test/template.test.ts | 166 +++++++++++++++++- .../@aws-cdk/aws-kinesisfirehose/package.json | 2 +- .../test/delivery-stream.test.ts | 153 +++++++--------- packages/@aws-cdk/aws-neptune/package.json | 4 +- .../@aws-cdk/aws-neptune/test/cluster.test.ts | 49 +++--- .../aws-neptune/test/instance.test.ts | 30 ++-- .../aws-neptune/test/parameter-group.test.ts | 10 +- .../aws-neptune/test/subnet-group.test.ts | 10 +- 18 files changed, 566 insertions(+), 233 deletions(-) create mode 100644 packages/@aws-cdk/assertions/lib/private/mappings.ts create mode 100644 packages/@aws-cdk/assertions/lib/private/outputs.ts delete mode 100644 packages/@aws-cdk/assertions/lib/private/resource.ts create mode 100644 packages/@aws-cdk/assertions/lib/private/resources.ts create mode 100644 packages/@aws-cdk/assertions/lib/private/section.ts create mode 100644 packages/@aws-cdk/assertions/test/private/section.test.ts diff --git a/packages/@aws-cdk/assertions/README.md b/packages/@aws-cdk/assertions/README.md index bfeb893417c31..67109b1156964 100644 --- a/packages/@aws-cdk/assertions/README.md +++ b/packages/@aws-cdk/assertions/README.md @@ -107,11 +107,23 @@ By default, the `hasResource()` and `hasResourceProperties()` APIs perform deep partial object matching. This behavior can be configured using matchers. See subsequent section on [special matchers](#special-matchers). +## Other Sections + +Similar to the `hasResource()` and `findResources()`, we have equivalent methods +to check and find other sections of the CloudFormation resources. + +* Outputs - `hasOutput()` and `findOutputs()` +* Mapping - `hasMapping()` and `findMappings()` + +All of the defaults and behaviour documented for `hasResource()` and +`findResources()` apply to these methods. + ## Special Matchers -The expectation provided to the `hasResourceXXX()` methods, besides carrying -literal values, as seen in the above examples, can also have special matchers -encoded. +The expectation provided to the `hasXXX()` and `findXXX()` methods, besides +carrying literal values, as seen in the above examples, also accept special +matchers. + They are available as part of the `Match` class. ### Object Matchers diff --git a/packages/@aws-cdk/assertions/lib/match.ts b/packages/@aws-cdk/assertions/lib/match.ts index 0272f344efcbc..2a88bada59bac 100644 --- a/packages/@aws-cdk/assertions/lib/match.ts +++ b/packages/@aws-cdk/assertions/lib/match.ts @@ -99,7 +99,7 @@ class LiteralMatch extends Matcher { return new ObjectMatch(this.name, this.pattern, { partial: this.partialObjects }).test(actual); } - const result = new MatchResult(); + const result = new MatchResult(actual); if (typeof this.pattern !== typeof actual) { result.push(this, [], `Expected type ${typeof this.pattern} but received ${getType(actual)}`); return result; @@ -152,16 +152,16 @@ class ArrayMatch extends Matcher { public test(actual: any): MatchResult { if (!Array.isArray(actual)) { - return new MatchResult().push(this, [], `Expected type array but received ${getType(actual)}`); + return new MatchResult(actual).push(this, [], `Expected type array but received ${getType(actual)}`); } if (!this.partial && this.pattern.length !== actual.length) { - return new MatchResult().push(this, [], `Expected array of length ${this.pattern.length} but received ${actual.length}`); + return new MatchResult(actual).push(this, [], `Expected array of length ${this.pattern.length} but received ${actual.length}`); } let patternIdx = 0; let actualIdx = 0; - const result = new MatchResult(); + const result = new MatchResult(actual); while (patternIdx < this.pattern.length && actualIdx < actual.length) { const patternElement = this.pattern[patternIdx]; const matcher = Matcher.isMatcher(patternElement) ? patternElement : new LiteralMatch(this.name, patternElement); @@ -220,10 +220,10 @@ class ObjectMatch extends Matcher { public test(actual: any): MatchResult { if (typeof actual !== 'object' || Array.isArray(actual)) { - return new MatchResult().push(this, [], `Expected type object but received ${getType(actual)}`); + return new MatchResult(actual).push(this, [], `Expected type object but received ${getType(actual)}`); } - const result = new MatchResult(); + const result = new MatchResult(actual); if (!this.partial) { for (const a of Object.keys(actual)) { if (!(a in this.pattern)) { diff --git a/packages/@aws-cdk/assertions/lib/matcher.ts b/packages/@aws-cdk/assertions/lib/matcher.ts index 0495b72d25c5a..a3263d6f00829 100644 --- a/packages/@aws-cdk/assertions/lib/matcher.ts +++ b/packages/@aws-cdk/assertions/lib/matcher.ts @@ -27,8 +27,16 @@ export abstract class Matcher { * The result of `Match.test()`. */ export class MatchResult { + /** + * The target for which this result was generated. + */ + public readonly target: any; private readonly failures: MatchFailure[] = []; + constructor(target: any) { + this.target = target; + } + /** * Push a new failure into this result at a specific path. * If the failure occurred at root of the match tree, set the path to an empty list. diff --git a/packages/@aws-cdk/assertions/lib/private/mappings.ts b/packages/@aws-cdk/assertions/lib/private/mappings.ts new file mode 100644 index 0000000000000..0def435cc0e1d --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/private/mappings.ts @@ -0,0 +1,31 @@ +import { StackInspector } from '../vendored/assert'; +import { formatFailure, matchSection } from './section'; + +export function findMappings(inspector: StackInspector, props: any = {}): { [key: string]: any }[] { + const section: { [key: string] : {} } = inspector.value.Mappings; + const result = matchSection(section, props); + + if (!result.match) { + return []; + } + + return result.matches; +} + +export function hasMapping(inspector: StackInspector, props: any): string | void { + const section: { [key: string]: {} } = inspector.value.Mappings; + const result = matchSection(section, props); + + if (result.match) { + return; + } + + if (result.closestResult === undefined) { + return 'No mappings found in the template'; + } + + return [ + `Template has ${result.analyzedCount} mappings, but none match as expected.`, + formatFailure(result.closestResult), + ].join('\n'); +} \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/private/outputs.ts b/packages/@aws-cdk/assertions/lib/private/outputs.ts new file mode 100644 index 0000000000000..46e5a6cb1d52b --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/private/outputs.ts @@ -0,0 +1,31 @@ +import { StackInspector } from '../vendored/assert'; +import { formatFailure, matchSection } from './section'; + +export function findOutputs(inspector: StackInspector, props: any = {}): { [key: string]: any }[] { + const section: { [key: string] : {} } = inspector.value.Outputs; + const result = matchSection(section, props); + + if (!result.match) { + return []; + } + + return result.matches; +} + +export function hasOutput(inspector: StackInspector, props: any): string | void { + const section: { [key: string]: {} } = inspector.value.Outputs; + const result = matchSection(section, props); + + if (result.match) { + return; + } + + if (result.closestResult === undefined) { + return 'No outputs found in the template'; + } + + return [ + `Template has ${result.analyzedCount} outputs, but none match as expected.`, + formatFailure(result.closestResult), + ].join('\n'); +} \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/private/resource.ts b/packages/@aws-cdk/assertions/lib/private/resource.ts deleted file mode 100644 index 22a2b01734b70..0000000000000 --- a/packages/@aws-cdk/assertions/lib/private/resource.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Match } from '../match'; -import { Matcher, MatchResult } from '../matcher'; -import { StackInspector } from '../vendored/assert'; - -export function findResources(inspector: StackInspector, type: string, props: any = {}): { [key: string]: any }[] { - const matcher = Matcher.isMatcher(props) ? props : Match.objectLike(props); - let results: { [key: string]: any }[] = []; - - eachResourceWithType(inspector, type, (resource) => { - const result = matcher.test(resource); - if (!result.hasFailed()) { - results.push(resource); - } - }); - - return results; -} - -export function hasResource(inspector: StackInspector, type: string, props: any): string | void { - const matcher = Matcher.isMatcher(props) ? props : Match.objectLike(props); - let closestResult: MatchResult | undefined = undefined; - let closestResource: { [key: string]: any } | undefined = undefined; - let count: number = 0; - - let match = false; - eachResourceWithType(inspector, type, (resource) => { - if (match) { return; } - count++; - const result = matcher.test(resource); - if (!result.hasFailed()) { - match = true; - } - if (closestResult === undefined || closestResult.failCount > result.failCount) { - closestResult = result; - closestResource = resource; - } - }); - - if (match) { - return; - } - - if (closestResult === undefined) { - return `No resource with type ${type} found`; - } - - if (!closestResource) { - throw new Error('unexpected: closestResult is null'); - } - - return [ - `${count} resources with type ${type} found, but none match as expected.`, - formatMessage(closestResult, closestResource), - ].join('\n'); -} - -function eachResourceWithType( - inspector: StackInspector, - type: string, - cb: (resource: {[key: string]: any}) => void): void { - - for (const logicalId of Object.keys(inspector.value.Resources ?? {})) { - const resource: { [key: string]: any } = inspector.value.Resources[logicalId]; - if (resource.Type === type) { - cb(resource); - } - } -} - -function formatMessage(closestResult: MatchResult, closestResource: {}): string { - return [ - 'The closest result is:', - reindent(JSON.stringify(closestResource, undefined, 2)), - 'with the following mismatches:', - ...closestResult.toHumanStrings().map(s => `\t${s}`), - ].join('\n'); -} - -function reindent(x: string, indent: number = 2): string { - const pad = ' '.repeat(indent); - return pad + x.split('\n').join(`\n${pad}`); -} \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/private/resources.ts b/packages/@aws-cdk/assertions/lib/private/resources.ts new file mode 100644 index 0000000000000..31286a8a6f1a4 --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/private/resources.ts @@ -0,0 +1,42 @@ +import { StackInspector } from '../vendored/assert'; +import { formatFailure, matchSection } from './section'; + +// Partial type for CloudFormation Resource +type Resource = { + Type: string; +} + +export function findResources(inspector: StackInspector, type: string, props: any = {}): { [key: string]: any }[] { + const section: { [key: string] : Resource } = inspector.value.Resources; + const result = matchSection(filterType(section, type), props); + + if (!result.match) { + return []; + } + + return result.matches; +} + +export function hasResource(inspector: StackInspector, type: string, props: any): string | void { + const section: { [key: string]: Resource } = inspector.value.Resources; + const result = matchSection(filterType(section, type), props); + + if (result.match) { + return; + } + + if (result.closestResult === undefined) { + return `No resource with type ${type} found`; + } + + return [ + `Template has ${result.analyzedCount} resources with type ${type}, but none match as expected.`, + formatFailure(result.closestResult), + ].join('\n'); +} + +function filterType(section: { [key: string]: Resource }, type: string): { [key: string]: Resource } { + return Object.entries(section ?? {}) + .filter(([_, v]) => v.Type === type) + .reduce((agg, [k, v]) => { return { ...agg, [k]: v }; }, {}); +} \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/private/section.ts b/packages/@aws-cdk/assertions/lib/private/section.ts new file mode 100644 index 0000000000000..d8f0123de20d6 --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/private/section.ts @@ -0,0 +1,58 @@ +import { Match } from '../match'; +import { Matcher, MatchResult } from '../matcher'; + +export type MatchSuccess = { match: true, matches: any[] }; +export type MatchFailure = { match: false, closestResult?: MatchResult, analyzedCount: number }; + +export function matchSection(section: any, props: any): MatchSuccess | MatchFailure { + const matcher = Matcher.isMatcher(props) ? props : Match.objectLike(props); + let closestResult: MatchResult | undefined = undefined; + let matching: any[] = []; + let count = 0; + + eachEntryInSection( + section, + + (entry) => { + const result = matcher.test(entry); + if (!result.hasFailed()) { + matching.push(entry); + } else { + count++; + if (closestResult === undefined || closestResult.failCount > result.failCount) { + closestResult = result; + } + } + }, + ); + + if (matching.length > 0) { + return { match: true, matches: matching }; + } else { + return { match: false, closestResult, analyzedCount: count }; + } +} + +function eachEntryInSection( + section: any, + cb: (entry: {[key: string]: any}) => void): void { + + for (const logicalId of Object.keys(section ?? {})) { + const resource: { [key: string]: any } = section[logicalId]; + cb(resource); + } +} + +export function formatFailure(closestResult: MatchResult): string { + return [ + 'The closest result is:', + leftPad(JSON.stringify(closestResult.target, undefined, 2)), + 'with the following mismatches:', + ...closestResult.toHumanStrings().map(s => `\t${s}`), + ].join('\n'); +} + +function leftPad(x: string, indent: number = 2): string { + const pad = ' '.repeat(indent); + return pad + x.split('\n').join(`\n${pad}`); +} \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/template.ts b/packages/@aws-cdk/assertions/lib/template.ts index 480290bc45b04..848c46bcc295a 100644 --- a/packages/@aws-cdk/assertions/lib/template.ts +++ b/packages/@aws-cdk/assertions/lib/template.ts @@ -1,7 +1,9 @@ import { Stack, Stage } from '@aws-cdk/core'; import { Match } from './match'; import { Matcher } from './matcher'; -import { findResources, hasResource } from './private/resource'; +import { findMappings, hasMapping } from './private/mappings'; +import { findOutputs, hasOutput } from './private/outputs'; +import { findResources, hasResource } from './private/resources'; import * as assert from './vendored/assert'; /** @@ -103,6 +105,52 @@ export class Template { return findResources(this.inspector, type, props); } + /** + * Assert that an Output with the given properties exists in the CloudFormation template. + * By default, performs partial matching on the resource, via the `Match.objectLike()`. + * To configure different behavour, use other matchers in the `Match` class. + * @param props the output as should be expected in the template. + */ + public hasOutput(props: any): void { + const matchError = hasOutput(this.inspector, props); + if (matchError) { + throw new Error(matchError); + } + } + + /** + * Get the set of matching Outputs that match the given properties in the CloudFormation template. + * @param props by default, matches all Outputs in the template. + * When a literal object is provided, performs a partial match via `Match.objectLike()`. + * Use the `Match` APIs to configure a different behaviour. + */ + public findOutputs(props: any = {}): { [key: string]: any }[] { + return findOutputs(this.inspector, props); + } + + /** + * Assert that a Mapping with the given properties exists in the CloudFormation template. + * By default, performs partial matching on the resource, via the `Match.objectLike()`. + * To configure different behavour, use other matchers in the `Match` class. + * @param props the output as should be expected in the template. + */ + public hasMapping(props: any): void { + const matchError = hasMapping(this.inspector, props); + if (matchError) { + throw new Error(matchError); + } + } + + /** + * Get the set of matching Mappings that match the given properties in the CloudFormation template. + * @param props by default, matches all Mappings in the template. + * When a literal object is provided, performs a partial match via `Match.objectLike()`. + * Use the `Match` APIs to configure a different behaviour. + */ + public findMappings(props: any = {}): { [key: string]: any }[] { + return findMappings(this.inspector, props); + } + /** * Assert that the CloudFormation template matches the given value * @param expected the expected CloudFormation template as key-value pairs. diff --git a/packages/@aws-cdk/assertions/test/private/section.test.ts b/packages/@aws-cdk/assertions/test/private/section.test.ts new file mode 100644 index 0000000000000..20cc2d5d14e0b --- /dev/null +++ b/packages/@aws-cdk/assertions/test/private/section.test.ts @@ -0,0 +1,43 @@ +import { Match } from '../../lib'; +import { MatchFailure, matchSection, MatchSuccess } from '../../lib/private/section'; + +describe('section', () => { + describe('matchSection', () => { + test('success', () => { + // GIVEN + const matcher = Match.objectLike({ foo: 'bar' }); + const section = { + Entry1: { foo: 'bar' }, + Entry2: { foo: 'bar', baz: 'qux' }, + Entry3: { fred: 'waldo' }, + }; + + // WHEN + const result = matchSection(section, matcher); + + // THEN + expect(result.match).toEqual(true); + const success = result as MatchSuccess; + expect(success.matches.length).toEqual(2); + }); + + test('failure', () => { + // GIVEN + const matcher = Match.objectLike({ foo: 'bar' }); + const section = { + Entry1: { foo: 'qux' }, + Entry3: { fred: 'waldo' }, + }; + + // WHEN + const result = matchSection(section, matcher); + + // THEN + expect(result.match).toEqual(false); + const success = result as MatchFailure; + expect(success.analyzedCount).toEqual(2); + expect(success.closestResult).toBeDefined(); + expect(success.closestResult?.target).toEqual({ foo: 'qux' }); + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/test/template.test.ts b/packages/@aws-cdk/assertions/test/template.test.ts index fcbc199f1f2da..50fb60a1a27f7 100644 --- a/packages/@aws-cdk/assertions/test/template.test.ts +++ b/packages/@aws-cdk/assertions/test/template.test.ts @@ -1,4 +1,4 @@ -import { App, CfnResource, Stack } from '@aws-cdk/core'; +import { App, CfnMapping, CfnOutput, CfnResource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { Match, Template } from '../lib'; @@ -330,6 +330,170 @@ describe('Template', () => { expect(inspect.findResources('Foo::Bar').length).toEqual(2); }); }); + + describe('hasOutput', () => { + test('matching', () => { + const stack = new Stack(); + new CfnOutput(stack, 'Foo', { + value: 'Bar', + }); + new CfnOutput(stack, 'Fred', { + value: 'Waldo', + }); + + const inspect = Template.fromStack(stack); + expect(() => inspect.hasOutput({ Value: 'Bar' })).not.toThrow(); + }); + + test('not matching', (done) => { + const stack = new Stack(); + new CfnOutput(stack, 'Foo', { + value: 'Bar', + exportName: 'ExportBar', + }); + new CfnOutput(stack, 'Fred', { + value: 'Waldo', + }); + + const inspect = Template.fromStack(stack); + expectToThrow( + () => inspect.hasOutput({ + Value: 'Bar', + Export: { Name: 'ExportBaz' }, + }), + [ + /2 outputs/, + /Expected ExportBaz but received ExportBar/, + ], + done, + ); + done(); + }); + }); + + describe('findOutputs', () => { + test('matching', () => { + const stack = new Stack(); + new CfnOutput(stack, 'Foo', { + value: 'Fred', + description: 'FooFred', + }); + new CfnOutput(stack, 'Bar', { + value: 'Fred', + description: 'BarFred', + }); + new CfnOutput(stack, 'Baz', { + value: 'Waldo', + description: 'BazWaldo', + }); + + const inspect = Template.fromStack(stack); + const result = inspect.findOutputs({ Value: 'Fred' }); + expect(result).toEqual([ + { Value: 'Fred', Description: 'FooFred' }, + { Value: 'Fred', Description: 'BarFred' }, + ]); + }); + + test('not matching', () => { + const stack = new Stack(); + new CfnOutput(stack, 'Foo', { + value: 'Fred', + }); + + const inspect = Template.fromStack(stack); + const result = inspect.findOutputs({ Value: 'Waldo' }); + expect(result.length).toEqual(0); + }); + }); + + describe('hasMapping', () => { + test('matching', () => { + const stack = new Stack(); + new CfnMapping(stack, 'Foo', { + mapping: { + Foo: { Bar: 'Lightning', Fred: 'Waldo' }, + Baz: { Bar: 'Qux' }, + }, + }); + new CfnMapping(stack, 'Fred', { + mapping: { + Foo: { Bar: 'Lightning' }, + }, + }); + + const inspect = Template.fromStack(stack); + expect(() => inspect.hasMapping({ Foo: { Bar: 'Lightning' } })).not.toThrow(); + }); + + test('not matching', (done) => { + const stack = new Stack(); + new CfnMapping(stack, 'Foo', { + mapping: { + Foo: { Bar: 'Fred', Baz: 'Waldo' }, + Qux: { Bar: 'Fred' }, + }, + }); + new CfnMapping(stack, 'Fred', { + mapping: { + Foo: { Baz: 'Baz' }, + }, + }); + + const inspect = Template.fromStack(stack); + expectToThrow( + () => inspect.hasMapping({ + Foo: { Bar: 'Qux' }, + }), + [ + /2 mappings/, + /Expected Qux but received Fred/, + ], + done, + ); + done(); + }); + }); + + describe('findMappings', () => { + test('matching', () => { + const stack = new Stack(); + new CfnMapping(stack, 'Foo', { + mapping: { + Foo: { Bar: 'Lightning', Fred: 'Waldo' }, + Baz: { Bar: 'Qux' }, + }, + }); + new CfnMapping(stack, 'Fred', { + mapping: { + Foo: { Bar: 'Lightning' }, + }, + }); + + const inspect = Template.fromStack(stack); + const result = inspect.findMappings({ Foo: { Bar: 'Lightning' } }); + expect(result).toEqual([ + { + Foo: { Bar: 'Lightning', Fred: 'Waldo' }, + Baz: { Bar: 'Qux' }, + }, + { Foo: { Bar: 'Lightning' } }, + ]); + }); + + test('not matching', () => { + const stack = new Stack(); + new CfnMapping(stack, 'Foo', { + mapping: { + Foo: { Bar: 'Fred', Baz: 'Waldo' }, + }, + }); + + const inspect = Template.fromStack(stack); + const result = inspect.findMappings({ Foo: { Bar: 'Waldo' } }); + expect(result.length).toEqual(0); + }); + }); }); function expectToThrow(fn: () => void, msgs: (RegExp | string)[], done: jest.DoneCallback): void { diff --git a/packages/@aws-cdk/aws-kinesisfirehose/package.json b/packages/@aws-cdk/aws-kinesisfirehose/package.json index 58c5568c8c299..fe73affa84c97 100644 --- a/packages/@aws-cdk/aws-kinesisfirehose/package.json +++ b/packages/@aws-cdk/aws-kinesisfirehose/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-kinesisfirehose/test/delivery-stream.test.ts b/packages/@aws-cdk/aws-kinesisfirehose/test/delivery-stream.test.ts index 345e7feb0774a..7c36a29e379b5 100644 --- a/packages/@aws-cdk/aws-kinesisfirehose/test/delivery-stream.test.ts +++ b/packages/@aws-cdk/aws-kinesisfirehose/test/delivery-stream.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT, ResourcePart, SynthUtils, anything, arrayWith } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; @@ -47,11 +46,11 @@ describe('delivery stream', () => { destinations: [mockS3Destination], }); - expect(stack).toHaveResource('AWS::KinesisFirehose::DeliveryStream', { - DeliveryStreamEncryptionConfigurationInput: ABSENT, - DeliveryStreamName: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::KinesisFirehose::DeliveryStream', { + DeliveryStreamEncryptionConfigurationInput: Match.absentProperty(), + DeliveryStreamName: Match.absentProperty(), DeliveryStreamType: 'DirectPut', - KinesisStreamSourceConfiguration: ABSENT, + KinesisStreamSourceConfiguration: Match.absentProperty(), ExtendedS3DestinationConfiguration: { BucketARN: bucketArn, RoleARN: roleArn, @@ -77,14 +76,14 @@ describe('delivery stream', () => { destinations: [mockS3Destination], }); - expect(stack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ - { + Match.objectLike({ Principal: { Service: 'firehose.amazonaws.com', }, - }, + }), ], }, }); @@ -99,32 +98,33 @@ describe('delivery stream', () => { role: deliveryStreamRole, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { - Action: arrayWith( - 'kinesis:DescribeStream', + Effect: 'Allow', + Action: Match.arrayWith([ 'kinesis:GetRecords', 'kinesis:GetShardIterator', 'kinesis:ListShards', - ), + 'kinesis:DescribeStream', + ]), Resource: stack.resolve(sourceStream.streamArn), }, ], }, Roles: [stack.resolve(deliveryStreamRole.roleName)], }); - expect(stack).toHaveResource('AWS::KinesisFirehose::DeliveryStream', { + Template.fromStack(stack).hasResourceProperties('AWS::KinesisFirehose::DeliveryStream', { DeliveryStreamType: 'KinesisStreamAsSource', KinesisStreamSourceConfiguration: { KinesisStreamARN: stack.resolve(sourceStream.streamArn), RoleARN: stack.resolve(deliveryStreamRole.roleArn), }, }); - expect(stack).toHaveResourceLike('AWS::KinesisFirehose::DeliveryStream', { - DependsOn: arrayWith('DeliveryStreamRoleDefaultPolicy2759968B'), - }, ResourcePart.CompleteDefinition); + Template.fromStack(stack).hasResource('AWS::KinesisFirehose::DeliveryStream', { + DependsOn: Match.arrayWith(['DeliveryStreamRoleDefaultPolicy2759968B']), + }); }); test('requesting customer-owned encryption creates key and configuration', () => { @@ -134,21 +134,21 @@ describe('delivery stream', () => { role: deliveryStreamRole, }); - expect(stack).toHaveResource('AWS::KMS::Key'); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).resourceCountIs('AWS::KMS::Key', 1); + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ - { - Action: arrayWith( - 'kms:Encrypt', + Match.objectLike({ + Action: Match.arrayWith([ 'kms:Decrypt', - ), - }, + 'kms:Encrypt', + ]), + }), ], }, Roles: [stack.resolve(deliveryStreamRole.roleName)], }); - expect(stack).toHaveResourceLike('AWS::KinesisFirehose::DeliveryStream', { + Template.fromStack(stack).hasResourceProperties('AWS::KinesisFirehose::DeliveryStream', { DeliveryStreamEncryptionConfigurationInput: { KeyARN: { 'Fn::GetAtt': [ @@ -170,22 +170,22 @@ describe('delivery stream', () => { role: deliveryStreamRole, }); - expect(stack).toHaveResource('AWS::KMS::Key'); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).resourceCountIs('AWS::KMS::Key', 1); + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ - { - Action: arrayWith( - 'kms:Encrypt', + Match.objectLike({ + Action: Match.arrayWith([ 'kms:Decrypt', - ), + 'kms:Encrypt', + ]), Resource: stack.resolve(key.keyArn), - }, + }), ], }, Roles: [stack.resolve(deliveryStreamRole.roleName)], }); - expect(stack).toHaveResourceLike('AWS::KinesisFirehose::DeliveryStream', { + Template.fromStack(stack).hasResourceProperties('AWS::KinesisFirehose::DeliveryStream', { DeliveryStreamEncryptionConfigurationInput: { KeyARN: stack.resolve(key.keyArn), KeyType: 'CUSTOMER_MANAGED_CMK', @@ -200,24 +200,12 @@ describe('delivery stream', () => { role: deliveryStreamRole, }); - expect(stack).not.toHaveResource('AWS::KMS::Key'); - expect(stack).not.toHaveResourceLike('AWS::IAM::Policy', { - PolicyDocument: { - Statement: [ - { - Action: arrayWith( - 'kms:Encrypt', - 'kms:Decrypt', - ), - }, - ], - }, - Roles: [stack.resolve(deliveryStreamRole.roleName)], - }); - expect(stack).toHaveResourceLike('AWS::KinesisFirehose::DeliveryStream', { + Template.fromStack(stack).resourceCountIs('AWS::KMS::Key', 0); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Policy', 0); + Template.fromStack(stack).hasResourceProperties('AWS::KinesisFirehose::DeliveryStream', { DeliveryStreamType: 'DirectPut', DeliveryStreamEncryptionConfigurationInput: { - KeyARN: ABSENT, + KeyARN: Match.absentProperty(), KeyType: 'AWS_OWNED_CMK', }, }); @@ -230,23 +218,11 @@ describe('delivery stream', () => { role: deliveryStreamRole, }); - expect(stack).not.toHaveResource('AWS::KMS::Key'); - expect(stack).not.toHaveResourceLike('AWS::IAM::Policy', { - PolicyDocument: { - Statement: [ - { - Action: arrayWith( - 'kms:Encrypt', - 'kms:Decrypt', - ), - }, - ], - }, - Roles: [stack.resolve(deliveryStreamRole.roleName)], - }); - expect(stack).toHaveResourceLike('AWS::KinesisFirehose::DeliveryStream', { + Template.fromStack(stack).resourceCountIs('AWS::KMS::Key', 0); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Policy', 0); + Template.fromStack(stack).hasResourceProperties('AWS::KinesisFirehose::DeliveryStream', { DeliveryStreamType: 'DirectPut', - DeliveryStreamEncryptionConfigurationInput: ABSENT, + DeliveryStreamEncryptionConfigurationInput: Match.absentProperty(), }); }); @@ -300,13 +276,13 @@ describe('delivery stream', () => { deliveryStream.grant(role, 'firehose:PutRecord'); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ - { + Match.objectLike({ Action: 'firehose:PutRecord', Resource: stack.resolve(deliveryStream.deliveryStreamArn), - }, + }), ], }, Roles: [stack.resolve(role.roleName)], @@ -323,16 +299,16 @@ describe('delivery stream', () => { deliveryStream.grantPutRecords(role); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ - { + Match.objectLike({ Action: [ 'firehose:PutRecord', 'firehose:PutRecordBatch', ], Resource: stack.resolve(deliveryStream.deliveryStreamArn), - }, + }), ], }, Roles: [stack.resolve(role.roleName)], @@ -346,12 +322,12 @@ describe('delivery stream', () => { destinations: [mockS3Destination], }); - expect(stack).toHaveResourceLike('AWS::KinesisFirehose::DeliveryStream', { + Template.fromStack(stack).hasResource('AWS::KinesisFirehose::DeliveryStream', { DependsOn: [dependableId], - }, ResourcePart.CompleteDefinition); - expect(stack).toHaveResourceLike('AWS::IAM::Role', { - DependsOn: ABSENT, - }, ResourcePart.CompleteDefinition); + }); + Template.fromStack(stack).hasResource('AWS::IAM::Role', { + DependsOn: Match.absentProperty(), + }); }); test('supplying 0 or multiple destinations throws', () => { @@ -472,19 +448,18 @@ describe('delivery stream', () => { securityGroup.connections.allowFrom(deliveryStream, ec2.Port.allTcp()); - expect(stack).toHaveResourceLike('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { SecurityGroupIngress: [ - { + Match.objectLike({ CidrIp: { - 'Fn::FindInMap': [ - anything(), + 'Fn::FindInMap': Match.arrayWith([ { Ref: 'AWS::Region', }, 'FirehoseCidrBlock', - ], + ]), }, - }, + }), ], }); }); @@ -499,11 +474,11 @@ describe('delivery stream', () => { securityGroup.connections.allowFrom(deliveryStream, ec2.Port.allTcp()); - expect(stack).toHaveResourceLike('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { SecurityGroupIngress: [ - { + Match.objectLike({ CidrIp: '13.57.135.192/27', - }, + }), ], }); }); @@ -516,7 +491,11 @@ describe('delivery stream', () => { destinations: [mockS3Destination], }); - expect(Object.keys(SynthUtils.toCloudFormation(stack).Mappings).length).toBe(1); + Template.fromStack(stack).hasMapping({ + 'af-south-1': { + FirehoseCidrBlock: '13.244.121.224/27', + }, + }); }); test('can add tags', () => { @@ -526,7 +505,7 @@ describe('delivery stream', () => { cdk.Tags.of(deliveryStream).add('tagKey', 'tagValue'); - expect(stack).toHaveResource('AWS::KinesisFirehose::DeliveryStream', { + Template.fromStack(stack).hasResourceProperties('AWS::KinesisFirehose::DeliveryStream', { Tags: [ { Key: 'tagKey', diff --git a/packages/@aws-cdk/aws-neptune/package.json b/packages/@aws-cdk/aws-neptune/package.json index dfc50d27ac70d..b3a429fb6642a 100644 --- a/packages/@aws-cdk/aws-neptune/package.json +++ b/packages/@aws-cdk/aws-neptune/package.json @@ -74,12 +74,12 @@ }, "license": "Apache-2.0", "devDependencies": { + "@aws-cdk/assertions": "0.0.0", "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "pkglint": "0.0.0", - "@aws-cdk/assert-internal": "0.0.0" + "pkglint": "0.0.0" }, "dependencies": { "@aws-cdk/aws-ec2": "0.0.0", diff --git a/packages/@aws-cdk/aws-neptune/test/cluster.test.ts b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts index 24e61c8be34a1..74f4770a8a74c 100644 --- a/packages/@aws-cdk/aws-neptune/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT, ResourcePart } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; @@ -21,7 +20,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResource('AWS::Neptune::DBCluster', { Properties: { DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId'] }], @@ -29,14 +28,14 @@ describe('DatabaseCluster', () => { }, DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResource('AWS::Neptune::DBInstance', { + Template.fromStack(stack).hasResource('AWS::Neptune::DBInstance', { DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResource('AWS::Neptune::DBSubnetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBSubnetGroup', { SubnetIds: [ { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, @@ -58,7 +57,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId'] }], }); @@ -112,7 +111,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { EngineVersion: '1.0.4.1', DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId'] }], @@ -136,7 +135,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, VpcSecurityGroupIds: ['SecurityGroupId12345'], }); @@ -161,7 +160,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { DBClusterParameterGroupName: { Ref: 'ParamsA8366201' }, }); }); @@ -184,7 +183,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { AssociatedRoles: [ { RoleArn: { @@ -213,7 +212,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { DBClusterParameterGroupName: 'ParamGroupName', }); }); @@ -231,7 +230,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { KmsKeyId: { 'Fn::GetAtt': [ 'Key961B73FD', @@ -254,7 +253,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { StorageEncrypted: true, }); }); @@ -307,7 +306,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBInstance', { DBInstanceIdentifier: `${instanceIdentifierBase}1`, }); }); @@ -326,7 +325,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBInstance', { DBInstanceIdentifier: `${clusterIdentifier}instance1`, }); }); @@ -373,7 +372,7 @@ describe('DatabaseCluster', () => { cluster.connections.allowToAnyIpv4(ec2.Port.tcp(443)); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: 'sg-123456789', }); }); @@ -391,7 +390,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { BackupRetentionPeriod: 20, }); }); @@ -410,7 +409,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { BackupRetentionPeriod: 20, PreferredBackupWindow: '07:34-08:04', }); @@ -429,7 +428,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { PreferredMaintenanceWindow: '07:34-08:04', }); }); @@ -446,8 +445,8 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::Neptune::DBCluster', { - IamAuthEnabled: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { + IamAuthEnabled: Match.absentProperty(), }); }); @@ -467,10 +466,10 @@ describe('DatabaseCluster', () => { cluster.grantConnect(role); // THEN - expect(stack).toHaveResourceLike('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { IamAuthEnabled: true, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [{ Effect: 'Allow', diff --git a/packages/@aws-cdk/aws-neptune/test/instance.test.ts b/packages/@aws-cdk/aws-neptune/test/instance.test.ts index dff005199ffc7..38a6981ab2a78 100644 --- a/packages/@aws-cdk/aws-neptune/test/instance.test.ts +++ b/packages/@aws-cdk/aws-neptune/test/instance.test.ts @@ -1,4 +1,4 @@ -import { expect as expectCDK, haveOutput, haveResource, ResourcePart } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import * as constructs from 'constructs'; @@ -17,14 +17,14 @@ describe('DatabaseInstance', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + Template.fromStack(stack).hasResource('AWS::Neptune::DBInstance', { Properties: { DBClusterIdentifier: { Ref: 'DatabaseB269D8BB' }, DBInstanceClass: 'db.r5.large', }, DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition)); + }); }); test('check that the endpoint works', () => { @@ -43,9 +43,9 @@ describe('DatabaseInstance', () => { }); // THEN - expectCDK(stack).to(haveOutput({ - exportName, - outputValue: { + Template.fromStack(stack).hasOutput({ + Export: { Name: exportName }, + Value: { 'Fn::Join': [ '', [ @@ -55,7 +55,7 @@ describe('DatabaseInstance', () => { ], ], }, - })); + }); }); test('check importing works as expected', () => { @@ -78,10 +78,10 @@ describe('DatabaseInstance', () => { }); // THEN - expectCDK(stack).to(haveOutput({ - exportName: endpointExportName, - outputValue: `${instanceEndpointAddress}:${port}`, - })); + Template.fromStack(stack).hasOutput({ + Export: { Name: endpointExportName }, + Value: `${instanceEndpointAddress}:${port}`, + }); }); test('instance with parameter group', () => { @@ -102,9 +102,9 @@ describe('DatabaseInstance', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBInstance', { DBParameterGroupName: { Ref: 'ParamsA8366201' }, - })); + }); }); test('instance type from CfnParameter', () => { @@ -130,11 +130,11 @@ describe('DatabaseInstance', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBInstance', { DBInstanceClass: { Ref: 'NeptuneInstaneType', }, - })); + }); }); test('instance type from string throws if missing db prefix', () => { diff --git a/packages/@aws-cdk/aws-neptune/test/parameter-group.test.ts b/packages/@aws-cdk/aws-neptune/test/parameter-group.test.ts index 3267f70aed062..57d71b1767734 100644 --- a/packages/@aws-cdk/aws-neptune/test/parameter-group.test.ts +++ b/packages/@aws-cdk/aws-neptune/test/parameter-group.test.ts @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import { Stack } from '@aws-cdk/core'; import { ClusterParameterGroup, ParameterGroup } from '../lib'; @@ -17,12 +17,12 @@ describe('ClusterParameterGroup', () => { }); // THEN - expect(stack).to(haveResource('AWS::Neptune::DBClusterParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBClusterParameterGroup', { Description: 'desc', Parameters: { key: 'value', }, - })); + }); }); @@ -39,12 +39,12 @@ describe('ClusterParameterGroup', () => { }); // THEN - expect(stack).to(haveResource('AWS::Neptune::DBParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBParameterGroup', { Description: 'desc', Parameters: { key: 'value', }, - })); + }); }); }); diff --git a/packages/@aws-cdk/aws-neptune/test/subnet-group.test.ts b/packages/@aws-cdk/aws-neptune/test/subnet-group.test.ts index 675b2cf89a920..5e736d212a12e 100644 --- a/packages/@aws-cdk/aws-neptune/test/subnet-group.test.ts +++ b/packages/@aws-cdk/aws-neptune/test/subnet-group.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import { Stack } from '@aws-cdk/core'; import { SubnetGroup } from '../lib'; @@ -17,7 +17,7 @@ test('creates a subnet group from minimal properties', () => { vpc, }); - expect(stack).toHaveResource('AWS::Neptune::DBSubnetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBSubnetGroup', { DBSubnetGroupDescription: 'MyGroup', SubnetIds: [ { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, @@ -34,7 +34,7 @@ test('creates a subnet group from all properties', () => { vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE }, }); - expect(stack).toHaveResource('AWS::Neptune::DBSubnetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBSubnetGroup', { DBSubnetGroupDescription: 'My Shared Group', DBSubnetGroupName: 'SharedGroup', SubnetIds: [ @@ -51,7 +51,7 @@ describe('subnet selection', () => { vpc, }); - expect(stack).toHaveResource('AWS::Neptune::DBSubnetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBSubnetGroup', { DBSubnetGroupDescription: 'MyGroup', SubnetIds: [ { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, @@ -67,7 +67,7 @@ describe('subnet selection', () => { vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, }); - expect(stack).toHaveResource('AWS::Neptune::DBSubnetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBSubnetGroup', { DBSubnetGroupDescription: 'MyGroup', SubnetIds: [ { Ref: 'VPCPublicSubnet1SubnetB4246D30' }, From c06b161636425801de67f2f95166359ff0dd5dd6 Mon Sep 17 00:00:00 2001 From: Madeline Kusters <80541297+madeline-k@users.noreply.github.com> Date: Tue, 24 Aug 2021 09:48:31 -0700 Subject: [PATCH 08/91] chore(cli): Add node 10 deprecation warning (#16205) Testing: Downgraded to node 10.24.1 on my laptop, and saw the warning banner "Node v10.24.1 has reached end-of-life and will no longer be supported in new releases after 2021-09-01..." when running cli commands on a CDK app linked to this repo. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/aws-cdk/bin/cdk.ts | 1 + packages/aws-cdk/package.json | 1 + yarn.lock | 16 ++++++++++++++++ 3 files changed, 18 insertions(+) diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index 1103b6354bf67..29f217360355f 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -1,6 +1,7 @@ #!/usr/bin/env node import 'source-map-support/register'; import * as cxapi from '@aws-cdk/cx-api'; +import '@jsii/check-node/run'; import * as colors from 'colors/safe'; import * as yargs from 'yargs'; diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 4485c964c105e..497afb2e2ec92 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -73,6 +73,7 @@ "@aws-cdk/cloudformation-diff": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/region-info": "0.0.0", + "@jsii/check-node": "1.33.0", "archiver": "^5.3.0", "aws-sdk": "^2.848.0", "camelcase": "^6.2.0", diff --git a/yarn.lock b/yarn.lock index 431e3492b2be6..dc35a18bf0134 100644 --- a/yarn.lock +++ b/yarn.lock @@ -557,6 +557,14 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@jsii/check-node@1.33.0": + version "1.33.0" + resolved "https://registry.yarnpkg.com/@jsii/check-node/-/check-node-1.33.0.tgz#55d75cbef1c84e2012c67ab8d6de63f773be4a9b" + integrity sha512-Bajxa09dhkuQ8bM1ve6qtm2oFNhW9/+GaKRh4Deewsk/G86ovLXI/rRS6TfCsSw4E0TGPFWzWy0tBeJuEDo7sw== + dependencies: + chalk "^4.1.2" + semver "^7.3.5" + "@jsii/spec@^1.31.0": version "1.31.0" resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.31.0.tgz#9298dc163fdae0bab4006b817592235a29922871" @@ -2686,6 +2694,14 @@ chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + char-regex@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" From a1a65bc9a38b06ec51dff462e52b1beb8d421a56 Mon Sep 17 00:00:00 2001 From: Julian Michel Date: Tue, 24 Aug 2021 19:37:21 +0200 Subject: [PATCH 09/91] fix(sqs): unable to import a FIFO queue when the queue ARN is a token (#15976) The fifo property of a SQS queue is determined based on the queue name. The suffix `.fifo` indicates that it is a fifo queue. When the queue is imported from a token, the name is not known on synthesis time. Therefore, the name of the queue can't be used to initialize the queue as a FIFO queue. In some constructs, CDK checks during synthesis if the given queue is a FIFIO queue. Because of that, an additional option is necessary to specify a FIFO queue. A new boolean property `fifo` is introduced which can be used to specify a FIFO queue that is imported from a token. The property is optional and is only necessary when a FIFO queue should be imported from a token. Closes #12466. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-sqs/lib/queue-base.ts | 9 +++ packages/@aws-cdk/aws-sqs/lib/queue.ts | 25 ++++++- packages/@aws-cdk/aws-sqs/test/test.sqs.ts | 83 ++++++++++++++++++++- 3 files changed, 113 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-sqs/lib/queue-base.ts b/packages/@aws-cdk/aws-sqs/lib/queue-base.ts index 162c149aec2ad..23cf09218cc58 100644 --- a/packages/@aws-cdk/aws-sqs/lib/queue-base.ts +++ b/packages/@aws-cdk/aws-sqs/lib/queue-base.ts @@ -275,4 +275,13 @@ export interface QueueAttributes { * @default - None */ readonly keyArn?: string; + + /** + * Whether this queue is an Amazon SQS FIFO queue. If false, this is a standard queue. + * + * In case of a FIFO queue which is imported from a token, this value has to be explicitly set to true. + * + * @default - if fifo is not specified, the property will be determined based on the queue name (not possible for FIFO queues imported from a token) + */ + readonly fifo?: boolean; } diff --git a/packages/@aws-cdk/aws-sqs/lib/queue.ts b/packages/@aws-cdk/aws-sqs/lib/queue.ts index 414ddaaf8a919..db2992630aa63 100644 --- a/packages/@aws-cdk/aws-sqs/lib/queue.ts +++ b/packages/@aws-cdk/aws-sqs/lib/queue.ts @@ -1,5 +1,5 @@ import * as kms from '@aws-cdk/aws-kms'; -import { Duration, RemovalPolicy, Stack, Token } from '@aws-cdk/core'; +import { Duration, RemovalPolicy, Stack, Token, ArnFormat } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { IQueue, QueueAttributes, QueueBase } from './queue-base'; import { CfnQueue } from './sqs.generated'; @@ -257,7 +257,7 @@ export class Queue extends QueueBase { */ public static fromQueueAttributes(scope: Construct, id: string, attrs: QueueAttributes): IQueue { const stack = Stack.of(scope); - const parsedArn = stack.parseArn(attrs.queueArn); + const parsedArn = stack.splitArn(attrs.queueArn, ArnFormat.NO_RESOURCE_NAME); const queueName = attrs.queueName || parsedArn.resource; const queueUrl = attrs.queueUrl || `https://sqs.${parsedArn.region}.${stack.urlSuffix}/${parsedArn.account}/${queueName}`; @@ -268,9 +268,28 @@ export class Queue extends QueueBase { public readonly encryptionMasterKey = attrs.keyArn ? kms.Key.fromKeyArn(this, 'Key', attrs.keyArn) : undefined; - public readonly fifo = queueName.endsWith('.fifo') ? true : false; + public readonly fifo: boolean = this.determineFifo();; protected readonly autoCreatePolicy = false; + + /** + * Determine fifo flag based on queueName and fifo attribute + */ + private determineFifo(): boolean { + if (Token.isUnresolved(this.queueArn)) { + return attrs.fifo || false; + } else { + if (typeof attrs.fifo !== 'undefined') { + if (attrs.fifo && !queueName.endsWith('.fifo')) { + throw new Error("FIFO queue names must end in '.fifo'"); + } + if (!attrs.fifo && queueName.endsWith('.fifo')) { + throw new Error("Non-FIFO queue name may not end in '.fifo'"); + } + } + return queueName.endsWith('.fifo') ? true : false; + } + } } return new Import(scope, id); diff --git a/packages/@aws-cdk/aws-sqs/test/test.sqs.ts b/packages/@aws-cdk/aws-sqs/test/test.sqs.ts index da8637ba5d4f5..ae334e5fcd4d3 100644 --- a/packages/@aws-cdk/aws-sqs/test/test.sqs.ts +++ b/packages/@aws-cdk/aws-sqs/test/test.sqs.ts @@ -1,7 +1,7 @@ import { expect, haveResource, ResourcePart } from '@aws-cdk/assert-internal'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; -import { CfnParameter, Duration, Stack, App } from '@aws-cdk/core'; +import { CfnParameter, Duration, Stack, App, Token } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import * as sqs from '../lib'; @@ -191,6 +191,87 @@ export = { test.done(); }, + 'import queueArn from token, fifo and standard queues can be defined'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const stdQueue1 = sqs.Queue.fromQueueAttributes(stack, 'StdQueue1', { + queueArn: Token.asString({ Ref: 'ARN' }), + }); + const stdQueue2 = sqs.Queue.fromQueueAttributes(stack, 'StdQueue2', { + queueArn: Token.asString({ Ref: 'ARN' }), + fifo: false, + }); + const fifoQueue = sqs.Queue.fromQueueAttributes(stack, 'FifoQueue', { + queueArn: Token.asString({ Ref: 'ARN' }), + fifo: true, + }); + + // THEN + test.deepEqual(stdQueue1.fifo, false); + test.deepEqual(stdQueue2.fifo, false); + test.deepEqual(fifoQueue.fifo, true); + test.done(); + }, + + 'import queueArn from token, check attributes'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const stdQueue1 = sqs.Queue.fromQueueArn(stack, 'StdQueue', Token.asString({ Ref: 'ARN' })); + + // THEN + test.deepEqual(stack.resolve(stdQueue1.queueArn), { + Ref: 'ARN', + }); + test.deepEqual(stack.resolve(stdQueue1.queueName), { + 'Fn::Select': [5, { 'Fn::Split': [':', { Ref: 'ARN' }] }], + }); + test.deepEqual(stack.resolve(stdQueue1.queueUrl), { + 'Fn::Join': + ['', + ['https://sqs.', + { 'Fn::Select': [3, { 'Fn::Split': [':', { Ref: 'ARN' }] }] }, + '.', + { Ref: 'AWS::URLSuffix' }, + '/', + { 'Fn::Select': [4, { 'Fn::Split': [':', { Ref: 'ARN' }] }] }, + '/', + { 'Fn::Select': [5, { 'Fn::Split': [':', { Ref: 'ARN' }] }] }]], + }); + test.deepEqual(stdQueue1.fifo, false); + test.done(); + }, + + 'fails if fifo flag is set for non fifo queue name'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'my-stack'); + + // THEN + test.throws(() => sqs.Queue.fromQueueAttributes(stack, 'ImportedStdQueue', { + queueArn: 'arn:aws:sqs:us-west-2:123456789012:queue1', + fifo: true, + }), /FIFO queue names must end in '.fifo'/); + test.done(); + }, + + + 'fails if fifo flag is false for fifo queue name'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'my-stack'); + + // THEN + test.throws(() => sqs.Queue.fromQueueAttributes(stack, 'ImportedFifoQueue', { + queueArn: 'arn:aws:sqs:us-west-2:123456789012:queue1.fifo', + fifo: false, + }), /Non-FIFO queue name may not end in '.fifo'/); + test.done(); + }, + 'importing works correctly for cross region queue'(test: Test) { // GIVEN const stack = new Stack(undefined, 'Stack', { env: { region: 'us-east-1' } }); From eb54cd416a48708898e30986058491e21125b2f7 Mon Sep 17 00:00:00 2001 From: Julian Michel Date: Tue, 24 Aug 2021 20:17:15 +0200 Subject: [PATCH 10/91] fix(ssm): StringParameter.fromStringParameterAttributes cannot accept version as a numeric Token (#16048) In `StringParameter.fromStringParameterAttributes()` and `StringParameter.fromSecureStringParameterAttributes()` the version of a parameter can be specified using data type number. If a token value (e.g. CloudFormation Parameter) was used here, the token was not resolved correctly. The conversion of property `version` was corrected by using method `Tokenization.stringifyNumber()`. Fixes #11913. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-ssm/lib/parameter.ts | 5 +- ...g.parameter-store-string.lit.expected.json | 18 +++++ .../test/integ.parameter-store-string.lit.ts | 20 ++++++ .../@aws-cdk/aws-ssm/test/test.parameter.ts | 68 +++++++++++++++++++ 4 files changed, 109 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-ssm/lib/parameter.ts b/packages/@aws-cdk/aws-ssm/lib/parameter.ts index 416454c1ef8cc..df30c8197cf91 100644 --- a/packages/@aws-cdk/aws-ssm/lib/parameter.ts +++ b/packages/@aws-cdk/aws-ssm/lib/parameter.ts @@ -4,6 +4,7 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { CfnDynamicReference, CfnDynamicReferenceService, CfnParameter, Construct as CompatConstruct, ContextProvider, Fn, IResource, Resource, Stack, Token, + Tokenization, } from '@aws-cdk/core'; import { Construct } from 'constructs'; import * as ssm from './ssm.generated'; @@ -324,7 +325,7 @@ export class StringParameter extends ParameterBase implements IStringParameter { const type = attrs.type || ParameterType.STRING; const stringValue = attrs.version - ? new CfnDynamicReference(CfnDynamicReferenceService.SSM, `${attrs.parameterName}:${attrs.version}`).toString() + ? new CfnDynamicReference(CfnDynamicReferenceService.SSM, `${attrs.parameterName}:${Tokenization.stringifyNumber(attrs.version)}`).toString() : new CfnParameter(scope as CompatConstruct, `${id}.Parameter`, { type: `AWS::SSM::Parameter::Value<${type}>`, default: attrs.parameterName }).valueAsString; class Import extends ParameterBase { @@ -341,7 +342,7 @@ export class StringParameter extends ParameterBase implements IStringParameter { * Imports a secure string parameter from the SSM parameter store. */ public static fromSecureStringParameterAttributes(scope: Construct, id: string, attrs: SecureStringParameterAttributes): IStringParameter { - const stringValue = new CfnDynamicReference(CfnDynamicReferenceService.SSM_SECURE, `${attrs.parameterName}:${attrs.version}`).toString(); + const stringValue = new CfnDynamicReference(CfnDynamicReferenceService.SSM_SECURE, `${attrs.parameterName}:${Tokenization.stringifyNumber(attrs.version)}`).toString(); class Import extends ParameterBase { public readonly parameterName = attrs.parameterName; diff --git a/packages/@aws-cdk/aws-ssm/test/integ.parameter-store-string.lit.expected.json b/packages/@aws-cdk/aws-ssm/test/integ.parameter-store-string.lit.expected.json index 1d3d7baba28e0..074f6694ac41b 100644 --- a/packages/@aws-cdk/aws-ssm/test/integ.parameter-store-string.lit.expected.json +++ b/packages/@aws-cdk/aws-ssm/test/integ.parameter-store-string.lit.expected.json @@ -13,6 +13,10 @@ }, { "Parameters": { + "MyParameterVersion": { + "Type": "Number", + "Default": 1 + }, "MyValueParameter": { "Type": "AWS::SSM::Parameter::Value", "Default": "/My/Public/Parameter" @@ -28,6 +32,20 @@ "Value": { "Ref": "MyValueParameter" } + }, + "TheValueVersionFromToken": { + "Value": { + "Fn::Join": [ + "", + [ + "{{resolve:ssm:/My/Public/Parameter:", + { + "Ref": "MyParameterVersion" + }, + "}}" + ] + ] + } } } } diff --git a/packages/@aws-cdk/aws-ssm/test/integ.parameter-store-string.lit.ts b/packages/@aws-cdk/aws-ssm/test/integ.parameter-store-string.lit.ts index 7c4604fa79c9e..09e0f6f17f01f 100644 --- a/packages/@aws-cdk/aws-ssm/test/integ.parameter-store-string.lit.ts +++ b/packages/@aws-cdk/aws-ssm/test/integ.parameter-store-string.lit.ts @@ -17,6 +17,13 @@ class UsingStack extends cdk.Stack { constructor(scope: cdk.App, id: string) { super(scope, id); + // Parameter that contains version number, will be used to pass + // version value from token. + const parameterVersion = new cdk.CfnParameter(this, 'MyParameterVersion', { + type: 'Number', + default: 1, + }).valueAsNumber; + /// !show // Retrieve the latest value of the non-secret parameter // with name "/My/String/Parameter". @@ -24,6 +31,11 @@ class UsingStack extends cdk.Stack { parameterName: '/My/Public/Parameter', // 'version' can be specified but is optional. }).stringValue; + const stringValueVersionFromToken = ssm.StringParameter.fromStringParameterAttributes(this, 'MyValueVersionFromToken', { + parameterName: '/My/Public/Parameter', + // parameter version from token + version: parameterVersion, + }).stringValue; // Retrieve a specific version of the secret (SecureString) parameter. // 'version' is always required. @@ -31,13 +43,21 @@ class UsingStack extends cdk.Stack { parameterName: '/My/Secret/Parameter', version: 5, }); + const secretValueVersionFromToken = ssm.StringParameter.fromSecureStringParameterAttributes(this, 'MySecureValueVersionFromToken', { + parameterName: '/My/Secret/Parameter', + // parameter version from token + version: parameterVersion, + }); + /// !hide new cdk.CfnResource(this, 'Dummy', { type: 'AWS::SNS::Topic' }); new cdk.CfnOutput(this, 'TheValue', { value: stringValue }); + new cdk.CfnOutput(this, 'TheValueVersionFromToken', { value: stringValueVersionFromToken }); // Cannot be provisioned so cannot be actually used Array.isArray(secretValue); + Array.isArray(secretValueVersionFromToken); } } diff --git a/packages/@aws-cdk/aws-ssm/test/test.parameter.ts b/packages/@aws-cdk/aws-ssm/test/test.parameter.ts index 8ce26dd03a0c5..83b68eda4a508 100644 --- a/packages/@aws-cdk/aws-ssm/test/test.parameter.ts +++ b/packages/@aws-cdk/aws-ssm/test/test.parameter.ts @@ -344,6 +344,40 @@ export = { test.done(); }, + 'StringParameter.fromStringParameterAttributes with version from token'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const param = ssm.StringParameter.fromStringParameterAttributes(stack, 'MyParamName', { + parameterName: 'MyParamName', + version: cdk.Token.asNumber({ Ref: 'version' }), + }); + + // THEN + test.deepEqual(stack.resolve(param.parameterArn), { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':ssm:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':parameter/MyParamName', + ]], + }); + test.deepEqual(stack.resolve(param.parameterName), 'MyParamName'); + test.deepEqual(stack.resolve(param.parameterType), 'String'); + test.deepEqual(stack.resolve(param.stringValue), { + 'Fn::Join': ['', [ + '{{resolve:ssm:MyParamName:', + { Ref: 'version' }, + '}}', + ]], + }); + test.done(); + }, + 'StringParameter.fromSecureStringParameterAttributes'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -372,6 +406,40 @@ export = { test.done(); }, + 'StringParameter.fromSecureStringParameterAttributes with version from token'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const param = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'MyParamName', { + parameterName: 'MyParamName', + version: cdk.Token.asNumber({ Ref: 'version' }), + }); + + // THEN + test.deepEqual(stack.resolve(param.parameterArn), { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':ssm:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':parameter/MyParamName', + ]], + }); + test.deepEqual(stack.resolve(param.parameterName), 'MyParamName'); + test.deepEqual(stack.resolve(param.parameterType), 'SecureString'); + test.deepEqual(stack.resolve(param.stringValue), { + 'Fn::Join': ['', [ + '{{resolve:ssm-secure:MyParamName:', + { Ref: 'version' }, + '}}', + ]], + }); + test.done(); + }, + 'StringParameter.fromSecureStringParameterAttributes with encryption key creates the correct policy for grantRead'(test: Test) { // GIVEN const stack = new cdk.Stack(); From 0f7beb29be18d809052f4d46e415a0394c9299ab Mon Sep 17 00:00:00 2001 From: Colin Ihrig Date: Tue, 24 Aug 2021 14:59:05 -0400 Subject: [PATCH 11/91] feat(docdb): cluster - deletion protection (#15216) This commit adds support for `deletionProtection` on docdb `DatabaseCluster`s. Fixes: https://github.com/aws/aws-cdk/issues/15170 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-docdb/README.md | 18 ++++++++++++++++ packages/@aws-cdk/aws-docdb/lib/cluster.ts | 11 ++++++++++ .../@aws-cdk/aws-docdb/test/cluster.test.ts | 21 +++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/packages/@aws-cdk/aws-docdb/README.md b/packages/@aws-cdk/aws-docdb/README.md index fb1c4eeb270ee..6f7ed89e28e11 100644 --- a/packages/@aws-cdk/aws-docdb/README.md +++ b/packages/@aws-cdk/aws-docdb/README.md @@ -60,6 +60,24 @@ const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { cluster.addSecurityGroups(securityGroup); ``` +## Deletion protection + +Deletion protection can be enabled on an Amazon DocumentDB cluster to prevent accidental deletion of the cluster: + +```ts +const cluster = new DatabaseCluster(this, 'Database', { + masterUser: { + username: 'myuser' + }, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.LARGE), + vpcSubnets: { + subnetType: ec2.SubnetType.PUBLIC, + }, + vpc, + deletionProtection: true // Enable deletion protection. +}); +``` + ## Rotating credentials When the master password is generated and stored in AWS Secrets Manager, it can be rotated automatically: diff --git a/packages/@aws-cdk/aws-docdb/lib/cluster.ts b/packages/@aws-cdk/aws-docdb/lib/cluster.ts index f90e976db8798..56b5a724dd439 100644 --- a/packages/@aws-cdk/aws-docdb/lib/cluster.ts +++ b/packages/@aws-cdk/aws-docdb/lib/cluster.ts @@ -136,6 +136,16 @@ export interface DatabaseClusterProps { * @default - Retain cluster. */ readonly removalPolicy?: RemovalPolicy + + /** + * Specifies whether this cluster can be deleted. If deletionProtection is + * enabled, the cluster cannot be deleted unless it is modified and + * deletionProtection is disabled. deletionProtection protects clusters from + * being accidentally deleted. + * + * @default - false + */ + readonly deletionProtection?: boolean; } /** @@ -361,6 +371,7 @@ export class DatabaseCluster extends DatabaseClusterBase { port: props.port, vpcSecurityGroupIds: [this.securityGroupId], dbClusterParameterGroupName: props.parameterGroup?.parameterGroupName, + deletionProtection: props.deletionProtection, // Admin masterUsername: secret ? secret.secretValueFromJson('username').toString() : props.masterUser.username, masterUserPassword: secret diff --git a/packages/@aws-cdk/aws-docdb/test/cluster.test.ts b/packages/@aws-cdk/aws-docdb/test/cluster.test.ts index beb564ed738e5..cb1f8653509df 100644 --- a/packages/@aws-cdk/aws-docdb/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-docdb/test/cluster.test.ts @@ -167,6 +167,27 @@ describe('DatabaseCluster', () => { })); }); + test('can configure cluster deletion protection', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + masterUser: { + username: 'admin', + }, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + vpc, + deletionProtection: true, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { + DeletionProtection: true, + })); + }); + test('cluster with parameter group', () => { // GIVEN const stack = testStack(); From cdee1af03c34a1c08988e672bae6edc2538a8877 Mon Sep 17 00:00:00 2001 From: Madeline Kusters <80541297+madeline-k@users.noreply.github.com> Date: Tue, 24 Aug 2021 13:12:06 -0700 Subject: [PATCH 12/91] fix(resourcegroups): ResourceGroup not using TagType.STANDARD, causes deploy failure (#16211) This PR removes a patch to the cloudformation spec that is no longer necessary. Without the patch, the correct tag type of STANDARD is generated for CfnResourceGroup. PR where the patch was introduced: https://github.com/aws/aws-cdk/pull/6995 Closes #12986 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../530_ResourceGroups_Tags_patch.json | 24 ------------------- 1 file changed, 24 deletions(-) delete mode 100644 packages/@aws-cdk/cfnspec/spec-source/530_ResourceGroups_Tags_patch.json diff --git a/packages/@aws-cdk/cfnspec/spec-source/530_ResourceGroups_Tags_patch.json b/packages/@aws-cdk/cfnspec/spec-source/530_ResourceGroups_Tags_patch.json deleted file mode 100644 index da2d3c603ddbd..0000000000000 --- a/packages/@aws-cdk/cfnspec/spec-source/530_ResourceGroups_Tags_patch.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "ResourceTypes": { - "AWS::ResourceGroups::Group": { - "patch": { - "description": "Sets AWS::ResourceGroups::Group#Tags to Json", - "operations": [ - { - "op": "remove", - "path": "/Properties/Tags/ItemType" - }, - { - "op": "remove", - "path": "/Properties/Tags/Type" - }, - { - "op": "add", - "path": "/Properties/Tags/PrimitiveType", - "value": "Json" - } - ] - } - } - } -} From 27a37ac26172301178a87fda93370b1554f5d513 Mon Sep 17 00:00:00 2001 From: Eli Polonsky Date: Tue, 24 Aug 2021 16:17:26 -0700 Subject: [PATCH 13/91] chore(integ): node check breaks cli integ tests (#16216) The Node runtime check performed by jsii and introduced in this [PR](https://github.com/aws/aws-cdk/pull/16205) breaks the CLI integration tests since it changes the output of the `synth` command. This PR makes it so the test asserts on the content of the template rather than the output of the CLI. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-cdk/test/integ/cli/cli.integtest.ts | 47 ++++++++++++------- packages/aws-cdk/test/integ/helpers/cdk.ts | 6 +++ 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/packages/aws-cdk/test/integ/cli/cli.integtest.ts b/packages/aws-cdk/test/integ/cli/cli.integtest.ts index 957d04bf09fd9..873e3a1787ee5 100644 --- a/packages/aws-cdk/test/integ/cli/cli.integtest.ts +++ b/packages/aws-cdk/test/integ/cli/cli.integtest.ts @@ -39,23 +39,36 @@ integTest('Termination protection', withDefaultFixture(async (fixture) => { })); integTest('cdk synth', withDefaultFixture(async (fixture) => { - await expect(fixture.cdk(['synth', fixture.fullStackName('test-1')], { verbose: false })).resolves.toEqual( - `Resources: - topic69831491: - Type: AWS::SNS::Topic - Metadata: - aws:cdk:path: ${fixture.stackNamePrefix}-test-1/topic/Resource`); - - await expect(fixture.cdk(['synth', fixture.fullStackName('test-2')], { verbose: false })).resolves.toEqual( - `Resources: - topic152D84A37: - Type: AWS::SNS::Topic - Metadata: - aws:cdk:path: ${fixture.stackNamePrefix}-test-2/topic1/Resource - topic2A4FB547F: - Type: AWS::SNS::Topic - Metadata: - aws:cdk:path: ${fixture.stackNamePrefix}-test-2/topic2/Resource`); + await fixture.cdk(['synth', fixture.fullStackName('test-1')]); + expect(fixture.template('test-1')).toEqual({ + Resources: { + topic69831491: { + Type: 'AWS::SNS::Topic', + Metadata: { + 'aws:cdk:path': `${fixture.stackNamePrefix}-test-1/topic/Resource`, + }, + }, + }, + }); + + await fixture.cdk(['synth', fixture.fullStackName('test-2')], { verbose: false }); + expect(fixture.template('test-2')).toEqual({ + Resources: { + topic152D84A37: { + Type: 'AWS::SNS::Topic', + Metadata: { + 'aws:cdk:path': `${fixture.stackNamePrefix}-test-2/topic1/Resource`, + }, + }, + topic2A4FB547F: { + Type: 'AWS::SNS::Topic', + Metadata: { + 'aws:cdk:path': `${fixture.stackNamePrefix}-test-2/topic2/Resource`, + }, + }, + }, + }); + })); integTest('ssm parameter provider error', withDefaultFixture(async (fixture) => { diff --git a/packages/aws-cdk/test/integ/helpers/cdk.ts b/packages/aws-cdk/test/integ/helpers/cdk.ts index 8bf256140d13b..4a09f43063600 100644 --- a/packages/aws-cdk/test/integ/helpers/cdk.ts +++ b/packages/aws-cdk/test/integ/helpers/cdk.ts @@ -382,6 +382,12 @@ export class TestFixture { }); } + public template(stackName: string): any { + const fullStackName = this.fullStackName(stackName); + const templatePath = path.join(this.integTestDir, 'cdk.out', `${fullStackName}.template.json`); + return JSON.parse(fs.readFileSync(templatePath, { encoding: 'utf-8' }).toString()); + } + public get bootstrapStackName() { return this.fullStackName('bootstrap-stack'); } From e547ba0d1491af0abe703132fa06fe786ffd7070 Mon Sep 17 00:00:00 2001 From: kaizen3031593 <36202692+kaizen3031593@users.noreply.github.com> Date: Tue, 24 Aug 2021 20:16:53 -0400 Subject: [PATCH 14/91] feat(cloudwatch): add support for cross-account alarms (#16007) Allows `accountId` to be specified in a CloudWatch Alarm. This opens up the ability to create cross-account alarms, which was just [announced](https://aws.amazon.com/about-aws/whats-new/2021/08/announcing-amazon-cloudwatch-cross-account-alarms/). closes #15959. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-cloudwatch/README.md | 4 ++ packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts | 11 +-- .../test/cross-environment.test.ts | 67 ++++++++++++++++++- 3 files changed, 76 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudwatch/README.md b/packages/@aws-cdk/aws-cloudwatch/README.md index ff1f1997489f4..bdcf58e61024f 100644 --- a/packages/@aws-cdk/aws-cloudwatch/README.md +++ b/packages/@aws-cdk/aws-cloudwatch/README.md @@ -28,6 +28,8 @@ represents the amount of errors reported by that Lambda function: const errors = fn.metricErrors(); ``` +`Metric` objects can be account and region aware. You can specify `account` and `region` as properties of the metric, or use the `metric.attachTo(Construct)` method. `metric.attachTo()` will automatically copy the `region` and `account` fields of the `Construct`, which can come from anywhere in the Construct tree. + You can also instantiate `Metric` objects to reference any [published metric](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/aws-services-cloudwatch-metrics.html) that's not exposed using a convenience method on the CDK construct. @@ -160,6 +162,8 @@ The most important properties to set while creating an Alarms are: - `evaluationPeriods`: how many consecutive periods the metric has to be breaching the the threshold for the alarm to trigger. +To create a cross-account alarm, make sure you have enabled [cross-account functionality](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Cross-Account-Cross-Region.html) in CloudWatch. Then, set the `account` property in the `Metric` object either manually or via the `metric.attachTo()` method. + ### Alarm Actions To add actions to an alarm, use the integration classes from the diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts index 6b102d567125f..41928d0af7193 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts @@ -257,7 +257,10 @@ export class Alarm extends AlarmBase { return dispatchMetric(metric, { withStat(stat, conf) { self.validateMetricStat(stat, metric); - if (conf.renderingProperties?.label == undefined) { + const canRenderAsLegacyMetric = conf.renderingProperties?.label == undefined && + (stat.account == undefined || Stack.of(self).account == stat.account); + // Do this to disturb existing templates as little as possible + if (canRenderAsLegacyMetric) { return dropUndefined({ dimensions: stat.dimensions, namespace: stat.namespace, @@ -283,6 +286,7 @@ export class Alarm extends AlarmBase { unit: stat.unitFilter, }, id: 'm1', + accountId: stat.account, label: conf.renderingProperties?.label, returnData: true, } as CfnAlarm.MetricDataQueryProperty, @@ -344,7 +348,7 @@ export class Alarm extends AlarmBase { } /** - * Validate that if a region and account are in the given stat config, they match the Alarm + * Validate that if a region is in the given stat config, they match the Alarm */ private validateMetricStat(stat: MetricStatConfig, metric: IMetric) { const stack = Stack.of(this); @@ -352,9 +356,6 @@ export class Alarm extends AlarmBase { if (definitelyDifferent(stat.region, stack.region)) { throw new Error(`Cannot create an Alarm in region '${stack.region}' based on metric '${metric}' in '${stat.region}'`); } - if (definitelyDifferent(stat.account, stack.account)) { - throw new Error(`Cannot create an Alarm in account '${stack.account}' based on metric '${metric}' in '${stat.account}'`); - } } } diff --git a/packages/@aws-cdk/aws-cloudwatch/test/cross-environment.test.ts b/packages/@aws-cdk/aws-cloudwatch/test/cross-environment.test.ts index 7c7ed7d04bf6f..f17391591a8c5 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/cross-environment.test.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/cross-environment.test.ts @@ -6,11 +6,12 @@ const a = new Metric({ namespace: 'Test', metricName: 'ACount' }); let stack1: Stack; let stack2: Stack; +let stack3: Stack; describe('cross environment', () => { beforeEach(() => { stack1 = new Stack(undefined, undefined, { env: { region: 'pluto', account: '1234' } }); stack2 = new Stack(undefined, undefined, { env: { region: 'mars', account: '5678' } }); - + stack3 = new Stack(undefined, undefined, { env: { region: 'pluto', account: '0000' } }); }); describe('in graphs', () => { @@ -112,6 +113,70 @@ describe('cross environment', () => { }); + + test('metric attached to stack3 will render in stack1', () => { + //Cross-account metrics are supported in Alarms + + // GIVEN + new Alarm(stack1, 'Alarm', { + threshold: 1, + evaluationPeriods: 1, + metric: a.attachTo(stack3), + }); + + // THEN + Template.fromStack(stack1).hasResourceProperties('AWS::CloudWatch::Alarm', { + Metrics: [ + { + AccountId: '0000', + Id: 'm1', + MetricStat: { + Metric: { + MetricName: 'ACount', + Namespace: 'Test', + }, + Period: 300, + Stat: 'Average', + }, + ReturnData: true, + }, + ], + }); + }); + + test('metric can render in a different account', () => { + // GIVEN + const b = new Metric({ + namespace: 'Test', + metricName: 'ACount', + account: '0000', + }); + + new Alarm(stack1, 'Alarm', { + threshold: 1, + evaluationPeriods: 1, + metric: b, + }); + + // THEN + Template.fromStack(stack1).hasResourceProperties('AWS::CloudWatch::Alarm', { + Metrics: [ + { + AccountId: '0000', + Id: 'm1', + MetricStat: { + Metric: { + MetricName: 'ACount', + Namespace: 'Test', + }, + Period: 300, + Stat: 'Average', + }, + ReturnData: true, + }, + ], + }); + }); }); }); From e678f104df4fb0377c6ad5c8abc4132433363871 Mon Sep 17 00:00:00 2001 From: Ben Chaimberg Date: Wed, 25 Aug 2021 05:51:34 -0700 Subject: [PATCH 15/91] revert: temporarily transfer @skinny85 module ownership (#16206) This reverts commit 77dfa2f8546d03671b3304d2910a9c7e387ff6a8. --- .github/workflows/issue-label-assign.yml | 116 +++++++++++------------ 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/.github/workflows/issue-label-assign.yml b/.github/workflows/issue-label-assign.yml index 618de4693571f..64e3daf97887b 100644 --- a/.github/workflows/issue-label-assign.yml +++ b/.github/workflows/issue-label-assign.yml @@ -21,23 +21,23 @@ jobs: [ {"keywords":["(cli)","(command line)"],"labels":["package/tools"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/alexa-ask)","(alexa-ask)","(alexa ask)"],"labels":["@aws-cdk/alexa-ask"],"assignees":["madeline-k"]}, - {"keywords":["(@aws-cdk/app-delivery)","(app-delivery)","(app delivery)"],"labels":["@aws-cdk/app-delivery"],"assignees":["madeline-k"]}, + {"keywords":["(@aws-cdk/app-delivery)","(app-delivery)","(app delivery)"],"labels":["@aws-cdk/app-delivery"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/assert)","(assert)"],"labels":["@aws-cdk/assert"],"assignees":["nija-at"]}, {"keywords":["(@aws-cdk/assets)","(assets)"],"labels":["@aws-cdk/assets"],"assignees":["eladb"]}, - {"keywords":["(@aws-cdk/aws-accessanalyzer)","(aws-accessanalyzer)","(accessanalyzer)","(access analyzer)"],"labels":["@aws-cdk/aws-accessanalyzer"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-acmpca)","(aws-acmpca)","(acmpca)"],"labels":["@aws-cdk/aws-acmpca"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-accessanalyzer)","(aws-accessanalyzer)","(accessanalyzer)","(access analyzer)"],"labels":["@aws-cdk/aws-accessanalyzer"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-acmpca)","(aws-acmpca)","(acmpca)"],"labels":["@aws-cdk/aws-acmpca"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-amazonmq)","(aws-amazonmq)","(amazonmq)","(amazon mq)","(amazon-mq)"],"labels":["@aws-cdk/aws-amazonmq"],"assignees":["otaviomacedo"]}, - {"keywords":["(@aws-cdk/aws-amplify)","(aws-amplify)","(amplify)"],"labels":["@aws-cdk/aws-amplify"],"assignees":["madeline-k"]}, + {"keywords":["(@aws-cdk/aws-amplify)","(aws-amplify)","(amplify)"],"labels":["@aws-cdk/aws-amplify"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-apigateway)","(aws-apigateway)","(apigateway)","(api gateway)","(api-gateway)"],"labels":["@aws-cdk/aws-apigateway"],"assignees":["nija-at"]}, {"keywords":["(@aws-cdk/aws-apigatewayv2)","(aws-apigatewayv2)","(apigatewayv2)","(apigateway v2)","(api-gateway-v2)"],"labels":["@aws-cdk/aws-apigatewayv2"],"assignees":["nija-at"]}, {"keywords":["(@aws-cdk/aws-apigatewayv2-authorizers)","(aws-apigatewayv2-authorizers)","(apigatewayv2-authorizers)"],"labels":["@aws-cdk/aws-apigatewayv2-authorizers"],"assignees":["nija-at"]}, {"keywords":["(@aws-cdk/aws-apigatewayv2-integrations)","(aws-apigatewayv2-integrations)","(apigatewayv2-integrations)","(apigateway v2 integrations)","(api-gateway-v2-integrations)"],"labels":["@aws-cdk/aws-apigatewayv2-integrations"],"assignees":["nija-at"]}, {"keywords":["(@aws-cdk/aws-appconfig)","(aws-appconfig)","(appconfig)","(app config)","(app-config)"],"labels":["@aws-cdk/aws-appconfig"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-appflow)","(aws-appflow)","(appflow)","(app flow)","(app-flow)"],"labels":["@aws-cdk/aws-appflow"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-appintegrations)","(aws-appintegrations)","(appintegrations)"],"labels":["@aws-cdk/aws-appintegrations"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-appflow)","(aws-appflow)","(appflow)","(app flow)","(app-flow)"],"labels":["@aws-cdk/aws-appflow"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-appintegrations)","(aws-appintegrations)","(appintegrations)"],"labels":["@aws-cdk/aws-appintegrations"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-applicationautoscaling)","(aws-applicationautoscaling)","(applicationautoscaling)","(application autoscaling)","(application-autoscaling)"],"labels":["@aws-cdk/aws-applicationautoscaling"],"assignees":["comcalvi"]}, {"keywords":["(@aws-cdk/aws-applicationinsights)","(aws-applicationinsights)","(applicationinsights)","(application insights)","(application-insights)"],"labels":["@aws-cdk/aws-applicationinsights"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-appmesh)","(aws-appmesh)","(appmesh)","(app mesh)","(app-mesh)"],"labels":["@aws-cdk/aws-appmesh"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-appmesh)","(aws-appmesh)","(appmesh)","(app mesh)","(app-mesh)"],"labels":["@aws-cdk/aws-appmesh"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-appstream)","(aws-appstream)","(appstream)","(app stream)","(app-stream)"],"labels":["@aws-cdk/aws-appstream"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-appsync)","(aws-appsync)","(appsync)","(app sync)","(app-sync)"],"labels":["@aws-cdk/aws-appsync"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-athena)","(aws-athena)","(athena)"],"labels":["@aws-cdk/aws-athena"],"assignees":["BenChaimberg"]}, @@ -54,7 +54,7 @@ jobs: {"keywords":["(@aws-cdk/aws-ce)","(aws-ce)","(ce)"],"labels":["@aws-cdk/aws-ce"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-certificatemanager)","(aws-certificatemanager)","(certificatemanager)","(certificate manager)","(certificate-manager)","(acm)"],"labels":["@aws-cdk/aws-certificatemanager"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-chatbot)","(aws-chatbot)","(chatbot)"],"labels":["@aws-cdk/aws-chatbot"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-cloud9)","(aws-cloud9)","(cloud9)","(cloud 9)"],"labels":["@aws-cdk/aws-cloud9"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-cloud9)","(aws-cloud9)","(cloud9)","(cloud 9)"],"labels":["@aws-cdk/aws-cloud9"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-cloudformation)","(aws-cloudformation)","(cloudformation)","(cloud formation)"],"labels":["@aws-cdk/aws-cloudformation"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-cloudfront)","(aws-cloudfront)","(cloudfront)","(cloud front)"],"labels":["@aws-cdk/aws-cloudfront"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-cloudfront-origins)","(aws-cloudfront-origins)","(cloudfront-origins)","(cloudfront origins)"],"labels":["@aws-cdk/aws-cloudfront-origins"],"assignees":["njlynch"]}, @@ -62,31 +62,31 @@ jobs: {"keywords":["(@aws-cdk/aws-cloudwatch)","(aws-cloudwatch)","(cloudwatch)","(cloud watch)"],"labels":["@aws-cdk/aws-cloudwatch"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-cloudwatch-actions)","(aws-cloudwatch-actions)","(cloudwatch-actions)","(cloudwatch actions)"],"labels":["@aws-cdk/aws-cloudwatch-actions"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-codeartifact)","(aws-codeartifact)","(codeartifact)","(code artifact)","(code-artifact)"],"labels":["@aws-cdk/aws-codeartifact"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-codebuild)","(aws-codebuild)","(codebuild)","(code build)","(code-build)"],"labels":["@aws-cdk/aws-codebuild"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codecommit)","(aws-codecommit)","(codecommit)","(code commit)", "(code-commit)"],"labels":["@aws-cdk/aws-codecommit"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codedeploy)","(aws-codedeploy)","(codedeploy)","(code deploy)","(code-deploy)"],"labels":["@aws-cdk/aws-codedeploy"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codeguruprofiler)","(aws-codeguruprofiler)","(codeguruprofiler)","(codeguru profiler)","(codeguru-profiler)"],"labels":["@aws-cdk/aws-codeguruprofiler"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codegurureviewer)","(aws-codegurureviewer)","(codegurureviewer)","(codeguru reviewer)","(codeguru-reviewer)"],"labels":["@aws-cdk/aws-codegurureviewer"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codepipeline)","(aws-codepipeline)","(codepipeline)","(code pipeline)","(code-pipeline)"],"labels":["@aws-cdk/aws-codepipeline"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codepipeline-actions)","(aws-codepipeline-actions)","(codepipeline-actions)","(codepipeline actions)"],"labels":["@aws-cdk/aws-codepipeline-actions"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codestar)","(aws-codestar)","(codestar)"],"labels":["@aws-cdk/aws-codestar"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codestarconnections)","(aws-codestarconnections)","(codestarconnections)","(codestar connections)","(codestar-connections)"],"labels":["@aws-cdk/aws-codestarconnections"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codestarnotifications)","(aws-codestarnotifications)","(codestarnotifications)","(codestar notifications)","(codestar-notifications)"],"labels":["@aws-cdk/aws-codestarnotifications"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-codebuild)","(aws-codebuild)","(codebuild)","(code build)","(code-build)"],"labels":["@aws-cdk/aws-codebuild"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-codecommit)","(aws-codecommit)","(codecommit)","(code commit)", "(code-commit)"],"labels":["@aws-cdk/aws-codecommit"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-codedeploy)","(aws-codedeploy)","(codedeploy)","(code deploy)","(code-deploy)"],"labels":["@aws-cdk/aws-codedeploy"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-codeguruprofiler)","(aws-codeguruprofiler)","(codeguruprofiler)","(codeguru profiler)","(codeguru-profiler)"],"labels":["@aws-cdk/aws-codeguruprofiler"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-codegurureviewer)","(aws-codegurureviewer)","(codegurureviewer)","(codeguru reviewer)","(codeguru-reviewer)"],"labels":["@aws-cdk/aws-codegurureviewer"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-codepipeline)","(aws-codepipeline)","(codepipeline)","(code pipeline)","(code-pipeline)"],"labels":["@aws-cdk/aws-codepipeline"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-codepipeline-actions)","(aws-codepipeline-actions)","(codepipeline-actions)","(codepipeline actions)"],"labels":["@aws-cdk/aws-codepipeline-actions"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-codestar)","(aws-codestar)","(codestar)"],"labels":["@aws-cdk/aws-codestar"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-codestarconnections)","(aws-codestarconnections)","(codestarconnections)","(codestar connections)","(codestar-connections)"],"labels":["@aws-cdk/aws-codestarconnections"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-codestarnotifications)","(aws-codestarnotifications)","(codestarnotifications)","(codestar notifications)","(codestar-notifications)"],"labels":["@aws-cdk/aws-codestarnotifications"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-cognito)","(aws-cognito)","(cognito)"],"labels":["@aws-cdk/aws-cognito"],"assignees":["nija-at"]}, {"keywords":["(@aws-cdk/aws-config)","(aws-config)","(config)"],"labels":["@aws-cdk/aws-config"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-customerprofiles)","(aws-customerprofiles)","(customerprofiles)"],"labels":["@aws-cdk/aws-customerprofiles"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-databrew)","(aws-databrew)","(databrew)"],"labels":["@aws-cdk/aws-databrew"],"assignees":["BenChaimberg"]}, {"keywords":["(@aws-cdk/aws-datapipeline)","(aws-datapipeline)","(datapipeline)","(data pipeline)","(data-pipeline)"],"labels":["@aws-cdk/aws-datapipeline"],"assignees":["BenChaimberg"]}, {"keywords":["(@aws-cdk/aws-datasync)","(aws-datasync)","(datasync)"],"labels":["@aws-cdk/aws-datasync"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-dax)","(aws-dax)","(dax)"],"labels":["@aws-cdk/aws-dax"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-detective)","(aws-detective)","(detective)"],"labels":["@aws-cdk/aws-detective"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-dax)","(aws-dax)","(dax)"],"labels":["@aws-cdk/aws-dax"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-detective)","(aws-detective)","(detective)"],"labels":["@aws-cdk/aws-detective"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-devopsguru)","(aws-devopsguru)","(devopsguru)"],"labels":["@aws-cdk/aws-devopsguru"],"assignees":["nija-at"]}, {"keywords":["(@aws-cdk/aws-directoryservice)","(aws-directoryservice)","(directoryservice)","(directory service)","(directory-service)"],"labels":["@aws-cdk/aws-directoryservice"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-dlm)","(aws-dlm)","(dlm)"],"labels":["@aws-cdk/aws-dlm"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-dms)","(aws-dms)","(dms)"],"labels":["@aws-cdk/aws-dms"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-docdb)","(aws-docdb)","(docdb)","(doc db)","(doc-db)"],"labels":["@aws-cdk/aws-docdb"],"assignees":["madeline-k"]}, - {"keywords":["(@aws-cdk/aws-dynamodb)","(aws-dynamodb)","(dynamodb)","(dynamo db)","(dynamo-db)"],"labels":["@aws-cdk/aws-dynamodb"],"assignees":["madeline-k"]}, - {"keywords":["(@aws-cdk/aws-dynamodb-global)","(aws-dynamodb-global)","(dynamodb-global)","(dynamodb global)"],"labels":["@aws-cdk/aws-dynamodb-global"],"assignees":["madeline-k"]}, + {"keywords":["(@aws-cdk/aws-docdb)","(aws-docdb)","(docdb)","(doc db)","(doc-db)"],"labels":["@aws-cdk/aws-docdb"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-dynamodb)","(aws-dynamodb)","(dynamodb)","(dynamo db)","(dynamo-db)"],"labels":["@aws-cdk/aws-dynamodb"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-dynamodb-global)","(aws-dynamodb-global)","(dynamodb-global)","(dynamodb global)"],"labels":["@aws-cdk/aws-dynamodb-global"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-ec2)","(aws-ec2)","(ec2)","(vpc)"],"labels":["@aws-cdk/aws-ec2"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-ecr)","(aws-ecr)","(ecr)"],"labels":["@aws-cdk/aws-ecr"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-ecr-assets)","(aws-ecr-assets)","(ecr-assets)","(ecr assets)","(ecrassets)"],"labels":["@aws-cdk/aws-ecr-assets"],"assignees":["eladb"]}, @@ -96,7 +96,7 @@ jobs: {"keywords":["(@aws-cdk/aws-eks)","(aws-eks)","(eks)"],"labels":["@aws-cdk/aws-eks"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-eks-legacy)","(aws-eks-legacy)","(eks-legacy)"],"labels":["@aws-cdk/aws-eks-legacy"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-elasticache)","(aws-elasticache)","(elasticache)","(elastic cache)","(elastic-cache)"],"labels":["@aws-cdk/aws-elasticache"],"assignees":["otaviomacedo"]}, - {"keywords":["(@aws-cdk/aws-elasticbeanstalk)","(aws-elasticbeanstalk)","(elasticbeanstalk)","(elastic beanstalk)","(elastic-beanstalk)"],"labels":["@aws-cdk/aws-elasticbeanstalk"],"assignees":["madeline-k"]}, + {"keywords":["(@aws-cdk/aws-elasticbeanstalk)","(aws-elasticbeanstalk)","(elasticbeanstalk)","(elastic beanstalk)","(elastic-beanstalk)"],"labels":["@aws-cdk/aws-elasticbeanstalk"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-elasticloadbalancing)","(aws-elasticloadbalancing)","(elasticloadbalancing)","(elastic loadbalancing)","(elastic-loadbalancing)","(elb)"],"labels":["@aws-cdk/aws-elasticloadbalancing"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-elasticloadbalancingv2)","(aws-elasticloadbalancingv2)","(elasticloadbalancingv2)","(elastic-loadbalancing-v2)","(elbv2)","(elb v2)"],"labels":["@aws-cdk/aws-elasticloadbalancingv2"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-elasticloadbalancingv2-actions)","(aws-elasticloadbalancingv2-actions)","(elasticloadbalancingv2-actions)"],"labels":["@aws-cdk/aws-elasticloadbalancingv2-actions"],"assignees":["njlynch"]}, @@ -106,7 +106,7 @@ jobs: {"keywords":["(@aws-cdk/aws-emrcontainers)","(aws-emrcontainers)","(emrcontainers)"],"labels":["@aws-cdk/aws-emrcontainers"],"assignees":["BenChaimberg"]}, {"keywords":["(@aws-cdk/aws-events)","(aws-events)","(events)","(eventbridge)","(event-bridge)"],"labels":["@aws-cdk/aws-events"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-events-targets)","(aws-events-targets)","(events-targets)","(events targets)"],"labels":["@aws-cdk/aws-events-targets"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-eventschemas)","(aws-eventschemas)","(eventschemas)","(event schemas)"],"labels":["@aws-cdk/aws-eventschemas"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-eventschemas)","(aws-eventschemas)","(eventschemas)","(event schemas)"],"labels":["@aws-cdk/aws-eventschemas"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-finspace)","(aws-finspace)","(finspace)"],"labels":["@aws-cdk/aws-finspace"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-fis)","(aws-fis)","(fis)"],"labels":["@aws-cdk/aws-fis"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-fms)","(aws-fms)","(fms)"],"labels":["@aws-cdk/aws-fms"],"assignees":["rix0rrr"]}, @@ -116,23 +116,23 @@ jobs: {"keywords":["(@aws-cdk/aws-globalaccelerator)","(aws-globalaccelerator)","(globalaccelerator)","(global accelerator)","(global-accelerator)"],"labels":["@aws-cdk/aws-globalaccelerator"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-globalaccelerator-endpoints)","(aws-globalaccelerator-endpoints)","(globalaccelerator-endpoints)"],"labels":["@aws-cdk/aws-globalaccelerator-endpoints"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-glue)","(aws-glue)","(glue)"],"labels":["@aws-cdk/aws-glue"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-greengrass)","(aws-greengrass)","(greengrass)","(green grass)","(green-grass)"],"labels":["@aws-cdk/aws-greengrass"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-greengrassv2)","(aws-greengrassv2)","(greengrassv2)"],"labels":["@aws-cdk/aws-greengrassv2"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-greengrass)","(aws-greengrass)","(greengrass)","(green grass)","(green-grass)"],"labels":["@aws-cdk/aws-greengrass"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-greengrassv2)","(aws-greengrassv2)","(greengrassv2)"],"labels":["@aws-cdk/aws-greengrassv2"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-groundstation)","(aws-groundstation)","(groundstation)"],"labels":["@aws-cdk/aws-groundstation"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-guardduty)","(aws-guardduty)","(guardduty)","(guard duty)","(guard-duty)"],"labels":["@aws-cdk/aws-guardduty"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-iam)","(aws-iam)","(iam)"],"labels":["@aws-cdk/aws-iam"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-imagebuilder)","(aws-imagebuilder)","(imagebuilder)","(image builder)","(image-builder)"],"labels":["@aws-cdk/aws-imagebuilder"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-inspector)","(aws-inspector)","(inspector)"],"labels":["@aws-cdk/aws-inspector"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-iot)","(aws-iot)","(iot)"],"labels":["@aws-cdk/aws-iot"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-iot1click)","(aws-iot1click)","(iot1click)","(iot 1click)"],"labels":["@aws-cdk/aws-iot1click"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-iotanalytics)","(aws-iotanalytics)","(iotanalytics)","(iot analytics)","(iot-analytics)"],"labels":["@aws-cdk/aws-iotanalytics"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-iotevents)","(aws-iotevents)","(iotevents)","(iot events)","(iot-events)"],"labels":["@aws-cdk/aws-iotevents"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-iotfleethub)","(aws-iotfleethub)","(iotfleethub)"],"labels":["@aws-cdk/aws-iotfleethub"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-iotsitewise)","(aws-iotsitewise)","(iotsitewise)","(iot sitewise)","(iot-sitewise)","(iot-site-wise)","(iot site wise)"],"labels":["@aws-cdk/aws-iotsitewise"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-iotthingsgraph)","(aws-iotthingsgraph)","(iotthingsgraph)","(iot things graph)","(iot-things-graph)"],"labels":["@aws-cdk/aws-iotthingsgraph"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-iotwireless)","(aws-iotwireless)","(iotwireless)"],"labels":["@aws-cdk/aws-iotwireless"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-ivs)","(aws-ivs)","(Interactive Video Service)","(ivs)"],"labels":["@aws-cdk/aws-ivs"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-kendra)","(aws-kendra)","(kendra)"],"labels":["@aws-cdk/aws-kendra"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-imagebuilder)","(aws-imagebuilder)","(imagebuilder)","(image builder)","(image-builder)"],"labels":["@aws-cdk/aws-imagebuilder"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-inspector)","(aws-inspector)","(inspector)"],"labels":["@aws-cdk/aws-inspector"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iot)","(aws-iot)","(iot)"],"labels":["@aws-cdk/aws-iot"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iot1click)","(aws-iot1click)","(iot1click)","(iot 1click)"],"labels":["@aws-cdk/aws-iot1click"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iotanalytics)","(aws-iotanalytics)","(iotanalytics)","(iot analytics)","(iot-analytics)"],"labels":["@aws-cdk/aws-iotanalytics"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iotevents)","(aws-iotevents)","(iotevents)","(iot events)","(iot-events)"],"labels":["@aws-cdk/aws-iotevents"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iotfleethub)","(aws-iotfleethub)","(iotfleethub)"],"labels":["@aws-cdk/aws-iotfleethub"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iotsitewise)","(aws-iotsitewise)","(iotsitewise)","(iot sitewise)","(iot-sitewise)","(iot-site-wise)","(iot site wise)"],"labels":["@aws-cdk/aws-iotsitewise"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iotthingsgraph)","(aws-iotthingsgraph)","(iotthingsgraph)","(iot things graph)","(iot-things-graph)"],"labels":["@aws-cdk/aws-iotthingsgraph"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iotwireless)","(aws-iotwireless)","(iotwireless)"],"labels":["@aws-cdk/aws-iotwireless"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-ivs)","(aws-ivs)","(Interactive Video Service)","(ivs)"],"labels":["@aws-cdk/aws-ivs"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-kendra)","(aws-kendra)","(kendra)"],"labels":["@aws-cdk/aws-kendra"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-kinesis)","(aws-kinesis)","(kinesis)"],"labels":["@aws-cdk/aws-kinesis"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-kinesisanalytics)","(aws-kinesisanalytics)","(kinesisanalytics)","(kinesis analytics)","(kinesis-analytics)"],"labels":["@aws-cdk/aws-kinesisanalytics"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-kinesisanalytics-flink)","(aws-kinesisanalytics-flink)","(kinesisanalytics-flink)"],"labels":["@aws-cdk/aws-kinesisanalytics-flink"],"assignees":["otaviomacedo"]}, @@ -151,27 +151,27 @@ jobs: {"keywords":["(@aws-cdk/aws-lookoutmetrics)","(aws-lookoutmetrics)","(lookoutmetrics)"],"labels":["@aws-cdk/aws-lookoutmetrics"],"assignees":["BenChaimberg"]}, {"keywords":["(@aws-cdk/aws-lookoutvision)","(aws-lookoutvision)","(lookoutvision)"],"labels":["@aws-cdk/aws-lookoutvision"],"assignees":["BenChaimberg"]}, {"keywords":["(@aws-cdk/aws-macie)","(aws-macie)","(macie)"],"labels":["@aws-cdk/aws-macie"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-managedblockchain)","(aws-managedblockchain)","(managedblockchain)","(managed blockchain)","(managed-blockchain)"],"labels":["@aws-cdk/aws-managedblockchain"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-mediaconnect)","(aws-mediaconnect)","(mediaconnect)"],"labels":["@aws-cdk/aws-mediaconnect"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-mediaconvert)","(aws-mediaconvert)","(mediaconvert)","(media convert)","(media-convert)"],"labels":["@aws-cdk/aws-mediaconvert"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-medialive)","(aws-medialive)","(medialive)","(media live)","(media-live)"],"labels":["@aws-cdk/aws-medialive"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-mediastore)","(aws-mediastore)","(mediastore)","(media store)","(media-store)"],"labels":["@aws-cdk/aws-mediastore"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-mediapackage)","(aws-mediapackage)","(mediapackage)","(media package)","(media-package)"],"labels":["@aws-cdk/aws-mediapackage"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-managedblockchain)","(aws-managedblockchain)","(managedblockchain)","(managed blockchain)","(managed-blockchain)"],"labels":["@aws-cdk/aws-managedblockchain"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-mediaconnect)","(aws-mediaconnect)","(mediaconnect)"],"labels":["@aws-cdk/aws-mediaconnect"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-mediaconvert)","(aws-mediaconvert)","(mediaconvert)","(media convert)","(media-convert)"],"labels":["@aws-cdk/aws-mediaconvert"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-medialive)","(aws-medialive)","(medialive)","(media live)","(media-live)"],"labels":["@aws-cdk/aws-medialive"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-mediastore)","(aws-mediastore)","(mediastore)","(media store)","(media-store)"],"labels":["@aws-cdk/aws-mediastore"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-mediapackage)","(aws-mediapackage)","(mediapackage)","(media package)","(media-package)"],"labels":["@aws-cdk/aws-mediapackage"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-msk)","(aws-msk)","(msk)"],"labels":["@aws-cdk/aws-msk"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-mwaa)","(aws-mwaa)","(mwaa)"],"labels":["@aws-cdk/aws-mwaa"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-neptune)","(aws-neptune)","(neptune)"],"labels":["@aws-cdk/aws-neptune"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-networkfirewall)","(aws-networkfirewall)","(networkfirewall)"],"labels":["@aws-cdk/aws-networkfirewall"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-networkmanager)","(aws-networkmanager)","(networkmanager)","(network manager)","(network-manager)"],"labels":["@aws-cdk/aws-networkmanager"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-networkfirewall)","(aws-networkfirewall)","(networkfirewall)"],"labels":["@aws-cdk/aws-networkfirewall"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-networkmanager)","(aws-networkmanager)","(networkmanager)","(network manager)","(network-manager)"],"labels":["@aws-cdk/aws-networkmanager"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-nimblestudio)","(aws-nimblestudio)","(nimblestudio)"],"labels":["@aws-cdk/aws-nimblestudio"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-opsworks)","(aws-opsworks)","(opsworks)","(ops works)","(ops-works)"],"labels":["@aws-cdk/aws-opsworks"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-opsworkscm)","(aws-opsworkscm)","(opsworkscm)","(opsworks cm)","(opsworks-cm)"],"labels":["@aws-cdk/aws-opsworkscm"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-personalize)","(aws-personalize)","(personalize)"],"labels":["@aws-cdk/aws-personalize"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-pinpoint)","(aws-pinpoint)","(pinpoint)"],"labels":["@aws-cdk/aws-pinpoint"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-pinpointemail)","(aws-pinpointemail)","(pinpointemail)","(pinpoint email)","(pinpoint-email)"],"labels":["@aws-cdk/aws-pinpointemail"],"assignees":["otaviomacedo"]}, - {"keywords":["(@aws-cdk/aws-qldb)","(aws-qldb)","(qldb)"],"labels":["@aws-cdk/aws-qldb"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-qldb)","(aws-qldb)","(qldb)"],"labels":["@aws-cdk/aws-qldb"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-quicksight)","(aws-quicksight)","(quicksight)"],"labels":["@aws-cdk/aws-quicksight"],"assignees":["BenChaimberg"]}, {"keywords":["(@aws-cdk/aws-ram)","(aws-ram)","(ram)"],"labels":["@aws-cdk/aws-ram"],"assignees":["madeline-k"]}, - {"keywords":["(@aws-cdk/aws-rds)","(aws-rds)","(rds)"],"labels":["@aws-cdk/aws-rds"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-rds)","(aws-rds)","(rds)"],"labels":["@aws-cdk/aws-rds"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-redshift)","(aws-redshift)","(redshift)","(red shift)","(red-shift)"],"labels":["@aws-cdk/aws-redshift"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-resourcegroups)","(aws-resourcegroups)","(resourcegroups)","(resource groups)","(resource-groups)"],"labels":["@aws-cdk/aws-resourcegroups"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-robomaker)","(aws-robomaker)","(robomaker)","(robo maker)","(robo-maker)"],"labels":["@aws-cdk/aws-robomaker"],"assignees":["njlynch"]}, @@ -189,9 +189,9 @@ jobs: {"keywords":["(@aws-cdk/aws-sam)","(aws-sam)","(sam)"],"labels":["@aws-cdk/aws-sam"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-sdb)","(aws-sdb)","(sdb)"],"labels":["@aws-cdk/aws-sdb"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-secretsmanager)","(aws-secretsmanager)","(secretsmanager)","(secrets manager)","(secrets-manager)"],"labels":["@aws-cdk/aws-secretsmanager"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-securityhub)","(aws-securityhub)","(securityhub)","(security hub)","(security-hub)"],"labels":["@aws-cdk/aws-securityhub"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-servicecatalog)","(aws-servicecatalog)","(servicecatalog)","(service catalog)","(service-catalog)"],"labels":["@aws-cdk/aws-servicecatalog"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-servicecatalogappregistry)","(aws-servicecatalogappregistry)","(servicecatalogappregistry)"],"labels":["@aws-cdk/aws-servicecatalogappregistry"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-securityhub)","(aws-securityhub)","(securityhub)","(security hub)","(security-hub)"],"labels":["@aws-cdk/aws-securityhub"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-servicecatalog)","(aws-servicecatalog)","(servicecatalog)","(service catalog)","(service-catalog)"],"labels":["@aws-cdk/aws-servicecatalog"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-servicecatalogappregistry)","(aws-servicecatalogappregistry)","(servicecatalogappregistry)"],"labels":["@aws-cdk/aws-servicecatalogappregistry"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-servicediscovery)","(aws-servicediscovery)","(servicediscovery)","(service discovery)","(service-discovery)"],"labels":["@aws-cdk/aws-servicediscovery"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-ses)","(aws-ses)","(ses)"],"labels":["@aws-cdk/aws-ses"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-ses-actions)","(aws-ses-actions)","(ses-actions)","(ses actions)"],"labels":["@aws-cdk/aws-ses-actions"],"assignees":["otaviomacedo"]}, @@ -200,11 +200,11 @@ jobs: {"keywords":["(@aws-cdk/aws-sns-subscriptions)","(aws-sns-subscriptions)","(sns-subscriptions)","(sns subscriptions)"],"labels":["@aws-cdk/aws-sns-subscriptions"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-sqs)","(aws-sqs)","(sqs)"],"labels":["@aws-cdk/aws-sqs"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-ssm)","(aws-ssm)","(ssm)"],"labels":["@aws-cdk/aws-ssm"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-sso)","(aws-sso)","(sso)"],"labels":["@aws-cdk/aws-sso"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-sso)","(aws-sso)","(sso)"],"labels":["@aws-cdk/aws-sso"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-stepfunctions)","(aws-stepfunctions)","(stepfunctions)","(step functions)","(step-functions)"],"labels":["@aws-cdk/aws-stepfunctions"],"assignees":["BenChaimberg"]}, {"keywords":["(@aws-cdk/aws-stepfunctions-tasks)","(aws-stepfunctions-tasks)","(stepfunctions-tasks)","(stepfunctions tasks)"],"labels":["@aws-cdk/aws-stepfunctions-tasks"],"assignees":["BenChaimberg"]}, {"keywords":["(@aws-cdk/aws-synthetics)","(aws-synthetics)","(synthetics)"],"labels":["@aws-cdk/aws-synthetics"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-timestream)","(aws-timestream)","(timestream)"],"labels":["@aws-cdk/aws-timestream"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-timestream)","(aws-timestream)","(timestream)"],"labels":["@aws-cdk/aws-timestream"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-transfer)","(aws-transfer)","(transfer)"],"labels":["@aws-cdk/aws-transfer"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-waf)","(aws-waf)","(waf)"],"labels":["@aws-cdk/aws-waf"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-wafregional)","(aws-wafregional)","(wafregional)","(waf regional)","(waf-regional)"],"labels":["@aws-cdk/aws-wafregional"],"assignees":["njlynch"]}, @@ -213,16 +213,16 @@ jobs: {"keywords":["(@aws-cdk/aws-xray)","(aws-xray)","(xray)"],"labels":["@aws-cdk/aws-xray"],"assignees":["nija-at"]}, {"keywords":["(@aws-cdk/cfnspec)","(cfnspec)","(cfn spec)","(cfn-spec)"],"labels":["@aws-cdk/cfnspec"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/cloud-assembly-schema)","(cloud-assembly-schema)","(cloud assembly schema)"],"labels":["@aws-cdk/cloud-assembly-schema"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/cloudformation-diff)","(cloudformation-diff)","(cloudformation diff)","(cfn diff)"],"labels":["@aws-cdk/cloudformation-diff"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/cloudformation-include)","(cloudformation-include)","(cloudformation include)","(cfn include)","(cfn-include)"],"labels":["@aws-cdk/cloudformation-include"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/cloudformation-diff)","(cloudformation-diff)","(cloudformation diff)","(cfn diff)"],"labels":["@aws-cdk/cloudformation-diff"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/cloudformation-include)","(cloudformation-include)","(cloudformation include)","(cfn include)","(cfn-include)"],"labels":["@aws-cdk/cloudformation-include"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/core)","(core)"],"labels":["@aws-cdk/core"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/custom-resources)","(custom-resources)","(custom resources)"],"labels":["@aws-cdk/custom-resources"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/cx-api)","(cx-api)","(cx api)"],"labels":["@aws-cdk/cx-api"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-lambda-layer-awscli)","(aws-lambda-layer-awscli)","(lambda-layer-awscli)"],"labels":["@aws-cdk/aws-lambda-layer-awscli"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-lambda-layer-kubectl)","(aws-lambda-layer-kubectl)","(lambda-layer-kubectl)"],"labels":["@aws-cdk/aws-lambda-layer-kubectl"],"assignees":["eladb"]}, {"keywords":["(@aws-cdk/pipelines)","(pipelines)","(cdk pipelines)","(cdk-pipelines)"],"labels":["@aws-cdk/pipelines"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/region-info)","(region-info)","(region info)"],"labels":["@aws-cdk/region-info"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/region-info)","(region-info)","(region info)"],"labels":["@aws-cdk/region-info"],"assignees":["skinny85"]}, {"keywords":["(aws-cdk-lib)","(cdk-v2)", "(v2)", "(ubergen)"],"labels":["aws-cdk-lib"],"assignees":["nija-at"]}, {"keywords":["(monocdk)","(monocdk-experiment)"],"labels":["monocdk"],"assignees":["nija-at"]}, - {"keywords":["(@aws-cdk/yaml-cfn)","(aws-yaml-cfn)","(yaml-cfn)"],"labels":["@aws-cdk/aws-yaml-cfn"],"assignees":["BenChaimberg"]} + {"keywords":["(@aws-cdk/yaml-cfn)","(aws-yaml-cfn)","(yaml-cfn)"],"labels":["@aws-cdk/aws-yaml-cfn"],"assignees":["skinny85"]} ] From b1d69d7b06cd2a2ae8f578e217bdf7fef50a0163 Mon Sep 17 00:00:00 2001 From: Otavio Macedo Date: Wed, 25 Aug 2021 17:53:28 +0100 Subject: [PATCH 16/91] fix(s3): bucket is not emptied before update when the name changes (#16203) Changing the bucket name leads CloudFormation to try to delete the bucket and create a new one with the new name. If the bucket is not empty, the deployment will fail. With this change, the custom resource will clear the old bucket when it detects that there has been a name change. NB: this custom resource is created only when `autoDeleteObjects: true` is passed to the bucket. Fixes #14011. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../test/integ.s3-bucket.lit.expected.json | 18 +-- .../lib/auto-delete-objects-handler/index.ts | 24 +++- .../test/auto-delete-objects-handler.test.ts | 103 +++++++++++++++++- ...g.bucket-auto-delete-objects.expected.json | 18 +-- 4 files changed, 139 insertions(+), 24 deletions(-) diff --git a/packages/@aws-cdk/aws-kinesisfirehose-destinations/test/integ.s3-bucket.lit.expected.json b/packages/@aws-cdk/aws-kinesisfirehose-destinations/test/integ.s3-bucket.lit.expected.json index 2a85e28a6984d..ba6e2aecacda8 100644 --- a/packages/@aws-cdk/aws-kinesisfirehose-destinations/test/integ.s3-bucket.lit.expected.json +++ b/packages/@aws-cdk/aws-kinesisfirehose-destinations/test/integ.s3-bucket.lit.expected.json @@ -102,7 +102,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3BucketF01ADF6B" + "Ref": "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3BucketD715D8B0" }, "S3Key": { "Fn::Join": [ @@ -115,7 +115,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3VersionKey6FC34F51" + "Ref": "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3VersionKey6E76822C" } ] } @@ -128,7 +128,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3VersionKey6FC34F51" + "Ref": "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3VersionKey6E76822C" } ] } @@ -751,17 +751,17 @@ } }, "Parameters": { - "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3BucketF01ADF6B": { + "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3BucketD715D8B0": { "Type": "String", - "Description": "S3 bucket for asset \"1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1\"" + "Description": "S3 bucket for asset \"fe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9\"" }, - "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3VersionKey6FC34F51": { + "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3VersionKey6E76822C": { "Type": "String", - "Description": "S3 key for asset version \"1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1\"" + "Description": "S3 key for asset version \"fe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9\"" }, - "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1ArtifactHash9ECACDFD": { + "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9ArtifactHash9AE3702B": { "Type": "String", - "Description": "Artifact hash for asset \"1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1\"" + "Description": "Artifact hash for asset \"fe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9\"" }, "AssetParameters5ee078f2a1957fe672d6cfd84faf49e07b8460758b5cd2669b3df1212a14cd19S3BucketFEDDFB43": { "Type": "String", diff --git a/packages/@aws-cdk/aws-s3/lib/auto-delete-objects-handler/index.ts b/packages/@aws-cdk/aws-s3/lib/auto-delete-objects-handler/index.ts index 5dd144b446e8e..f431aacf3fca2 100644 --- a/packages/@aws-cdk/aws-s3/lib/auto-delete-objects-handler/index.ts +++ b/packages/@aws-cdk/aws-s3/lib/auto-delete-objects-handler/index.ts @@ -6,10 +6,25 @@ const s3 = new S3(); export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent) { switch (event.RequestType) { case 'Create': - case 'Update': return; + case 'Update': + return onUpdate(event); case 'Delete': - return onDelete(event); + return onDelete(event.ResourceProperties?.BucketName); + } +} + +async function onUpdate(event: AWSLambda.CloudFormationCustomResourceEvent) { + const updateEvent = event as AWSLambda.CloudFormationCustomResourceUpdateEvent; + const oldBucketName = updateEvent.OldResourceProperties?.BucketName; + const newBucketName = updateEvent.ResourceProperties?.BucketName; + const bucketNameHasChanged = newBucketName != null && oldBucketName != null && newBucketName !== oldBucketName; + + /* If the name of the bucket has changed, CloudFormation will try to delete the bucket + and create a new one with the new name. So we have to delete the contents of the + bucket so that this operation does not fail. */ + if (bucketNameHasChanged) { + return onDelete(oldBucketName); } } @@ -23,7 +38,7 @@ async function emptyBucket(bucketName: string) { const contents = [...listedObjects.Versions ?? [], ...listedObjects.DeleteMarkers ?? []]; if (contents.length === 0) { return; - }; + } const records = contents.map((record: any) => ({ Key: record.Key, VersionId: record.VersionId })); await s3.deleteObjects({ Bucket: bucketName, Delete: { Objects: records } }).promise(); @@ -33,8 +48,7 @@ async function emptyBucket(bucketName: string) { } } -async function onDelete(deleteEvent: AWSLambda.CloudFormationCustomResourceDeleteEvent) { - const bucketName = deleteEvent.ResourceProperties?.BucketName; +async function onDelete(bucketName?: string) { if (!bucketName) { throw new Error('No BucketName was provided.'); } diff --git a/packages/@aws-cdk/aws-s3/test/auto-delete-objects-handler.test.ts b/packages/@aws-cdk/aws-s3/test/auto-delete-objects-handler.test.ts index e5a7072441974..b09b9e5e4f5b3 100644 --- a/packages/@aws-cdk/aws-s3/test/auto-delete-objects-handler.test.ts +++ b/packages/@aws-cdk/aws-s3/test/auto-delete-objects-handler.test.ts @@ -37,7 +37,51 @@ test('does nothing on create event', async () => { expect(mockS3Client.deleteObjects).toHaveBeenCalledTimes(0); }); -test('does nothing on update event', async () => { +test('does nothing on update event when everything remains the same', async () => { + // GIVEN + const event: Partial = { + RequestType: 'Update', + ResourceProperties: { + ServiceToken: 'Foo', + BucketName: 'MyBucket', + }, + OldResourceProperties: { + ServiceToken: 'Foo', + BucketName: 'MyBucket', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockS3Client.listObjectVersions).toHaveBeenCalledTimes(0); + expect(mockS3Client.deleteObjects).toHaveBeenCalledTimes(0); +}); + +test('does nothing on update event when the bucket name remains the same but the service token changes', async () => { + // GIVEN + const event: Partial = { + RequestType: 'Update', + ResourceProperties: { + ServiceToken: 'Foo', + BucketName: 'MyBucket', + }, + OldResourceProperties: { + ServiceToken: 'Bar', + BucketName: 'MyBucket', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockS3Client.listObjectVersions).toHaveBeenCalledTimes(0); + expect(mockS3Client.deleteObjects).toHaveBeenCalledTimes(0); +}); + +test('does nothing on update event when the old resource properties are absent', async () => { // GIVEN const event: Partial = { RequestType: 'Update', @@ -55,6 +99,63 @@ test('does nothing on update event', async () => { expect(mockS3Client.deleteObjects).toHaveBeenCalledTimes(0); }); +test('does nothing on update event when the new resource properties are absent', async () => { + // GIVEN + const event: Partial = { + RequestType: 'Update', + OldResourceProperties: { + ServiceToken: 'Foo', + BucketName: 'MyBucket', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockS3Client.listObjectVersions).toHaveBeenCalledTimes(0); + expect(mockS3Client.deleteObjects).toHaveBeenCalledTimes(0); +}); + +test('deletes all objects when the name changes on update event', async () => { + // GIVEN + mockS3Client.promise.mockResolvedValue({ // listObjectVersions() call + Versions: [ + { Key: 'Key1', VersionId: 'VersionId1' }, + { Key: 'Key2', VersionId: 'VersionId2' }, + ], + }); + + const event: Partial = { + RequestType: 'Update', + OldResourceProperties: { + ServiceToken: 'Foo', + BucketName: 'MyBucket', + }, + ResourceProperties: { + ServiceToken: 'Foo', + BucketName: 'MyBucket-renamed', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockS3Client.listObjectVersions).toHaveBeenCalledTimes(1); + expect(mockS3Client.listObjectVersions).toHaveBeenCalledWith({ Bucket: 'MyBucket' }); + expect(mockS3Client.deleteObjects).toHaveBeenCalledTimes(1); + expect(mockS3Client.deleteObjects).toHaveBeenCalledWith({ + Bucket: 'MyBucket', + Delete: { + Objects: [ + { Key: 'Key1', VersionId: 'VersionId1' }, + { Key: 'Key2', VersionId: 'VersionId2' }, + ], + }, + }); +}); + test('deletes no objects on delete event when bucket has no objects', async () => { // GIVEN mockS3Client.promise.mockResolvedValue({ Versions: [] }); // listObjectVersions() call diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json b/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json index 107132c1cd2dd..6623d9d3e7a8c 100644 --- a/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json @@ -102,7 +102,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3BucketF01ADF6B" + "Ref": "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3BucketD715D8B0" }, "S3Key": { "Fn::Join": [ @@ -115,7 +115,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3VersionKey6FC34F51" + "Ref": "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3VersionKey6E76822C" } ] } @@ -128,7 +128,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3VersionKey6FC34F51" + "Ref": "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3VersionKey6E76822C" } ] } @@ -289,17 +289,17 @@ } }, "Parameters": { - "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3BucketF01ADF6B": { + "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3BucketD715D8B0": { "Type": "String", - "Description": "S3 bucket for asset \"1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1\"" + "Description": "S3 bucket for asset \"fe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9\"" }, - "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3VersionKey6FC34F51": { + "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3VersionKey6E76822C": { "Type": "String", - "Description": "S3 key for asset version \"1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1\"" + "Description": "S3 key for asset version \"fe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9\"" }, - "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1ArtifactHash9ECACDFD": { + "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9ArtifactHash9AE3702B": { "Type": "String", - "Description": "Artifact hash for asset \"1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1\"" + "Description": "Artifact hash for asset \"fe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9\"" }, "AssetParametersf7ee44e9b6217d201200d9abd42c6493b0b11e86be8a7f36163c3ea049c54653S3BucketDB5FAF47": { "Type": "String", From 5812340bfaebfef2bc41d5e7fbd0d45af92e0e49 Mon Sep 17 00:00:00 2001 From: Eli Polonsky Date: Wed, 25 Aug 2021 11:05:24 -0700 Subject: [PATCH 17/91] chore(integ): fix regression suite failures (#16226) Follow up to https://github.com/aws/aws-cdk/pull/16216 We need to apply a patch for the regression suite to pass because its running the older tests which still make the wrong assertion. See `NOTES.md` for more details. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../cli-regression-patches/v1.119.0/NOTES.md | 5 + .../v1.119.0/cli.integtest.js | 659 ++++++++++++++++++ 2 files changed, 664 insertions(+) create mode 100644 packages/aws-cdk/test/integ/cli-regression-patches/v1.119.0/NOTES.md create mode 100644 packages/aws-cdk/test/integ/cli-regression-patches/v1.119.0/cli.integtest.js diff --git a/packages/aws-cdk/test/integ/cli-regression-patches/v1.119.0/NOTES.md b/packages/aws-cdk/test/integ/cli-regression-patches/v1.119.0/NOTES.md new file mode 100644 index 0000000000000..5ca96b632f75a --- /dev/null +++ b/packages/aws-cdk/test/integ/cli-regression-patches/v1.119.0/NOTES.md @@ -0,0 +1,5 @@ +This [PR](https://github.com/aws/aws-cdk/pull/16205) added a node version check to our CLI courtesy of [`@jsii/check-node/run`](https://github.com/aws/jsii/tree/main/packages/%40jsii/check-node). + +This check now causes the CLI to print a deprecation warning that changes the output of the `synth` command. We don't consider this a breaking change since we have no guarantess for CLI output, but it did break some our integ tests (namely `cdk synth`) that used to rely on a specific output. + +This patch brings the [fix](https://github.com/aws/aws-cdk/pull/16216) into the regression suite. \ No newline at end of file diff --git a/packages/aws-cdk/test/integ/cli-regression-patches/v1.119.0/cli.integtest.js b/packages/aws-cdk/test/integ/cli-regression-patches/v1.119.0/cli.integtest.js new file mode 100644 index 0000000000000..b8009dcaab00e --- /dev/null +++ b/packages/aws-cdk/test/integ/cli-regression-patches/v1.119.0/cli.integtest.js @@ -0,0 +1,659 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs_1 = require("fs"); +const os = require("os"); +const path = require("path"); +const aws_1 = require("../helpers/aws"); +const cdk_1 = require("../helpers/cdk"); +const test_helpers_1 = require("../helpers/test-helpers"); +jest.setTimeout(600 * 1000); +test_helpers_1.integTest('VPC Lookup', cdk_1.withDefaultFixture(async (fixture) => { + fixture.log('Making sure we are clean before starting.'); + await fixture.cdkDestroy('define-vpc', { modEnv: { ENABLE_VPC_TESTING: 'DEFINE' } }); + fixture.log('Setting up: creating a VPC with known tags'); + await fixture.cdkDeploy('define-vpc', { modEnv: { ENABLE_VPC_TESTING: 'DEFINE' } }); + fixture.log('Setup complete!'); + fixture.log('Verifying we can now import that VPC'); + await fixture.cdkDeploy('import-vpc', { modEnv: { ENABLE_VPC_TESTING: 'IMPORT' } }); +})); +test_helpers_1.integTest('Two ways of shoing the version', cdk_1.withDefaultFixture(async (fixture) => { + const version1 = await fixture.cdk(['version'], { verbose: false }); + const version2 = await fixture.cdk(['--version'], { verbose: false }); + expect(version1).toEqual(version2); +})); +test_helpers_1.integTest('Termination protection', cdk_1.withDefaultFixture(async (fixture) => { + const stackName = 'termination-protection'; + await fixture.cdkDeploy(stackName); + // Try a destroy that should fail + await expect(fixture.cdkDestroy(stackName)).rejects.toThrow('exited with error'); + // Can update termination protection even though the change set doesn't contain changes + await fixture.cdkDeploy(stackName, { modEnv: { TERMINATION_PROTECTION: 'FALSE' } }); + await fixture.cdkDestroy(stackName); +})); +test_helpers_1.integTest('cdk synth', cdk_1.withDefaultFixture(async (fixture) => { + await fixture.cdk(['synth', fixture.fullStackName('test-1')]); + const template1 = await readTemplate(fixture, 'test-1'); + expect(template1).toEqual({ + Resources: { + topic69831491: { + Type: 'AWS::SNS::Topic', + Metadata: { + 'aws:cdk:path': `${fixture.stackNamePrefix}-test-1/topic/Resource`, + }, + }, + }, + }); + await fixture.cdk(['synth', fixture.fullStackName('test-2')], { verbose: false }); + const template2 = await readTemplate(fixture, 'test-2'); + expect(template2).toEqual({ + Resources: { + topic152D84A37: { + Type: 'AWS::SNS::Topic', + Metadata: { + 'aws:cdk:path': `${fixture.stackNamePrefix}-test-2/topic1/Resource`, + }, + }, + topic2A4FB547F: { + Type: 'AWS::SNS::Topic', + Metadata: { + 'aws:cdk:path': `${fixture.stackNamePrefix}-test-2/topic2/Resource`, + }, + }, + }, + }); +})); +test_helpers_1.integTest('ssm parameter provider error', cdk_1.withDefaultFixture(async (fixture) => { + await expect(fixture.cdk(['synth', + fixture.fullStackName('missing-ssm-parameter'), + '-c', 'test:ssm-parameter-name=/does/not/exist'], { + allowErrExit: true, + })).resolves.toContain('SSM parameter not available in account'); +})); +test_helpers_1.integTest('automatic ordering', cdk_1.withDefaultFixture(async (fixture) => { + // Deploy the consuming stack which will include the producing stack + await fixture.cdkDeploy('order-consuming'); + // Destroy the providing stack which will include the consuming stack + await fixture.cdkDestroy('order-providing'); +})); +test_helpers_1.integTest('context setting', cdk_1.withDefaultFixture(async (fixture) => { + await fs_1.promises.writeFile(path.join(fixture.integTestDir, 'cdk.context.json'), JSON.stringify({ + contextkey: 'this is the context value', + })); + try { + await expect(fixture.cdk(['context'])).resolves.toContain('this is the context value'); + // Test that deleting the contextkey works + await fixture.cdk(['context', '--reset', 'contextkey']); + await expect(fixture.cdk(['context'])).resolves.not.toContain('this is the context value'); + // Test that forced delete of the context key does not throw + await fixture.cdk(['context', '-f', '--reset', 'contextkey']); + } + finally { + await fs_1.promises.unlink(path.join(fixture.integTestDir, 'cdk.context.json')); + } +})); +test_helpers_1.integTest('context in stage propagates to top', cdk_1.withDefaultFixture(async (fixture) => { + await expect(fixture.cdkSynth({ + // This will make it error to prove that the context bubbles up, and also that we can fail on command + options: ['--no-lookups'], + modEnv: { + INTEG_STACK_SET: 'stage-using-context', + }, + allowErrExit: true, + })).resolves.toContain('Context lookups have been disabled'); +})); +test_helpers_1.integTest('deploy', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + const stackArn = await fixture.cdkDeploy('test-2', { captureStderr: false }); + // verify the number of resources in the stack + const response = await fixture.aws.cloudFormation('describeStackResources', { + StackName: stackArn, + }); + expect((_a = response.StackResources) === null || _a === void 0 ? void 0 : _a.length).toEqual(2); +})); +test_helpers_1.integTest('deploy all', cdk_1.withDefaultFixture(async (fixture) => { + const arns = await fixture.cdkDeploy('test-*', { captureStderr: false }); + // verify that we only deployed a single stack (there's a single ARN in the output) + expect(arns.split('\n').length).toEqual(2); +})); +test_helpers_1.integTest('nested stack with parameters', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + // STACK_NAME_PREFIX is used in MyTopicParam to allow multiple instances + // of this test to run in parallel, othewise they will attempt to create the same SNS topic. + const stackArn = await fixture.cdkDeploy('with-nested-stack-using-parameters', { + options: ['--parameters', `MyTopicParam=${fixture.stackNamePrefix}ThereIsNoSpoon`], + captureStderr: false, + }); + // verify that we only deployed a single stack (there's a single ARN in the output) + expect(stackArn.split('\n').length).toEqual(1); + // verify the number of resources in the stack + const response = await fixture.aws.cloudFormation('describeStackResources', { + StackName: stackArn, + }); + expect((_a = response.StackResources) === null || _a === void 0 ? void 0 : _a.length).toEqual(1); +})); +test_helpers_1.integTest('deploy without execute a named change set', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + const changeSetName = 'custom-change-set-name'; + const stackArn = await fixture.cdkDeploy('test-2', { + options: ['--no-execute', '--change-set-name', changeSetName], + captureStderr: false, + }); + // verify that we only deployed a single stack (there's a single ARN in the output) + expect(stackArn.split('\n').length).toEqual(1); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].StackStatus).toEqual('REVIEW_IN_PROGRESS'); + //verify a change set was created with the provided name + const changeSetResponse = await fixture.aws.cloudFormation('listChangeSets', { + StackName: stackArn, + }); + const changeSets = changeSetResponse.Summaries || []; + expect(changeSets.length).toEqual(1); + expect(changeSets[0].ChangeSetName).toEqual(changeSetName); + expect(changeSets[0].Status).toEqual('CREATE_COMPLETE'); +})); +test_helpers_1.integTest('security related changes without a CLI are expected to fail', cdk_1.withDefaultFixture(async (fixture) => { + // redirect /dev/null to stdin, which means there will not be tty attached + // since this stack includes security-related changes, the deployment should + // immediately fail because we can't confirm the changes + const stackName = 'iam-test'; + await expect(fixture.cdkDeploy(stackName, { + options: ['<', '/dev/null'], + neverRequireApproval: false, + })).rejects.toThrow('exited with error'); + // Ensure stack was not deployed + await expect(fixture.aws.cloudFormation('describeStacks', { + StackName: fixture.fullStackName(stackName), + })).rejects.toThrow('does not exist'); +})); +test_helpers_1.integTest('deploy wildcard with outputs', cdk_1.withDefaultFixture(async (fixture) => { + const outputsFile = path.join(fixture.integTestDir, 'outputs', 'outputs.json'); + await fs_1.promises.mkdir(path.dirname(outputsFile), { recursive: true }); + await fixture.cdkDeploy(['outputs-test-*'], { + options: ['--outputs-file', outputsFile], + }); + const outputs = JSON.parse((await fs_1.promises.readFile(outputsFile, { encoding: 'utf-8' })).toString()); + expect(outputs).toEqual({ + [`${fixture.stackNamePrefix}-outputs-test-1`]: { + TopicName: `${fixture.stackNamePrefix}-outputs-test-1MyTopic`, + }, + [`${fixture.stackNamePrefix}-outputs-test-2`]: { + TopicName: `${fixture.stackNamePrefix}-outputs-test-2MyOtherTopic`, + }, + }); +})); +test_helpers_1.integTest('deploy with parameters', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + const stackArn = await fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}bazinga`, + ], + captureStderr: false, + }); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].Parameters).toEqual([ + { + ParameterKey: 'TopicNameParam', + ParameterValue: `${fixture.stackNamePrefix}bazinga`, + }, + ]); +})); +test_helpers_1.integTest('update to stack in ROLLBACK_COMPLETE state will delete stack and create a new one', cdk_1.withDefaultFixture(async (fixture) => { + var _a, _b, _c, _d; + // GIVEN + await expect(fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}@aww`, + ], + captureStderr: false, + })).rejects.toThrow('exited with error'); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: fixture.fullStackName('param-test-1'), + }); + const stackArn = (_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].StackId; + expect((_b = response.Stacks) === null || _b === void 0 ? void 0 : _b[0].StackStatus).toEqual('ROLLBACK_COMPLETE'); + // WHEN + const newStackArn = await fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}allgood`, + ], + captureStderr: false, + }); + const newStackResponse = await fixture.aws.cloudFormation('describeStacks', { + StackName: newStackArn, + }); + // THEN + expect(stackArn).not.toEqual(newStackArn); // new stack was created + expect((_c = newStackResponse.Stacks) === null || _c === void 0 ? void 0 : _c[0].StackStatus).toEqual('CREATE_COMPLETE'); + expect((_d = newStackResponse.Stacks) === null || _d === void 0 ? void 0 : _d[0].Parameters).toEqual([ + { + ParameterKey: 'TopicNameParam', + ParameterValue: `${fixture.stackNamePrefix}allgood`, + }, + ]); +})); +test_helpers_1.integTest('stack in UPDATE_ROLLBACK_COMPLETE state can be updated', cdk_1.withDefaultFixture(async (fixture) => { + var _a, _b, _c, _d; + // GIVEN + const stackArn = await fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}nice`, + ], + captureStderr: false, + }); + let response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].StackStatus).toEqual('CREATE_COMPLETE'); + // bad parameter name with @ will put stack into UPDATE_ROLLBACK_COMPLETE + await expect(fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}@aww`, + ], + captureStderr: false, + })).rejects.toThrow('exited with error'); + ; + response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_b = response.Stacks) === null || _b === void 0 ? void 0 : _b[0].StackStatus).toEqual('UPDATE_ROLLBACK_COMPLETE'); + // WHEN + await fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}allgood`, + ], + captureStderr: false, + }); + response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + // THEN + expect((_c = response.Stacks) === null || _c === void 0 ? void 0 : _c[0].StackStatus).toEqual('UPDATE_COMPLETE'); + expect((_d = response.Stacks) === null || _d === void 0 ? void 0 : _d[0].Parameters).toEqual([ + { + ParameterKey: 'TopicNameParam', + ParameterValue: `${fixture.stackNamePrefix}allgood`, + }, + ]); +})); +test_helpers_1.integTest('deploy with wildcard and parameters', cdk_1.withDefaultFixture(async (fixture) => { + await fixture.cdkDeploy('param-test-*', { + options: [ + '--parameters', `${fixture.stackNamePrefix}-param-test-1:TopicNameParam=${fixture.stackNamePrefix}bazinga`, + '--parameters', `${fixture.stackNamePrefix}-param-test-2:OtherTopicNameParam=${fixture.stackNamePrefix}ThatsMySpot`, + '--parameters', `${fixture.stackNamePrefix}-param-test-3:DisplayNameParam=${fixture.stackNamePrefix}HeyThere`, + '--parameters', `${fixture.stackNamePrefix}-param-test-3:OtherDisplayNameParam=${fixture.stackNamePrefix}AnotherOne`, + ], + }); +})); +test_helpers_1.integTest('deploy with parameters multi', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + const paramVal1 = `${fixture.stackNamePrefix}bazinga`; + const paramVal2 = `${fixture.stackNamePrefix}=jagshemash`; + const stackArn = await fixture.cdkDeploy('param-test-3', { + options: [ + '--parameters', `DisplayNameParam=${paramVal1}`, + '--parameters', `OtherDisplayNameParam=${paramVal2}`, + ], + captureStderr: false, + }); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].Parameters).toEqual([ + { + ParameterKey: 'DisplayNameParam', + ParameterValue: paramVal1, + }, + { + ParameterKey: 'OtherDisplayNameParam', + ParameterValue: paramVal2, + }, + ]); +})); +test_helpers_1.integTest('deploy with notification ARN', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + const topicName = `${fixture.stackNamePrefix}-test-topic`; + const response = await fixture.aws.sns('createTopic', { Name: topicName }); + const topicArn = response.TopicArn; + try { + await fixture.cdkDeploy('test-2', { + options: ['--notification-arns', topicArn], + }); + // verify that the stack we deployed has our notification ARN + const describeResponse = await fixture.aws.cloudFormation('describeStacks', { + StackName: fixture.fullStackName('test-2'), + }); + expect((_a = describeResponse.Stacks) === null || _a === void 0 ? void 0 : _a[0].NotificationARNs).toEqual([topicArn]); + } + finally { + await fixture.aws.sns('deleteTopic', { + TopicArn: topicArn, + }); + } +})); +test_helpers_1.integTest('deploy with role', cdk_1.withDefaultFixture(async (fixture) => { + const roleName = `${fixture.stackNamePrefix}-test-role`; + await deleteRole(); + const createResponse = await fixture.aws.iam('createRole', { + RoleName: roleName, + AssumeRolePolicyDocument: JSON.stringify({ + Version: '2012-10-17', + Statement: [{ + Action: 'sts:AssumeRole', + Principal: { Service: 'cloudformation.amazonaws.com' }, + Effect: 'Allow', + }, { + Action: 'sts:AssumeRole', + Principal: { AWS: (await fixture.aws.sts('getCallerIdentity', {})).Arn }, + Effect: 'Allow', + }], + }), + }); + const roleArn = createResponse.Role.Arn; + try { + await fixture.aws.iam('putRolePolicy', { + RoleName: roleName, + PolicyName: 'DefaultPolicy', + PolicyDocument: JSON.stringify({ + Version: '2012-10-17', + Statement: [{ + Action: '*', + Resource: '*', + Effect: 'Allow', + }], + }), + }); + await aws_1.retry(fixture.output, 'Trying to assume fresh role', aws_1.retry.forSeconds(300), async () => { + await fixture.aws.sts('assumeRole', { + RoleArn: roleArn, + RoleSessionName: 'testing', + }); + }); + // In principle, the role has replicated from 'us-east-1' to wherever we're testing. + // Give it a little more sleep to make sure CloudFormation is not hitting a box + // that doesn't have it yet. + await aws_1.sleep(5000); + await fixture.cdkDeploy('test-2', { + options: ['--role-arn', roleArn], + }); + // Immediately delete the stack again before we delete the role. + // + // Since roles are sticky, if we delete the role before the stack, subsequent DeleteStack + // operations will fail when CloudFormation tries to assume the role that's already gone. + await fixture.cdkDestroy('test-2'); + } + finally { + await deleteRole(); + } + async function deleteRole() { + try { + for (const policyName of (await fixture.aws.iam('listRolePolicies', { RoleName: roleName })).PolicyNames) { + await fixture.aws.iam('deleteRolePolicy', { + RoleName: roleName, + PolicyName: policyName, + }); + } + await fixture.aws.iam('deleteRole', { RoleName: roleName }); + } + catch (e) { + if (e.message.indexOf('cannot be found') > -1) { + return; + } + throw e; + } + } +})); +test_helpers_1.integTest('cdk diff', cdk_1.withDefaultFixture(async (fixture) => { + const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); + expect(diff1).toContain('AWS::SNS::Topic'); + const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); + expect(diff2).toContain('AWS::SNS::Topic'); + // We can make it fail by passing --fail + await expect(fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1')])) + .rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('cdk diff --fail on multiple stacks exits with error if any of the stacks contains a diff', cdk_1.withDefaultFixture(async (fixture) => { + // GIVEN + const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); + expect(diff1).toContain('AWS::SNS::Topic'); + await fixture.cdkDeploy('test-2'); + const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); + expect(diff2).toContain('There were no differences'); + // WHEN / THEN + await expect(fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1'), fixture.fullStackName('test-2')])).rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('cdk diff --fail with multiple stack exits with if any of the stacks contains a diff', cdk_1.withDefaultFixture(async (fixture) => { + // GIVEN + await fixture.cdkDeploy('test-1'); + const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); + expect(diff1).toContain('There were no differences'); + const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); + expect(diff2).toContain('AWS::SNS::Topic'); + // WHEN / THEN + await expect(fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1'), fixture.fullStackName('test-2')])).rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('cdk diff --security-only --fail exits when security changes are present', cdk_1.withDefaultFixture(async (fixture) => { + const stackName = 'iam-test'; + await expect(fixture.cdk(['diff', '--security-only', '--fail', fixture.fullStackName(stackName)])).rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('deploy stack with docker asset', cdk_1.withDefaultFixture(async (fixture) => { + await fixture.cdkDeploy('docker'); +})); +test_helpers_1.integTest('deploy and test stack with lambda asset', cdk_1.withDefaultFixture(async (fixture) => { + var _a, _b; + const stackArn = await fixture.cdkDeploy('lambda', { captureStderr: false }); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + const lambdaArn = (_b = (_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].Outputs) === null || _b === void 0 ? void 0 : _b[0].OutputValue; + if (lambdaArn === undefined) { + throw new Error('Stack did not have expected Lambda ARN output'); + } + const output = await fixture.aws.lambda('invoke', { + FunctionName: lambdaArn, + }); + expect(JSON.stringify(output.Payload)).toContain('dear asset'); +})); +test_helpers_1.integTest('cdk ls', cdk_1.withDefaultFixture(async (fixture) => { + const listing = await fixture.cdk(['ls'], { captureStderr: false }); + const expectedStacks = [ + 'conditional-resource', + 'docker', + 'docker-with-custom-file', + 'failed', + 'iam-test', + 'lambda', + 'missing-ssm-parameter', + 'order-providing', + 'outputs-test-1', + 'outputs-test-2', + 'param-test-1', + 'param-test-2', + 'param-test-3', + 'termination-protection', + 'test-1', + 'test-2', + 'with-nested-stack', + 'with-nested-stack-using-parameters', + 'order-consuming', + ]; + for (const stack of expectedStacks) { + expect(listing).toContain(fixture.fullStackName(stack)); + } +})); +test_helpers_1.integTest('synthing a stage with errors leads to failure', cdk_1.withDefaultFixture(async (fixture) => { + const output = await fixture.cdk(['synth'], { + allowErrExit: true, + modEnv: { + INTEG_STACK_SET: 'stage-with-errors', + }, + }); + expect(output).toContain('This is an error'); +})); +test_helpers_1.integTest('synthing a stage with errors can be suppressed', cdk_1.withDefaultFixture(async (fixture) => { + await fixture.cdk(['synth', '--no-validation'], { + modEnv: { + INTEG_STACK_SET: 'stage-with-errors', + }, + }); +})); +test_helpers_1.integTest('deploy stack without resource', cdk_1.withDefaultFixture(async (fixture) => { + // Deploy the stack without resources + await fixture.cdkDeploy('conditional-resource', { modEnv: { NO_RESOURCE: 'TRUE' } }); + // This should have succeeded but not deployed the stack. + await expect(fixture.aws.cloudFormation('describeStacks', { StackName: fixture.fullStackName('conditional-resource') })) + .rejects.toThrow('conditional-resource does not exist'); + // Deploy the stack with resources + await fixture.cdkDeploy('conditional-resource'); + // Then again WITHOUT resources (this should destroy the stack) + await fixture.cdkDeploy('conditional-resource', { modEnv: { NO_RESOURCE: 'TRUE' } }); + await expect(fixture.aws.cloudFormation('describeStacks', { StackName: fixture.fullStackName('conditional-resource') })) + .rejects.toThrow('conditional-resource does not exist'); +})); +test_helpers_1.integTest('IAM diff', cdk_1.withDefaultFixture(async (fixture) => { + const output = await fixture.cdk(['diff', fixture.fullStackName('iam-test')]); + // Roughly check for a table like this: + // + // ┌───┬─────────────────┬────────┬────────────────┬────────────────────────────-──┬───────────┐ + // │ │ Resource │ Effect │ Action │ Principal │ Condition │ + // ├───┼─────────────────┼────────┼────────────────┼───────────────────────────────┼───────────┤ + // │ + │ ${SomeRole.Arn} │ Allow │ sts:AssumeRole │ Service:ec2.amazonaws.com │ │ + // └───┴─────────────────┴────────┴────────────────┴───────────────────────────────┴───────────┘ + expect(output).toContain('${SomeRole.Arn}'); + expect(output).toContain('sts:AssumeRole'); + expect(output).toContain('ec2.amazonaws.com'); +})); +test_helpers_1.integTest('fast deploy', cdk_1.withDefaultFixture(async (fixture) => { + // we are using a stack with a nested stack because CFN will always attempt to + // update a nested stack, which will allow us to verify that updates are actually + // skipped unless --force is specified. + const stackArn = await fixture.cdkDeploy('with-nested-stack', { captureStderr: false }); + const changeSet1 = await getLatestChangeSet(); + // Deploy the same stack again, there should be no new change set created + await fixture.cdkDeploy('with-nested-stack'); + const changeSet2 = await getLatestChangeSet(); + expect(changeSet2.ChangeSetId).toEqual(changeSet1.ChangeSetId); + // Deploy the stack again with --force, now we should create a changeset + await fixture.cdkDeploy('with-nested-stack', { options: ['--force'] }); + const changeSet3 = await getLatestChangeSet(); + expect(changeSet3.ChangeSetId).not.toEqual(changeSet2.ChangeSetId); + // Deploy the stack again with tags, expected to create a new changeset + // even though the resources didn't change. + await fixture.cdkDeploy('with-nested-stack', { options: ['--tags', 'key=value'] }); + const changeSet4 = await getLatestChangeSet(); + expect(changeSet4.ChangeSetId).not.toEqual(changeSet3.ChangeSetId); + async function getLatestChangeSet() { + var _a, _b, _c; + const response = await fixture.aws.cloudFormation('describeStacks', { StackName: stackArn }); + if (!((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0])) { + throw new Error('Did not get a ChangeSet at all'); + } + fixture.log(`Found Change Set ${(_b = response.Stacks) === null || _b === void 0 ? void 0 : _b[0].ChangeSetId}`); + return (_c = response.Stacks) === null || _c === void 0 ? void 0 : _c[0]; + } +})); +test_helpers_1.integTest('failed deploy does not hang', cdk_1.withDefaultFixture(async (fixture) => { + // this will hang if we introduce https://github.com/aws/aws-cdk/issues/6403 again. + await expect(fixture.cdkDeploy('failed')).rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('can still load old assemblies', cdk_1.withDefaultFixture(async (fixture) => { + const cxAsmDir = path.join(os.tmpdir(), 'cdk-integ-cx'); + const testAssembliesDirectory = path.join(__dirname, 'cloud-assemblies'); + for (const asmdir of await listChildDirs(testAssembliesDirectory)) { + fixture.log(`ASSEMBLY ${asmdir}`); + await cdk_1.cloneDirectory(asmdir, cxAsmDir); + // Some files in the asm directory that have a .js extension are + // actually treated as templates. Evaluate them using NodeJS. + const templates = await listChildren(cxAsmDir, fullPath => Promise.resolve(fullPath.endsWith('.js'))); + for (const template of templates) { + const targetName = template.replace(/.js$/, ''); + await cdk_1.shell([process.execPath, template, '>', targetName], { + cwd: cxAsmDir, + output: fixture.output, + modEnv: { + TEST_ACCOUNT: await fixture.aws.account(), + TEST_REGION: fixture.aws.region, + }, + }); + } + // Use this directory as a Cloud Assembly + const output = await fixture.cdk([ + '--app', cxAsmDir, + '-v', + 'synth', + ]); + // Assert that there was no providerError in CDK's stderr + // Because we rely on the app/framework to actually error in case the + // provider fails, we inspect the logs here. + expect(output).not.toContain('$providerError'); + } +})); +test_helpers_1.integTest('generating and loading assembly', cdk_1.withDefaultFixture(async (fixture) => { + const asmOutputDir = `${fixture.integTestDir}-cdk-integ-asm`; + await fixture.shell(['rm', '-rf', asmOutputDir]); + // Synthesize a Cloud Assembly tothe default directory (cdk.out) and a specific directory. + await fixture.cdk(['synth']); + await fixture.cdk(['synth', '--output', asmOutputDir]); + // cdk.out in the current directory and the indicated --output should be the same + await fixture.shell(['diff', 'cdk.out', asmOutputDir]); + // Check that we can 'ls' the synthesized asm. + // Change to some random directory to make sure we're not accidentally loading cdk.json + const list = await fixture.cdk(['--app', asmOutputDir, 'ls'], { cwd: os.tmpdir() }); + // Same stacks we know are in the app + expect(list).toContain(`${fixture.stackNamePrefix}-lambda`); + expect(list).toContain(`${fixture.stackNamePrefix}-test-1`); + expect(list).toContain(`${fixture.stackNamePrefix}-test-2`); + // Check that we can use '.' and just synth ,the generated asm + const stackTemplate = await fixture.cdk(['--app', '.', 'synth', fixture.fullStackName('test-2')], { + cwd: asmOutputDir, + }); + expect(stackTemplate).toContain('topic152D84A37'); + // Deploy a Lambda from the copied asm + await fixture.cdkDeploy('lambda', { options: ['-a', '.'], cwd: asmOutputDir }); + // Remove (rename) the original custom docker file that was used during synth. + // this verifies that the assemly has a copy of it and that the manifest uses + // relative paths to reference to it. + const customDockerFile = path.join(fixture.integTestDir, 'docker', 'Dockerfile.Custom'); + await fs_1.promises.rename(customDockerFile, `${customDockerFile}~`); + try { + // deploy a docker image with custom file without synth (uses assets) + await fixture.cdkDeploy('docker-with-custom-file', { options: ['-a', '.'], cwd: asmOutputDir }); + } + finally { + // Rename back to restore fixture to original state + await fs_1.promises.rename(`${customDockerFile}~`, customDockerFile); + } +})); +test_helpers_1.integTest('templates on disk contain metadata resource, also in nested assemblies', cdk_1.withDefaultFixture(async (fixture) => { + // Synth first, and switch on version reporting because cdk.json is disabling it + await fixture.cdk(['synth', '--version-reporting=true']); + // Load template from disk from root assembly + const templateContents = await fixture.shell(['cat', 'cdk.out/*-lambda.template.json']); + expect(JSON.parse(templateContents).Resources.CDKMetadata).toBeTruthy(); + // Load template from nested assembly + const nestedTemplateContents = await fixture.shell(['cat', 'cdk.out/assembly-*-stage/*-stage-StackInStage.template.json']); + expect(JSON.parse(nestedTemplateContents).Resources.CDKMetadata).toBeTruthy(); +})); +async function listChildren(parent, pred) { + const ret = new Array(); + for (const child of await fs_1.promises.readdir(parent, { encoding: 'utf-8' })) { + const fullPath = path.join(parent, child.toString()); + if (await pred(fullPath)) { + ret.push(fullPath); + } + } + return ret; +} +async function listChildDirs(parent) { + return listChildren(parent, async (fullPath) => (await fs_1.promises.stat(fullPath)).isDirectory()); +} +async function readTemplate(fixture, stackName) { + const fullStackName = fixture.fullStackName(stackName); + const templatePath = path.join(fixture.integTestDir, 'cdk.out', `${fullStackName}.template.json`); + return JSON.parse((await fs_1.promises.readFile(templatePath, { encoding: 'utf-8' })).toString()); +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xpLmludGVndGVzdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImNsaS5pbnRlZ3Rlc3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBQSwyQkFBb0M7QUFDcEMseUJBQXlCO0FBQ3pCLDZCQUE2QjtBQUM3Qix3Q0FBOEM7QUFDOUMsd0NBQXdGO0FBQ3hGLDBEQUFvRDtBQUVwRCxJQUFJLENBQUMsVUFBVSxDQUFDLEdBQUcsR0FBRyxJQUFJLENBQUMsQ0FBQztBQUU1Qix3QkFBUyxDQUFDLFlBQVksRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7SUFDM0QsT0FBTyxDQUFDLEdBQUcsQ0FBQywyQ0FBMkMsQ0FBQyxDQUFDO0lBQ3pELE1BQU0sT0FBTyxDQUFDLFVBQVUsQ0FBQyxZQUFZLEVBQUUsRUFBRSxNQUFNLEVBQUUsRUFBRSxrQkFBa0IsRUFBRSxRQUFRLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFFckYsT0FBTyxDQUFDLEdBQUcsQ0FBQyw0Q0FBNEMsQ0FBQyxDQUFDO0lBQzFELE1BQU0sT0FBTyxDQUFDLFNBQVMsQ0FBQyxZQUFZLEVBQUUsRUFBRSxNQUFNLEVBQUUsRUFBRSxrQkFBa0IsRUFBRSxRQUFRLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDcEYsT0FBTyxDQUFDLEdBQUcsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0lBRS9CLE9BQU8sQ0FBQyxHQUFHLENBQUMsc0NBQXNDLENBQUMsQ0FBQztJQUNwRCxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsWUFBWSxFQUFFLEVBQUUsTUFBTSxFQUFFLEVBQUUsa0JBQWtCLEVBQUUsUUFBUSxFQUFFLEVBQUUsQ0FBQyxDQUFDO0FBQ3RGLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLGdDQUFnQyxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUMvRSxNQUFNLFFBQVEsR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxTQUFTLENBQUMsRUFBRSxFQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFDO0lBQ3BFLE1BQU0sUUFBUSxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDLFdBQVcsQ0FBQyxFQUFFLEVBQUUsT0FBTyxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUM7SUFFdEUsTUFBTSxDQUFDLFFBQVEsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUMsQ0FBQztBQUNyQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyx3QkFBd0IsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7SUFDdkUsTUFBTSxTQUFTLEdBQUcsd0JBQXdCLENBQUM7SUFDM0MsTUFBTSxPQUFPLENBQUMsU0FBUyxDQUFDLFNBQVMsQ0FBQyxDQUFDO0lBRW5DLGlDQUFpQztJQUNqQyxNQUFNLE1BQU0sQ0FBQyxPQUFPLENBQUMsVUFBVSxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxtQkFBbUIsQ0FBQyxDQUFDO0lBRWpGLHVGQUF1RjtJQUN2RixNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsU0FBUyxFQUFFLEVBQUUsTUFBTSxFQUFFLEVBQUUsc0JBQXNCLEVBQUUsT0FBTyxFQUFFLEVBQUUsQ0FBQyxDQUFDO0lBQ3BGLE1BQU0sT0FBTyxDQUFDLFVBQVUsQ0FBQyxTQUFTLENBQUMsQ0FBQztBQUN0QyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyxXQUFXLEVBQUUsd0JBQWtCLENBQUMsS0FBSyxFQUFFLE9BQU8sRUFBRSxFQUFFO0lBQzFELE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDLE9BQU8sRUFBRSxPQUFPLENBQUMsYUFBYSxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUM5RCxNQUFNLFNBQVMsR0FBRyxNQUFNLFlBQVksQ0FBQyxPQUFPLEVBQUUsUUFBUSxDQUFDLENBQUM7SUFDeEQsTUFBTSxDQUFDLFNBQVMsQ0FBQyxDQUFDLE9BQU8sQ0FBQztRQUN4QixTQUFTLEVBQUU7WUFDVCxhQUFhLEVBQUU7Z0JBQ2IsSUFBSSxFQUFFLGlCQUFpQjtnQkFDdkIsUUFBUSxFQUFFO29CQUNSLGNBQWMsRUFBRSxHQUFHLE9BQU8sQ0FBQyxlQUFlLHdCQUF3QjtpQkFDbkU7YUFDRjtTQUNGO0tBQ0YsQ0FBQyxDQUFDO0lBRUgsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsT0FBTyxFQUFFLE9BQU8sQ0FBQyxhQUFhLENBQUMsUUFBUSxDQUFDLENBQUMsRUFBRSxFQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFDO0lBQ2xGLE1BQU0sU0FBUyxHQUFHLE1BQU0sWUFBWSxDQUFDLE9BQU8sRUFBRSxRQUFRLENBQUMsQ0FBQztJQUN4RCxNQUFNLENBQUMsU0FBUyxDQUFDLENBQUMsT0FBTyxDQUFDO1FBQ3hCLFNBQVMsRUFBRTtZQUNULGNBQWMsRUFBRTtnQkFDZCxJQUFJLEVBQUUsaUJBQWlCO2dCQUN2QixRQUFRLEVBQUU7b0JBQ1IsY0FBYyxFQUFFLEdBQUcsT0FBTyxDQUFDLGVBQWUseUJBQXlCO2lCQUNwRTthQUNGO1lBQ0QsY0FBYyxFQUFFO2dCQUNkLElBQUksRUFBRSxpQkFBaUI7Z0JBQ3ZCLFFBQVEsRUFBRTtvQkFDUixjQUFjLEVBQUUsR0FBRyxPQUFPLENBQUMsZUFBZSx5QkFBeUI7aUJBQ3BFO2FBQ0Y7U0FDRjtLQUNGLENBQUMsQ0FBQztBQUVMLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLDhCQUE4QixFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUM3RSxNQUFNLE1BQU0sQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsT0FBTztRQUMvQixPQUFPLENBQUMsYUFBYSxDQUFDLHVCQUF1QixDQUFDO1FBQzlDLElBQUksRUFBRSx5Q0FBeUMsQ0FBQyxFQUFFO1FBQ2xELFlBQVksRUFBRSxJQUFJO0tBQ25CLENBQUMsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxTQUFTLENBQUMsd0NBQXdDLENBQUMsQ0FBQztBQUNuRSxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyxvQkFBb0IsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7SUFDbkUsb0VBQW9FO0lBQ3BFLE1BQU0sT0FBTyxDQUFDLFNBQVMsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0lBRTNDLHFFQUFxRTtJQUNyRSxNQUFNLE9BQU8sQ0FBQyxVQUFVLENBQUMsaUJBQWlCLENBQUMsQ0FBQztBQUM5QyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyxpQkFBaUIsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7SUFDaEUsTUFBTSxhQUFFLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLFlBQVksRUFBRSxrQkFBa0IsQ0FBQyxFQUFFLElBQUksQ0FBQyxTQUFTLENBQUM7UUFDckYsVUFBVSxFQUFFLDJCQUEyQjtLQUN4QyxDQUFDLENBQUMsQ0FBQztJQUNKLElBQUk7UUFDRixNQUFNLE1BQU0sQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxTQUFTLENBQUMsMkJBQTJCLENBQUMsQ0FBQztRQUV2RiwwQ0FBMEM7UUFDMUMsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsU0FBUyxFQUFFLFNBQVMsRUFBRSxZQUFZLENBQUMsQ0FBQyxDQUFDO1FBQ3hELE1BQU0sTUFBTSxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLEdBQUcsQ0FBQyxTQUFTLENBQUMsMkJBQTJCLENBQUMsQ0FBQztRQUUzRiw0REFBNEQ7UUFDNUQsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsU0FBUyxFQUFFLElBQUksRUFBRSxTQUFTLEVBQUUsWUFBWSxDQUFDLENBQUMsQ0FBQztLQUUvRDtZQUFTO1FBQ1IsTUFBTSxhQUFFLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLFlBQVksRUFBRSxrQkFBa0IsQ0FBQyxDQUFDLENBQUM7S0FDdEU7QUFDSCxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyxvQ0FBb0MsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7SUFDbkYsTUFBTSxNQUFNLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQztRQUM1QixxR0FBcUc7UUFDckcsT0FBTyxFQUFFLENBQUMsY0FBYyxDQUFDO1FBQ3pCLE1BQU0sRUFBRTtZQUNOLGVBQWUsRUFBRSxxQkFBcUI7U0FDdkM7UUFDRCxZQUFZLEVBQUUsSUFBSTtLQUNuQixDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsU0FBUyxDQUFDLG9DQUFvQyxDQUFDLENBQUM7QUFDL0QsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUVKLHdCQUFTLENBQUMsUUFBUSxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTs7SUFDdkQsTUFBTSxRQUFRLEdBQUcsTUFBTSxPQUFPLENBQUMsU0FBUyxDQUFDLFFBQVEsRUFBRSxFQUFFLGFBQWEsRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFDO0lBRTdFLDhDQUE4QztJQUM5QyxNQUFNLFFBQVEsR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsY0FBYyxDQUFDLHdCQUF3QixFQUFFO1FBQzFFLFNBQVMsRUFBRSxRQUFRO0tBQ3BCLENBQUMsQ0FBQztJQUNILE1BQU0sT0FBQyxRQUFRLENBQUMsY0FBYywwQ0FBRSxNQUFNLENBQUMsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFDckQsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUVKLHdCQUFTLENBQUMsWUFBWSxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUMzRCxNQUFNLElBQUksR0FBRyxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsUUFBUSxFQUFFLEVBQUUsYUFBYSxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUM7SUFFekUsbUZBQW1GO0lBQ25GLE1BQU0sQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUM3QyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyw4QkFBOEIsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7O0lBQzdFLHdFQUF3RTtJQUN4RSw0RkFBNEY7SUFDNUYsTUFBTSxRQUFRLEdBQUcsTUFBTSxPQUFPLENBQUMsU0FBUyxDQUFDLG9DQUFvQyxFQUFFO1FBQzdFLE9BQU8sRUFBRSxDQUFDLGNBQWMsRUFBRSxnQkFBZ0IsT0FBTyxDQUFDLGVBQWUsZ0JBQWdCLENBQUM7UUFDbEYsYUFBYSxFQUFFLEtBQUs7S0FDckIsQ0FBQyxDQUFDO0lBRUgsbUZBQW1GO0lBQ25GLE1BQU0sQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUUvQyw4Q0FBOEM7SUFDOUMsTUFBTSxRQUFRLEdBQUcsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLGNBQWMsQ0FBQyx3QkFBd0IsRUFBRTtRQUMxRSxTQUFTLEVBQUUsUUFBUTtLQUNwQixDQUFDLENBQUM7SUFDSCxNQUFNLE9BQUMsUUFBUSxDQUFDLGNBQWMsMENBQUUsTUFBTSxDQUFDLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBQ3JELENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLDJDQUEyQyxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTs7SUFDMUYsTUFBTSxhQUFhLEdBQUcsd0JBQXdCLENBQUM7SUFDL0MsTUFBTSxRQUFRLEdBQUcsTUFBTSxPQUFPLENBQUMsU0FBUyxDQUFDLFFBQVEsRUFBRTtRQUNqRCxPQUFPLEVBQUUsQ0FBQyxjQUFjLEVBQUUsbUJBQW1CLEVBQUUsYUFBYSxDQUFDO1FBQzdELGFBQWEsRUFBRSxLQUFLO0tBQ3JCLENBQUMsQ0FBQztJQUNILG1GQUFtRjtJQUNuRixNQUFNLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsQ0FBQyxNQUFNLENBQUMsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFFL0MsTUFBTSxRQUFRLEdBQUcsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLGNBQWMsQ0FBQyxnQkFBZ0IsRUFBRTtRQUNsRSxTQUFTLEVBQUUsUUFBUTtLQUNwQixDQUFDLENBQUM7SUFDSCxNQUFNLE9BQUMsUUFBUSxDQUFDLE1BQU0sMENBQUcsQ0FBQyxFQUFFLFdBQVcsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxvQkFBb0IsQ0FBQyxDQUFDO0lBRXZFLHdEQUF3RDtJQUN4RCxNQUFNLGlCQUFpQixHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxjQUFjLENBQUMsZ0JBQWdCLEVBQUU7UUFDM0UsU0FBUyxFQUFFLFFBQVE7S0FDcEIsQ0FBQyxDQUFDO0lBQ0gsTUFBTSxVQUFVLEdBQUcsaUJBQWlCLENBQUMsU0FBUyxJQUFJLEVBQUUsQ0FBQztJQUNyRCxNQUFNLENBQUMsVUFBVSxDQUFDLE1BQU0sQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUNyQyxNQUFNLENBQUMsVUFBVSxDQUFDLENBQUMsQ0FBQyxDQUFDLGFBQWEsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxhQUFhLENBQUMsQ0FBQztJQUMzRCxNQUFNLENBQUMsVUFBVSxDQUFDLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLE9BQU8sQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0FBQzFELENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLDZEQUE2RCxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUM1RywwRUFBMEU7SUFDMUUsNEVBQTRFO0lBQzVFLHdEQUF3RDtJQUN4RCxNQUFNLFNBQVMsR0FBRyxVQUFVLENBQUM7SUFDN0IsTUFBTSxNQUFNLENBQUMsT0FBTyxDQUFDLFNBQVMsQ0FBQyxTQUFTLEVBQUU7UUFDeEMsT0FBTyxFQUFFLENBQUMsR0FBRyxFQUFFLFdBQVcsQ0FBQztRQUMzQixvQkFBb0IsRUFBRSxLQUFLO0tBQzVCLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxPQUFPLENBQUMsbUJBQW1CLENBQUMsQ0FBQztJQUV6QyxnQ0FBZ0M7SUFDaEMsTUFBTSxNQUFNLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxjQUFjLENBQUMsZ0JBQWdCLEVBQUU7UUFDeEQsU0FBUyxFQUFFLE9BQU8sQ0FBQyxhQUFhLENBQUMsU0FBUyxDQUFDO0tBQzVDLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxPQUFPLENBQUMsZ0JBQWdCLENBQUMsQ0FBQztBQUN4QyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyw4QkFBOEIsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7SUFDN0UsTUFBTSxXQUFXLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsWUFBWSxFQUFFLFNBQVMsRUFBRSxjQUFjLENBQUMsQ0FBQztJQUMvRSxNQUFNLGFBQUUsQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxXQUFXLENBQUMsRUFBRSxFQUFFLFNBQVMsRUFBRSxJQUFJLEVBQUUsQ0FBQyxDQUFDO0lBRS9ELE1BQU0sT0FBTyxDQUFDLFNBQVMsQ0FBQyxDQUFDLGdCQUFnQixDQUFDLEVBQUU7UUFDMUMsT0FBTyxFQUFFLENBQUMsZ0JBQWdCLEVBQUUsV0FBVyxDQUFDO0tBQ3pDLENBQUMsQ0FBQztJQUVILE1BQU0sT0FBTyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxNQUFNLGFBQUUsQ0FBQyxRQUFRLENBQUMsV0FBVyxFQUFFLEVBQUUsUUFBUSxFQUFFLE9BQU8sRUFBRSxDQUFDLENBQUMsQ0FBQyxRQUFRLEVBQUUsQ0FBQyxDQUFDO0lBQy9GLE1BQU0sQ0FBQyxPQUFPLENBQUMsQ0FBQyxPQUFPLENBQUM7UUFDdEIsQ0FBQyxHQUFHLE9BQU8sQ0FBQyxlQUFlLGlCQUFpQixDQUFDLEVBQUU7WUFDN0MsU0FBUyxFQUFFLEdBQUcsT0FBTyxDQUFDLGVBQWUsd0JBQXdCO1NBQzlEO1FBQ0QsQ0FBQyxHQUFHLE9BQU8sQ0FBQyxlQUFlLGlCQUFpQixDQUFDLEVBQUU7WUFDN0MsU0FBUyxFQUFFLEdBQUcsT0FBTyxDQUFDLGVBQWUsNkJBQTZCO1NBQ25FO0tBQ0YsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUVKLHdCQUFTLENBQUMsd0JBQXdCLEVBQUUsd0JBQWtCLENBQUMsS0FBSyxFQUFFLE9BQU8sRUFBRSxFQUFFOztJQUN2RSxNQUFNLFFBQVEsR0FBRyxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsY0FBYyxFQUFFO1FBQ3ZELE9BQU8sRUFBRTtZQUNQLGNBQWMsRUFBRSxrQkFBa0IsT0FBTyxDQUFDLGVBQWUsU0FBUztTQUNuRTtRQUNELGFBQWEsRUFBRSxLQUFLO0tBQ3JCLENBQUMsQ0FBQztJQUVILE1BQU0sUUFBUSxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxjQUFjLENBQUMsZ0JBQWdCLEVBQUU7UUFDbEUsU0FBUyxFQUFFLFFBQVE7S0FDcEIsQ0FBQyxDQUFDO0lBRUgsTUFBTSxPQUFDLFFBQVEsQ0FBQyxNQUFNLDBDQUFHLENBQUMsRUFBRSxVQUFVLENBQUMsQ0FBQyxPQUFPLENBQUM7UUFDOUM7WUFDRSxZQUFZLEVBQUUsZ0JBQWdCO1lBQzlCLGNBQWMsRUFBRSxHQUFHLE9BQU8sQ0FBQyxlQUFlLFNBQVM7U0FDcEQ7S0FDRixDQUFDLENBQUM7QUFDTCxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyxtRkFBbUYsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7O0lBQ2xJLFFBQVE7SUFDUixNQUFNLE1BQU0sQ0FBQyxPQUFPLENBQUMsU0FBUyxDQUFDLGNBQWMsRUFBRTtRQUM3QyxPQUFPLEVBQUU7WUFDUCxjQUFjLEVBQUUsa0JBQWtCLE9BQU8sQ0FBQyxlQUFlLE1BQU07U0FDaEU7UUFDRCxhQUFhLEVBQUUsS0FBSztLQUNyQixDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLG1CQUFtQixDQUFDLENBQUM7SUFFekMsTUFBTSxRQUFRLEdBQUcsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLGNBQWMsQ0FBQyxnQkFBZ0IsRUFBRTtRQUNsRSxTQUFTLEVBQUUsT0FBTyxDQUFDLGFBQWEsQ0FBQyxjQUFjLENBQUM7S0FDakQsQ0FBQyxDQUFDO0lBRUgsTUFBTSxRQUFRLFNBQUcsUUFBUSxDQUFDLE1BQU0sMENBQUcsQ0FBQyxFQUFFLE9BQU8sQ0FBQztJQUM5QyxNQUFNLE9BQUMsUUFBUSxDQUFDLE1BQU0sMENBQUcsQ0FBQyxFQUFFLFdBQVcsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxtQkFBbUIsQ0FBQyxDQUFDO0lBRXRFLE9BQU87SUFDUCxNQUFNLFdBQVcsR0FBRyxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsY0FBYyxFQUFFO1FBQzFELE9BQU8sRUFBRTtZQUNQLGNBQWMsRUFBRSxrQkFBa0IsT0FBTyxDQUFDLGVBQWUsU0FBUztTQUNuRTtRQUNELGFBQWEsRUFBRSxLQUFLO0tBQ3JCLENBQUMsQ0FBQztJQUVILE1BQU0sZ0JBQWdCLEdBQUcsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLGNBQWMsQ0FBQyxnQkFBZ0IsRUFBRTtRQUMxRSxTQUFTLEVBQUUsV0FBVztLQUN2QixDQUFDLENBQUM7SUFFSCxPQUFPO0lBQ1AsTUFBTSxDQUFFLFFBQVEsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxPQUFPLENBQUMsV0FBVyxDQUFDLENBQUMsQ0FBQyx3QkFBd0I7SUFDcEUsTUFBTSxPQUFDLGdCQUFnQixDQUFDLE1BQU0sMENBQUcsQ0FBQyxFQUFFLFdBQVcsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0lBQzVFLE1BQU0sT0FBQyxnQkFBZ0IsQ0FBQyxNQUFNLDBDQUFHLENBQUMsRUFBRSxVQUFVLENBQUMsQ0FBQyxPQUFPLENBQUM7UUFDdEQ7WUFDRSxZQUFZLEVBQUUsZ0JBQWdCO1lBQzlCLGNBQWMsRUFBRSxHQUFHLE9BQU8sQ0FBQyxlQUFlLFNBQVM7U0FDcEQ7S0FDRixDQUFDLENBQUM7QUFDTCxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyx3REFBd0QsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7O0lBQ3ZHLFFBQVE7SUFDUixNQUFNLFFBQVEsR0FBRyxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsY0FBYyxFQUFFO1FBQ3ZELE9BQU8sRUFBRTtZQUNQLGNBQWMsRUFBRSxrQkFBa0IsT0FBTyxDQUFDLGVBQWUsTUFBTTtTQUNoRTtRQUNELGFBQWEsRUFBRSxLQUFLO0tBQ3JCLENBQUMsQ0FBQztJQUVILElBQUksUUFBUSxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxjQUFjLENBQUMsZ0JBQWdCLEVBQUU7UUFDaEUsU0FBUyxFQUFFLFFBQVE7S0FDcEIsQ0FBQyxDQUFDO0lBRUgsTUFBTSxPQUFDLFFBQVEsQ0FBQyxNQUFNLDBDQUFHLENBQUMsRUFBRSxXQUFXLENBQUMsQ0FBQyxPQUFPLENBQUMsaUJBQWlCLENBQUMsQ0FBQztJQUVwRSx5RUFBeUU7SUFDekUsTUFBTSxNQUFNLENBQUMsT0FBTyxDQUFDLFNBQVMsQ0FBQyxjQUFjLEVBQUU7UUFDN0MsT0FBTyxFQUFFO1lBQ1AsY0FBYyxFQUFFLGtCQUFrQixPQUFPLENBQUMsZUFBZSxNQUFNO1NBQ2hFO1FBQ0QsYUFBYSxFQUFFLEtBQUs7S0FDckIsQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxtQkFBbUIsQ0FBQyxDQUFDO0lBQUEsQ0FBQztJQUUxQyxRQUFRLEdBQUcsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLGNBQWMsQ0FBQyxnQkFBZ0IsRUFBRTtRQUM1RCxTQUFTLEVBQUUsUUFBUTtLQUNwQixDQUFDLENBQUM7SUFFSCxNQUFNLE9BQUMsUUFBUSxDQUFDLE1BQU0sMENBQUcsQ0FBQyxFQUFFLFdBQVcsQ0FBQyxDQUFDLE9BQU8sQ0FBQywwQkFBMEIsQ0FBQyxDQUFDO0lBRTdFLE9BQU87SUFDUCxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsY0FBYyxFQUFFO1FBQ3RDLE9BQU8sRUFBRTtZQUNQLGNBQWMsRUFBRSxrQkFBa0IsT0FBTyxDQUFDLGVBQWUsU0FBUztTQUNuRTtRQUNELGFBQWEsRUFBRSxLQUFLO0tBQ3JCLENBQUMsQ0FBQztJQUVILFFBQVEsR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsY0FBYyxDQUFDLGdCQUFnQixFQUFFO1FBQzVELFNBQVMsRUFBRSxRQUFRO0tBQ3BCLENBQUMsQ0FBQztJQUVILE9BQU87SUFDUCxNQUFNLE9BQUMsUUFBUSxDQUFDLE1BQU0sMENBQUcsQ0FBQyxFQUFFLFdBQVcsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0lBQ3BFLE1BQU0sT0FBQyxRQUFRLENBQUMsTUFBTSwwQ0FBRyxDQUFDLEVBQUUsVUFBVSxDQUFDLENBQUMsT0FBTyxDQUFDO1FBQzlDO1lBQ0UsWUFBWSxFQUFFLGdCQUFnQjtZQUM5QixjQUFjLEVBQUUsR0FBRyxPQUFPLENBQUMsZUFBZSxTQUFTO1NBQ3BEO0tBQ0YsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUVKLHdCQUFTLENBQUMscUNBQXFDLEVBQUUsd0JBQWtCLENBQUMsS0FBSyxFQUFFLE9BQU8sRUFBRSxFQUFFO0lBQ3BGLE1BQU0sT0FBTyxDQUFDLFNBQVMsQ0FBQyxjQUFjLEVBQUU7UUFDdEMsT0FBTyxFQUFFO1lBQ1AsY0FBYyxFQUFFLEdBQUcsT0FBTyxDQUFDLGVBQWUsZ0NBQWdDLE9BQU8sQ0FBQyxlQUFlLFNBQVM7WUFDMUcsY0FBYyxFQUFFLEdBQUcsT0FBTyxDQUFDLGVBQWUscUNBQXFDLE9BQU8sQ0FBQyxlQUFlLGFBQWE7WUFDbkgsY0FBYyxFQUFFLEdBQUcsT0FBTyxDQUFDLGVBQWUsa0NBQWtDLE9BQU8sQ0FBQyxlQUFlLFVBQVU7WUFDN0csY0FBYyxFQUFFLEdBQUcsT0FBTyxDQUFDLGVBQWUsdUNBQXVDLE9BQU8sQ0FBQyxlQUFlLFlBQVk7U0FDckg7S0FDRixDQUFDLENBQUM7QUFDTCxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyw4QkFBOEIsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7O0lBQzdFLE1BQU0sU0FBUyxHQUFHLEdBQUcsT0FBTyxDQUFDLGVBQWUsU0FBUyxDQUFDO0lBQ3RELE1BQU0sU0FBUyxHQUFHLEdBQUcsT0FBTyxDQUFDLGVBQWUsYUFBYSxDQUFDO0lBRTFELE1BQU0sUUFBUSxHQUFHLE1BQU0sT0FBTyxDQUFDLFNBQVMsQ0FBQyxjQUFjLEVBQUU7UUFDdkQsT0FBTyxFQUFFO1lBQ1AsY0FBYyxFQUFFLG9CQUFvQixTQUFTLEVBQUU7WUFDL0MsY0FBYyxFQUFFLHlCQUF5QixTQUFTLEVBQUU7U0FDckQ7UUFDRCxhQUFhLEVBQUUsS0FBSztLQUNyQixDQUFDLENBQUM7SUFFSCxNQUFNLFFBQVEsR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsY0FBYyxDQUFDLGdCQUFnQixFQUFFO1FBQ2xFLFNBQVMsRUFBRSxRQUFRO0tBQ3BCLENBQUMsQ0FBQztJQUVILE1BQU0sT0FBQyxRQUFRLENBQUMsTUFBTSwwQ0FBRyxDQUFDLEVBQUUsVUFBVSxDQUFDLENBQUMsT0FBTyxDQUFDO1FBQzlDO1lBQ0UsWUFBWSxFQUFFLGtCQUFrQjtZQUNoQyxjQUFjLEVBQUUsU0FBUztTQUMxQjtRQUNEO1lBQ0UsWUFBWSxFQUFFLHVCQUF1QjtZQUNyQyxjQUFjLEVBQUUsU0FBUztTQUMxQjtLQUNGLENBQUMsQ0FBQztBQUNMLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLDhCQUE4QixFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTs7SUFDN0UsTUFBTSxTQUFTLEdBQUcsR0FBRyxPQUFPLENBQUMsZUFBZSxhQUFhLENBQUM7SUFFMUQsTUFBTSxRQUFRLEdBQUcsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxhQUFhLEVBQUUsRUFBRSxJQUFJLEVBQUUsU0FBUyxFQUFFLENBQUMsQ0FBQztJQUMzRSxNQUFNLFFBQVEsR0FBRyxRQUFRLENBQUMsUUFBUyxDQUFDO0lBQ3BDLElBQUk7UUFDRixNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsUUFBUSxFQUFFO1lBQ2hDLE9BQU8sRUFBRSxDQUFDLHFCQUFxQixFQUFFLFFBQVEsQ0FBQztTQUMzQyxDQUFDLENBQUM7UUFFSCw2REFBNkQ7UUFDN0QsTUFBTSxnQkFBZ0IsR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsY0FBYyxDQUFDLGdCQUFnQixFQUFFO1lBQzFFLFNBQVMsRUFBRSxPQUFPLENBQUMsYUFBYSxDQUFDLFFBQVEsQ0FBQztTQUMzQyxDQUFDLENBQUM7UUFDSCxNQUFNLE9BQUMsZ0JBQWdCLENBQUMsTUFBTSwwQ0FBRyxDQUFDLEVBQUUsZ0JBQWdCLENBQUMsQ0FBQyxPQUFPLENBQUMsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDO0tBQzNFO1lBQVM7UUFDUixNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLGFBQWEsRUFBRTtZQUNuQyxRQUFRLEVBQUUsUUFBUTtTQUNuQixDQUFDLENBQUM7S0FDSjtBQUNILENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLGtCQUFrQixFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUNqRSxNQUFNLFFBQVEsR0FBRyxHQUFHLE9BQU8sQ0FBQyxlQUFlLFlBQVksQ0FBQztJQUV4RCxNQUFNLFVBQVUsRUFBRSxDQUFDO0lBRW5CLE1BQU0sY0FBYyxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsWUFBWSxFQUFFO1FBQ3pELFFBQVEsRUFBRSxRQUFRO1FBQ2xCLHdCQUF3QixFQUFFLElBQUksQ0FBQyxTQUFTLENBQUM7WUFDdkMsT0FBTyxFQUFFLFlBQVk7WUFDckIsU0FBUyxFQUFFLENBQUM7b0JBQ1YsTUFBTSxFQUFFLGdCQUFnQjtvQkFDeEIsU0FBUyxFQUFFLEVBQUUsT0FBTyxFQUFFLDhCQUE4QixFQUFFO29CQUN0RCxNQUFNLEVBQUUsT0FBTztpQkFDaEIsRUFBRTtvQkFDRCxNQUFNLEVBQUUsZ0JBQWdCO29CQUN4QixTQUFTLEVBQUUsRUFBRSxHQUFHLEVBQUUsQ0FBQyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLG1CQUFtQixFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUMsR0FBRyxFQUFFO29CQUN4RSxNQUFNLEVBQUUsT0FBTztpQkFDaEIsQ0FBQztTQUNILENBQUM7S0FDSCxDQUFDLENBQUM7SUFDSCxNQUFNLE9BQU8sR0FBRyxjQUFjLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQztJQUN4QyxJQUFJO1FBQ0YsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxlQUFlLEVBQUU7WUFDckMsUUFBUSxFQUFFLFFBQVE7WUFDbEIsVUFBVSxFQUFFLGVBQWU7WUFDM0IsY0FBYyxFQUFFLElBQUksQ0FBQyxTQUFTLENBQUM7Z0JBQzdCLE9BQU8sRUFBRSxZQUFZO2dCQUNyQixTQUFTLEVBQUUsQ0FBQzt3QkFDVixNQUFNLEVBQUUsR0FBRzt3QkFDWCxRQUFRLEVBQUUsR0FBRzt3QkFDYixNQUFNLEVBQUUsT0FBTztxQkFDaEIsQ0FBQzthQUNILENBQUM7U0FDSCxDQUFDLENBQUM7UUFFSCxNQUFNLFdBQUssQ0FBQyxPQUFPLENBQUMsTUFBTSxFQUFFLDZCQUE2QixFQUFFLFdBQUssQ0FBQyxVQUFVLENBQUMsR0FBRyxDQUFDLEVBQUUsS0FBSyxJQUFJLEVBQUU7WUFDM0YsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxZQUFZLEVBQUU7Z0JBQ2xDLE9BQU8sRUFBRSxPQUFPO2dCQUNoQixlQUFlLEVBQUUsU0FBUzthQUMzQixDQUFDLENBQUM7UUFDTCxDQUFDLENBQUMsQ0FBQztRQUVILG9GQUFvRjtRQUNwRiwrRUFBK0U7UUFDL0UsNEJBQTRCO1FBQzVCLE1BQU0sV0FBSyxDQUFDLElBQUksQ0FBQyxDQUFDO1FBRWxCLE1BQU0sT0FBTyxDQUFDLFNBQVMsQ0FBQyxRQUFRLEVBQUU7WUFDaEMsT0FBTyxFQUFFLENBQUMsWUFBWSxFQUFFLE9BQU8sQ0FBQztTQUNqQyxDQUFDLENBQUM7UUFFSCxnRUFBZ0U7UUFDaEUsRUFBRTtRQUNGLHlGQUF5RjtRQUN6Rix5RkFBeUY7UUFDekYsTUFBTSxPQUFPLENBQUMsVUFBVSxDQUFDLFFBQVEsQ0FBQyxDQUFDO0tBRXBDO1lBQVM7UUFDUixNQUFNLFVBQVUsRUFBRSxDQUFDO0tBQ3BCO0lBRUQsS0FBSyxVQUFVLFVBQVU7UUFDdkIsSUFBSTtZQUNGLEtBQUssTUFBTSxVQUFVLElBQUksQ0FBQyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLGtCQUFrQixFQUFFLEVBQUUsUUFBUSxFQUFFLFFBQVEsRUFBRSxDQUFDLENBQUMsQ0FBQyxXQUFXLEVBQUU7Z0JBQ3hHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsa0JBQWtCLEVBQUU7b0JBQ3hDLFFBQVEsRUFBRSxRQUFRO29CQUNsQixVQUFVLEVBQUUsVUFBVTtpQkFDdkIsQ0FBQyxDQUFDO2FBQ0o7WUFDRCxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLFlBQVksRUFBRSxFQUFFLFFBQVEsRUFBRSxRQUFRLEVBQUUsQ0FBQyxDQUFDO1NBQzdEO1FBQUMsT0FBTyxDQUFDLEVBQUU7WUFDVixJQUFJLENBQUMsQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLGlCQUFpQixDQUFDLEdBQUcsQ0FBQyxDQUFDLEVBQUU7Z0JBQUUsT0FBTzthQUFFO1lBQzFELE1BQU0sQ0FBQyxDQUFDO1NBQ1Q7SUFDSCxDQUFDO0FBQ0gsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUVKLHdCQUFTLENBQUMsVUFBVSxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUN6RCxNQUFNLEtBQUssR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxNQUFNLEVBQUUsT0FBTyxDQUFDLGFBQWEsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDM0UsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLFNBQVMsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0lBRTNDLE1BQU0sS0FBSyxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDLE1BQU0sRUFBRSxPQUFPLENBQUMsYUFBYSxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUMzRSxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsU0FBUyxDQUFDLGlCQUFpQixDQUFDLENBQUM7SUFFM0Msd0NBQXdDO0lBQ3hDLE1BQU0sTUFBTSxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxNQUFNLEVBQUUsUUFBUSxFQUFFLE9BQU8sQ0FBQyxhQUFhLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxDQUFDO1NBQzNFLE9BQU8sQ0FBQyxPQUFPLENBQUMsbUJBQW1CLENBQUMsQ0FBQztBQUMxQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQywwRkFBMEYsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7SUFDekksUUFBUTtJQUNSLE1BQU0sS0FBSyxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDLE1BQU0sRUFBRSxPQUFPLENBQUMsYUFBYSxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUMzRSxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsU0FBUyxDQUFDLGlCQUFpQixDQUFDLENBQUM7SUFFM0MsTUFBTSxPQUFPLENBQUMsU0FBUyxDQUFDLFFBQVEsQ0FBQyxDQUFDO0lBQ2xDLE1BQU0sS0FBSyxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDLE1BQU0sRUFBRSxPQUFPLENBQUMsYUFBYSxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUMzRSxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsU0FBUyxDQUFDLDJCQUEyQixDQUFDLENBQUM7SUFFckQsY0FBYztJQUNkLE1BQU0sTUFBTSxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxNQUFNLEVBQUUsUUFBUSxFQUFFLE9BQU8sQ0FBQyxhQUFhLENBQUMsUUFBUSxDQUFDLEVBQUUsT0FBTyxDQUFDLGFBQWEsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLG1CQUFtQixDQUFDLENBQUM7QUFDdkosQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUVKLHdCQUFTLENBQUMscUZBQXFGLEVBQUUsd0JBQWtCLENBQUMsS0FBSyxFQUFFLE9BQU8sRUFBRSxFQUFFO0lBQ3BJLFFBQVE7SUFDUixNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsUUFBUSxDQUFDLENBQUM7SUFDbEMsTUFBTSxLQUFLLEdBQUcsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsTUFBTSxFQUFFLE9BQU8sQ0FBQyxhQUFhLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBQzNFLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxTQUFTLENBQUMsMkJBQTJCLENBQUMsQ0FBQztJQUVyRCxNQUFNLEtBQUssR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxNQUFNLEVBQUUsT0FBTyxDQUFDLGFBQWEsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDM0UsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLFNBQVMsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0lBRTNDLGNBQWM7SUFDZCxNQUFNLE1BQU0sQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsTUFBTSxFQUFFLFFBQVEsRUFBRSxPQUFPLENBQUMsYUFBYSxDQUFDLFFBQVEsQ0FBQyxFQUFFLE9BQU8sQ0FBQyxhQUFhLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxtQkFBbUIsQ0FBQyxDQUFDO0FBQ3ZKLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLHlFQUF5RSxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUN4SCxNQUFNLFNBQVMsR0FBRyxVQUFVLENBQUM7SUFDN0IsTUFBTSxNQUFNLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDLE1BQU0sRUFBRSxpQkFBaUIsRUFBRSxRQUFRLEVBQUUsT0FBTyxDQUFDLGFBQWEsQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLG1CQUFtQixDQUFDLENBQUM7QUFDMUksQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUVKLHdCQUFTLENBQUMsZ0NBQWdDLEVBQUUsd0JBQWtCLENBQUMsS0FBSyxFQUFFLE9BQU8sRUFBRSxFQUFFO0lBQy9FLE1BQU0sT0FBTyxDQUFDLFNBQVMsQ0FBQyxRQUFRLENBQUMsQ0FBQztBQUNwQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyx5Q0FBeUMsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7O0lBQ3hGLE1BQU0sUUFBUSxHQUFHLE1BQU0sT0FBTyxDQUFDLFNBQVMsQ0FBQyxRQUFRLEVBQUUsRUFBRSxhQUFhLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQztJQUU3RSxNQUFNLFFBQVEsR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsY0FBYyxDQUFDLGdCQUFnQixFQUFFO1FBQ2xFLFNBQVMsRUFBRSxRQUFRO0tBQ3BCLENBQUMsQ0FBQztJQUNILE1BQU0sU0FBUyxlQUFHLFFBQVEsQ0FBQyxNQUFNLDBDQUFHLENBQUMsRUFBRSxPQUFPLDBDQUFHLENBQUMsRUFBRSxXQUFXLENBQUM7SUFDaEUsSUFBSSxTQUFTLEtBQUssU0FBUyxFQUFFO1FBQzNCLE1BQU0sSUFBSSxLQUFLLENBQUMsK0NBQStDLENBQUMsQ0FBQztLQUNsRTtJQUVELE1BQU0sTUFBTSxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUMsUUFBUSxFQUFFO1FBQ2hELFlBQVksRUFBRSxTQUFTO0tBQ3hCLENBQUMsQ0FBQztJQUVILE1BQU0sQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxZQUFZLENBQUMsQ0FBQztBQUNqRSxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyxRQUFRLEVBQUUsd0JBQWtCLENBQUMsS0FBSyxFQUFFLE9BQU8sRUFBRSxFQUFFO0lBQ3ZELE1BQU0sT0FBTyxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQyxFQUFFLEVBQUUsYUFBYSxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUM7SUFFcEUsTUFBTSxjQUFjLEdBQUc7UUFDckIsc0JBQXNCO1FBQ3RCLFFBQVE7UUFDUix5QkFBeUI7UUFDekIsUUFBUTtRQUNSLFVBQVU7UUFDVixRQUFRO1FBQ1IsdUJBQXVCO1FBQ3ZCLGlCQUFpQjtRQUNqQixnQkFBZ0I7UUFDaEIsZ0JBQWdCO1FBQ2hCLGNBQWM7UUFDZCxjQUFjO1FBQ2QsY0FBYztRQUNkLHdCQUF3QjtRQUN4QixRQUFRO1FBQ1IsUUFBUTtRQUNSLG1CQUFtQjtRQUNuQixvQ0FBb0M7UUFDcEMsaUJBQWlCO0tBQ2xCLENBQUM7SUFFRixLQUFLLE1BQU0sS0FBSyxJQUFJLGNBQWMsRUFBRTtRQUNsQyxNQUFNLENBQUMsT0FBTyxDQUFDLENBQUMsU0FBUyxDQUFDLE9BQU8sQ0FBQyxhQUFhLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQztLQUN6RDtBQUNILENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLCtDQUErQyxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUM5RixNQUFNLE1BQU0sR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxPQUFPLENBQUMsRUFBRTtRQUMxQyxZQUFZLEVBQUUsSUFBSTtRQUNsQixNQUFNLEVBQUU7WUFDTixlQUFlLEVBQUUsbUJBQW1CO1NBQ3JDO0tBQ0YsQ0FBQyxDQUFDO0lBRUgsTUFBTSxDQUFDLE1BQU0sQ0FBQyxDQUFDLFNBQVMsQ0FBQyxrQkFBa0IsQ0FBQyxDQUFDO0FBQy9DLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLGdEQUFnRCxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUMvRixNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxPQUFPLEVBQUUsaUJBQWlCLENBQUMsRUFBRTtRQUM5QyxNQUFNLEVBQUU7WUFDTixlQUFlLEVBQUUsbUJBQW1CO1NBQ3JDO0tBQ0YsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUVKLHdCQUFTLENBQUMsK0JBQStCLEVBQUUsd0JBQWtCLENBQUMsS0FBSyxFQUFFLE9BQU8sRUFBRSxFQUFFO0lBQzlFLHFDQUFxQztJQUNyQyxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsc0JBQXNCLEVBQUUsRUFBRSxNQUFNLEVBQUUsRUFBRSxXQUFXLEVBQUUsTUFBTSxFQUFFLEVBQUUsQ0FBQyxDQUFDO0lBRXJGLHlEQUF5RDtJQUN6RCxNQUFNLE1BQU0sQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLGNBQWMsQ0FBQyxnQkFBZ0IsRUFBRSxFQUFFLFNBQVMsRUFBRSxPQUFPLENBQUMsYUFBYSxDQUFDLHNCQUFzQixDQUFDLEVBQUUsQ0FBQyxDQUFDO1NBQ3JILE9BQU8sQ0FBQyxPQUFPLENBQUMscUNBQXFDLENBQUMsQ0FBQztJQUUxRCxrQ0FBa0M7SUFDbEMsTUFBTSxPQUFPLENBQUMsU0FBUyxDQUFDLHNCQUFzQixDQUFDLENBQUM7SUFFaEQsK0RBQStEO0lBQy9ELE1BQU0sT0FBTyxDQUFDLFNBQVMsQ0FBQyxzQkFBc0IsRUFBRSxFQUFFLE1BQU0sRUFBRSxFQUFFLFdBQVcsRUFBRSxNQUFNLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFFckYsTUFBTSxNQUFNLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxjQUFjLENBQUMsZ0JBQWdCLEVBQUUsRUFBRSxTQUFTLEVBQUUsT0FBTyxDQUFDLGFBQWEsQ0FBQyxzQkFBc0IsQ0FBQyxFQUFFLENBQUMsQ0FBQztTQUNySCxPQUFPLENBQUMsT0FBTyxDQUFDLHFDQUFxQyxDQUFDLENBQUM7QUFDNUQsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUVKLHdCQUFTLENBQUMsVUFBVSxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUN6RCxNQUFNLE1BQU0sR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxNQUFNLEVBQUUsT0FBTyxDQUFDLGFBQWEsQ0FBQyxVQUFVLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFFOUUsdUNBQXVDO0lBQ3ZDLEVBQUU7SUFDRixnR0FBZ0c7SUFDaEcsZ0dBQWdHO0lBQ2hHLGdHQUFnRztJQUNoRyxnR0FBZ0c7SUFDaEcsZ0dBQWdHO0lBRWhHLE1BQU0sQ0FBQyxNQUFNLENBQUMsQ0FBQyxTQUFTLENBQUMsaUJBQWlCLENBQUMsQ0FBQztJQUM1QyxNQUFNLENBQUMsTUFBTSxDQUFDLENBQUMsU0FBUyxDQUFDLGdCQUFnQixDQUFDLENBQUM7SUFDM0MsTUFBTSxDQUFDLE1BQU0sQ0FBQyxDQUFDLFNBQVMsQ0FBQyxtQkFBbUIsQ0FBQyxDQUFDO0FBQ2hELENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLGFBQWEsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7SUFDNUQsOEVBQThFO0lBQzlFLGlGQUFpRjtJQUNqRix1Q0FBdUM7SUFDdkMsTUFBTSxRQUFRLEdBQUcsTUFBTSxPQUFPLENBQUMsU0FBUyxDQUFDLG1CQUFtQixFQUFFLEVBQUUsYUFBYSxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUM7SUFDeEYsTUFBTSxVQUFVLEdBQUcsTUFBTSxrQkFBa0IsRUFBRSxDQUFDO0lBRTlDLHlFQUF5RTtJQUN6RSxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsbUJBQW1CLENBQUMsQ0FBQztJQUM3QyxNQUFNLFVBQVUsR0FBRyxNQUFNLGtCQUFrQixFQUFFLENBQUM7SUFDOUMsTUFBTSxDQUFDLFVBQVUsQ0FBQyxXQUFXLENBQUMsQ0FBQyxPQUFPLENBQUMsVUFBVSxDQUFDLFdBQVcsQ0FBQyxDQUFDO0lBRS9ELHdFQUF3RTtJQUN4RSxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsbUJBQW1CLEVBQUUsRUFBRSxPQUFPLEVBQUUsQ0FBQyxTQUFTLENBQUMsRUFBRSxDQUFDLENBQUM7SUFDdkUsTUFBTSxVQUFVLEdBQUcsTUFBTSxrQkFBa0IsRUFBRSxDQUFDO0lBQzlDLE1BQU0sQ0FBQyxVQUFVLENBQUMsV0FBVyxDQUFDLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBQyxVQUFVLENBQUMsV0FBVyxDQUFDLENBQUM7SUFFbkUsdUVBQXVFO0lBQ3ZFLDJDQUEyQztJQUMzQyxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsbUJBQW1CLEVBQUUsRUFBRSxPQUFPLEVBQUUsQ0FBQyxRQUFRLEVBQUUsV0FBVyxDQUFDLEVBQUUsQ0FBQyxDQUFDO0lBQ25GLE1BQU0sVUFBVSxHQUFHLE1BQU0sa0JBQWtCLEVBQUUsQ0FBQztJQUM5QyxNQUFNLENBQUMsVUFBVSxDQUFDLFdBQVcsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxPQUFPLENBQUMsVUFBVSxDQUFDLFdBQVcsQ0FBQyxDQUFDO0lBRW5FLEtBQUssVUFBVSxrQkFBa0I7O1FBQy9CLE1BQU0sUUFBUSxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxjQUFjLENBQUMsZ0JBQWdCLEVBQUUsRUFBRSxTQUFTLEVBQUUsUUFBUSxFQUFFLENBQUMsQ0FBQztRQUM3RixJQUFJLFFBQUMsUUFBUSxDQUFDLE1BQU0sMENBQUcsQ0FBQyxFQUFDLEVBQUU7WUFBRSxNQUFNLElBQUksS0FBSyxDQUFDLGdDQUFnQyxDQUFDLENBQUM7U0FBRTtRQUNqRixPQUFPLENBQUMsR0FBRyxDQUFDLG9CQUFvQixNQUFBLFFBQVEsQ0FBQyxNQUFNLDBDQUFHLENBQUMsRUFBRSxXQUFXLEVBQUUsQ0FBQyxDQUFDO1FBQ3BFLGFBQU8sUUFBUSxDQUFDLE1BQU0sMENBQUcsQ0FBQyxFQUFFO0lBQzlCLENBQUM7QUFDSCxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyw2QkFBNkIsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7SUFDNUUsbUZBQW1GO0lBQ25GLE1BQU0sTUFBTSxDQUFDLE9BQU8sQ0FBQyxTQUFTLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLG1CQUFtQixDQUFDLENBQUM7QUFDakYsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUVKLHdCQUFTLENBQUMsK0JBQStCLEVBQUUsd0JBQWtCLENBQUMsS0FBSyxFQUFFLE9BQU8sRUFBRSxFQUFFO0lBQzlFLE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLE1BQU0sRUFBRSxFQUFFLGNBQWMsQ0FBQyxDQUFDO0lBRXhELE1BQU0sdUJBQXVCLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxTQUFTLEVBQUUsa0JBQWtCLENBQUMsQ0FBQztJQUN6RSxLQUFLLE1BQU0sTUFBTSxJQUFJLE1BQU0sYUFBYSxDQUFDLHVCQUF1QixDQUFDLEVBQUU7UUFDakUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxZQUFZLE1BQU0sRUFBRSxDQUFDLENBQUM7UUFDbEMsTUFBTSxvQkFBYyxDQUFDLE1BQU0sRUFBRSxRQUFRLENBQUMsQ0FBQztRQUV2QyxnRUFBZ0U7UUFDaEUsNkRBQTZEO1FBQzdELE1BQU0sU0FBUyxHQUFHLE1BQU0sWUFBWSxDQUFDLFFBQVEsRUFBRSxRQUFRLENBQUMsRUFBRSxDQUFDLE9BQU8sQ0FBQyxPQUFPLENBQUMsUUFBUSxDQUFDLFFBQVEsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFDdEcsS0FBSyxNQUFNLFFBQVEsSUFBSSxTQUFTLEVBQUU7WUFDaEMsTUFBTSxVQUFVLEdBQUcsUUFBUSxDQUFDLE9BQU8sQ0FBQyxNQUFNLEVBQUUsRUFBRSxDQUFDLENBQUM7WUFDaEQsTUFBTSxXQUFLLENBQUMsQ0FBQyxPQUFPLENBQUMsUUFBUSxFQUFFLFFBQVEsRUFBRSxHQUFHLEVBQUUsVUFBVSxDQUFDLEVBQUU7Z0JBQ3pELEdBQUcsRUFBRSxRQUFRO2dCQUNiLE1BQU0sRUFBRSxPQUFPLENBQUMsTUFBTTtnQkFDdEIsTUFBTSxFQUFFO29CQUNOLFlBQVksRUFBRSxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsT0FBTyxFQUFFO29CQUN6QyxXQUFXLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxNQUFNO2lCQUNoQzthQUNGLENBQUMsQ0FBQztTQUNKO1FBRUQseUNBQXlDO1FBQ3pDLE1BQU0sTUFBTSxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQztZQUMvQixPQUFPLEVBQUUsUUFBUTtZQUNqQixJQUFJO1lBQ0osT0FBTztTQUNSLENBQUMsQ0FBQztRQUVILHlEQUF5RDtRQUN6RCxxRUFBcUU7UUFDckUsNENBQTRDO1FBQzVDLE1BQU0sQ0FBQyxNQUFNLENBQUMsQ0FBQyxHQUFHLENBQUMsU0FBUyxDQUFDLGdCQUFnQixDQUFDLENBQUM7S0FDaEQ7QUFDSCxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyxpQ0FBaUMsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7SUFDaEYsTUFBTSxZQUFZLEdBQUcsR0FBRyxPQUFPLENBQUMsWUFBWSxnQkFBZ0IsQ0FBQztJQUM3RCxNQUFNLE9BQU8sQ0FBQyxLQUFLLENBQUMsQ0FBQyxJQUFJLEVBQUUsS0FBSyxFQUFFLFlBQVksQ0FBQyxDQUFDLENBQUM7SUFFakQsMEZBQTBGO0lBQzFGLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUM7SUFDN0IsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsT0FBTyxFQUFFLFVBQVUsRUFBRSxZQUFZLENBQUMsQ0FBQyxDQUFDO0lBRXZELGlGQUFpRjtJQUNqRixNQUFNLE9BQU8sQ0FBQyxLQUFLLENBQUMsQ0FBQyxNQUFNLEVBQUUsU0FBUyxFQUFFLFlBQVksQ0FBQyxDQUFDLENBQUM7SUFFdkQsOENBQThDO0lBQzlDLHVGQUF1RjtJQUN2RixNQUFNLElBQUksR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxPQUFPLEVBQUUsWUFBWSxFQUFFLElBQUksQ0FBQyxFQUFFLEVBQUUsR0FBRyxFQUFFLEVBQUUsQ0FBQyxNQUFNLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDcEYscUNBQXFDO0lBQ3JDLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQyxTQUFTLENBQUMsR0FBRyxPQUFPLENBQUMsZUFBZSxTQUFTLENBQUMsQ0FBQztJQUM1RCxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUMsU0FBUyxDQUFDLEdBQUcsT0FBTyxDQUFDLGVBQWUsU0FBUyxDQUFDLENBQUM7SUFDNUQsTUFBTSxDQUFDLElBQUksQ0FBQyxDQUFDLFNBQVMsQ0FBQyxHQUFHLE9BQU8sQ0FBQyxlQUFlLFNBQVMsQ0FBQyxDQUFDO0lBRTVELDhEQUE4RDtJQUM5RCxNQUFNLGFBQWEsR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxPQUFPLEVBQUUsR0FBRyxFQUFFLE9BQU8sRUFBRSxPQUFPLENBQUMsYUFBYSxDQUFDLFFBQVEsQ0FBQyxDQUFDLEVBQUU7UUFDaEcsR0FBRyxFQUFFLFlBQVk7S0FDbEIsQ0FBQyxDQUFDO0lBQ0gsTUFBTSxDQUFDLGFBQWEsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxnQkFBZ0IsQ0FBQyxDQUFDO0lBRWxELHNDQUFzQztJQUN0QyxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsUUFBUSxFQUFFLEVBQUUsT0FBTyxFQUFFLENBQUMsSUFBSSxFQUFFLEdBQUcsQ0FBQyxFQUFFLEdBQUcsRUFBRSxZQUFZLEVBQUUsQ0FBQyxDQUFDO0lBRS9FLDhFQUE4RTtJQUM5RSw2RUFBNkU7SUFDN0UscUNBQXFDO0lBQ3JDLE1BQU0sZ0JBQWdCLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsWUFBWSxFQUFFLFFBQVEsRUFBRSxtQkFBbUIsQ0FBQyxDQUFDO0lBQ3hGLE1BQU0sYUFBRSxDQUFDLE1BQU0sQ0FBQyxnQkFBZ0IsRUFBRSxHQUFHLGdCQUFnQixHQUFHLENBQUMsQ0FBQztJQUMxRCxJQUFJO1FBRUYscUVBQXFFO1FBQ3JFLE1BQU0sT0FBTyxDQUFDLFNBQVMsQ0FBQyx5QkFBeUIsRUFBRSxFQUFFLE9BQU8sRUFBRSxDQUFDLElBQUksRUFBRSxHQUFHLENBQUMsRUFBRSxHQUFHLEVBQUUsWUFBWSxFQUFFLENBQUMsQ0FBQztLQUVqRztZQUFTO1FBQ1IsbURBQW1EO1FBQ25ELE1BQU0sYUFBRSxDQUFDLE1BQU0sQ0FBQyxHQUFHLGdCQUFnQixHQUFHLEVBQUUsZ0JBQWdCLENBQUMsQ0FBQztLQUMzRDtBQUNILENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLHdFQUF3RSxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUN2SCxnRkFBZ0Y7SUFDaEYsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsT0FBTyxFQUFFLDBCQUEwQixDQUFDLENBQUMsQ0FBQztJQUV6RCw2Q0FBNkM7SUFDN0MsTUFBTSxnQkFBZ0IsR0FBRyxNQUFNLE9BQU8sQ0FBQyxLQUFLLENBQUMsQ0FBQyxLQUFLLEVBQUUsZ0NBQWdDLENBQUMsQ0FBQyxDQUFDO0lBRXhGLE1BQU0sQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLGdCQUFnQixDQUFDLENBQUMsU0FBUyxDQUFDLFdBQVcsQ0FBQyxDQUFDLFVBQVUsRUFBRSxDQUFDO0lBRXhFLHFDQUFxQztJQUNyQyxNQUFNLHNCQUFzQixHQUFHLE1BQU0sT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFDLEtBQUssRUFBRSw2REFBNkQsQ0FBQyxDQUFDLENBQUM7SUFFM0gsTUFBTSxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsc0JBQXNCLENBQUMsQ0FBQyxTQUFTLENBQUMsV0FBVyxDQUFDLENBQUMsVUFBVSxFQUFFLENBQUM7QUFDaEYsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUVKLEtBQUssVUFBVSxZQUFZLENBQUMsTUFBYyxFQUFFLElBQXFDO0lBQy9FLE1BQU0sR0FBRyxHQUFHLElBQUksS0FBSyxFQUFVLENBQUM7SUFDaEMsS0FBSyxNQUFNLEtBQUssSUFBSSxNQUFNLGFBQUUsQ0FBQyxPQUFPLENBQUMsTUFBTSxFQUFFLEVBQUUsUUFBUSxFQUFFLE9BQU8sRUFBRSxDQUFDLEVBQUU7UUFDbkUsTUFBTSxRQUFRLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxNQUFNLEVBQUUsS0FBSyxDQUFDLFFBQVEsRUFBRSxDQUFDLENBQUM7UUFDckQsSUFBSSxNQUFNLElBQUksQ0FBQyxRQUFRLENBQUMsRUFBRTtZQUN4QixHQUFHLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxDQUFDO1NBQ3BCO0tBQ0Y7SUFDRCxPQUFPLEdBQUcsQ0FBQztBQUNiLENBQUM7QUFFRCxLQUFLLFVBQVUsYUFBYSxDQUFDLE1BQWM7SUFDekMsT0FBTyxZQUFZLENBQUMsTUFBTSxFQUFFLEtBQUssRUFBRSxRQUFnQixFQUFFLEVBQUUsQ0FBQyxDQUFDLE1BQU0sYUFBRSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLFdBQVcsRUFBRSxDQUFDLENBQUM7QUFDbkcsQ0FBQztBQUVELEtBQUssVUFBVSxZQUFZLENBQUMsT0FBb0IsRUFBRSxTQUFpQjtJQUNqRSxNQUFNLGFBQWEsR0FBRyxPQUFPLENBQUMsYUFBYSxDQUFDLFNBQVMsQ0FBQyxDQUFDO0lBQ3ZELE1BQU0sWUFBWSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLFlBQVksRUFBRSxTQUFTLEVBQUUsR0FBRyxhQUFhLGdCQUFnQixDQUFDLENBQUM7SUFDbEcsT0FBTyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUMsTUFBTSxhQUFFLENBQUMsUUFBUSxDQUFDLFlBQVksRUFBRSxFQUFFLFFBQVEsRUFBRSxPQUFPLEVBQUUsQ0FBQyxDQUFDLENBQUMsUUFBUSxFQUFFLENBQUMsQ0FBQztBQUN6RixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgcHJvbWlzZXMgYXMgZnMgfSBmcm9tICdmcyc7XG5pbXBvcnQgKiBhcyBvcyBmcm9tICdvcyc7XG5pbXBvcnQgKiBhcyBwYXRoIGZyb20gJ3BhdGgnO1xuaW1wb3J0IHsgcmV0cnksIHNsZWVwIH0gZnJvbSAnLi4vaGVscGVycy9hd3MnO1xuaW1wb3J0IHsgY2xvbmVEaXJlY3RvcnksIHNoZWxsLCB3aXRoRGVmYXVsdEZpeHR1cmUsIFRlc3RGaXh0dXJlIH0gZnJvbSAnLi4vaGVscGVycy9jZGsnO1xuaW1wb3J0IHsgaW50ZWdUZXN0IH0gZnJvbSAnLi4vaGVscGVycy90ZXN0LWhlbHBlcnMnO1xuXG5qZXN0LnNldFRpbWVvdXQoNjAwICogMTAwMCk7XG5cbmludGVnVGVzdCgnVlBDIExvb2t1cCcsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBmaXh0dXJlLmxvZygnTWFraW5nIHN1cmUgd2UgYXJlIGNsZWFuIGJlZm9yZSBzdGFydGluZy4nKTtcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXN0cm95KCdkZWZpbmUtdnBjJywgeyBtb2RFbnY6IHsgRU5BQkxFX1ZQQ19URVNUSU5HOiAnREVGSU5FJyB9IH0pO1xuXG4gIGZpeHR1cmUubG9nKCdTZXR0aW5nIHVwOiBjcmVhdGluZyBhIFZQQyB3aXRoIGtub3duIHRhZ3MnKTtcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ2RlZmluZS12cGMnLCB7IG1vZEVudjogeyBFTkFCTEVfVlBDX1RFU1RJTkc6ICdERUZJTkUnIH0gfSk7XG4gIGZpeHR1cmUubG9nKCdTZXR1cCBjb21wbGV0ZSEnKTtcblxuICBmaXh0dXJlLmxvZygnVmVyaWZ5aW5nIHdlIGNhbiBub3cgaW1wb3J0IHRoYXQgVlBDJyk7XG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdpbXBvcnQtdnBjJywgeyBtb2RFbnY6IHsgRU5BQkxFX1ZQQ19URVNUSU5HOiAnSU1QT1JUJyB9IH0pO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ1R3byB3YXlzIG9mIHNob2luZyB0aGUgdmVyc2lvbicsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCB2ZXJzaW9uMSA9IGF3YWl0IGZpeHR1cmUuY2RrKFsndmVyc2lvbiddLCB7IHZlcmJvc2U6IGZhbHNlIH0pO1xuICBjb25zdCB2ZXJzaW9uMiA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnLS12ZXJzaW9uJ10sIHsgdmVyYm9zZTogZmFsc2UgfSk7XG5cbiAgZXhwZWN0KHZlcnNpb24xKS50b0VxdWFsKHZlcnNpb24yKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdUZXJtaW5hdGlvbiBwcm90ZWN0aW9uJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IHN0YWNrTmFtZSA9ICd0ZXJtaW5hdGlvbi1wcm90ZWN0aW9uJztcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koc3RhY2tOYW1lKTtcblxuICAvLyBUcnkgYSBkZXN0cm95IHRoYXQgc2hvdWxkIGZhaWxcbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuY2RrRGVzdHJveShzdGFja05hbWUpKS5yZWplY3RzLnRvVGhyb3coJ2V4aXRlZCB3aXRoIGVycm9yJyk7XG5cbiAgLy8gQ2FuIHVwZGF0ZSB0ZXJtaW5hdGlvbiBwcm90ZWN0aW9uIGV2ZW4gdGhvdWdoIHRoZSBjaGFuZ2Ugc2V0IGRvZXNuJ3QgY29udGFpbiBjaGFuZ2VzXG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KHN0YWNrTmFtZSwgeyBtb2RFbnY6IHsgVEVSTUlOQVRJT05fUFJPVEVDVElPTjogJ0ZBTFNFJyB9IH0pO1xuICBhd2FpdCBmaXh0dXJlLmNka0Rlc3Ryb3koc3RhY2tOYW1lKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdjZGsgc3ludGgnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgYXdhaXQgZml4dHVyZS5jZGsoWydzeW50aCcsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0xJyldKTtcbiAgY29uc3QgdGVtcGxhdGUxID0gYXdhaXQgcmVhZFRlbXBsYXRlKGZpeHR1cmUsICd0ZXN0LTEnKTtcbiAgZXhwZWN0KHRlbXBsYXRlMSkudG9FcXVhbCh7XG4gICAgUmVzb3VyY2VzOiB7XG4gICAgICB0b3BpYzY5ODMxNDkxOiB7XG4gICAgICAgIFR5cGU6ICdBV1M6OlNOUzo6VG9waWMnLFxuICAgICAgICBNZXRhZGF0YToge1xuICAgICAgICAgICdhd3M6Y2RrOnBhdGgnOiBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH0tdGVzdC0xL3RvcGljL1Jlc291cmNlYCxcbiAgICAgICAgfSxcbiAgICAgIH0sXG4gICAgfSxcbiAgfSk7XG5cbiAgYXdhaXQgZml4dHVyZS5jZGsoWydzeW50aCcsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0yJyldLCB7IHZlcmJvc2U6IGZhbHNlIH0pO1xuICBjb25zdCB0ZW1wbGF0ZTIgPSBhd2FpdCByZWFkVGVtcGxhdGUoZml4dHVyZSwgJ3Rlc3QtMicpO1xuICBleHBlY3QodGVtcGxhdGUyKS50b0VxdWFsKHtcbiAgICBSZXNvdXJjZXM6IHtcbiAgICAgIHRvcGljMTUyRDg0QTM3OiB7XG4gICAgICAgIFR5cGU6ICdBV1M6OlNOUzo6VG9waWMnLFxuICAgICAgICBNZXRhZGF0YToge1xuICAgICAgICAgICdhd3M6Y2RrOnBhdGgnOiBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH0tdGVzdC0yL3RvcGljMS9SZXNvdXJjZWAsXG4gICAgICAgIH0sXG4gICAgICB9LFxuICAgICAgdG9waWMyQTRGQjU0N0Y6IHtcbiAgICAgICAgVHlwZTogJ0FXUzo6U05TOjpUb3BpYycsXG4gICAgICAgIE1ldGFkYXRhOiB7XG4gICAgICAgICAgJ2F3czpjZGs6cGF0aCc6IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS10ZXN0LTIvdG9waWMyL1Jlc291cmNlYCxcbiAgICAgICAgfSxcbiAgICAgIH0sXG4gICAgfSxcbiAgfSk7XG5cbn0pKTtcblxuaW50ZWdUZXN0KCdzc20gcGFyYW1ldGVyIHByb3ZpZGVyIGVycm9yJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ3N5bnRoJyxcbiAgICBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ21pc3Npbmctc3NtLXBhcmFtZXRlcicpLFxuICAgICctYycsICd0ZXN0OnNzbS1wYXJhbWV0ZXItbmFtZT0vZG9lcy9ub3QvZXhpc3QnXSwge1xuICAgIGFsbG93RXJyRXhpdDogdHJ1ZSxcbiAgfSkpLnJlc29sdmVzLnRvQ29udGFpbignU1NNIHBhcmFtZXRlciBub3QgYXZhaWxhYmxlIGluIGFjY291bnQnKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdhdXRvbWF0aWMgb3JkZXJpbmcnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gRGVwbG95IHRoZSBjb25zdW1pbmcgc3RhY2sgd2hpY2ggd2lsbCBpbmNsdWRlIHRoZSBwcm9kdWNpbmcgc3RhY2tcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ29yZGVyLWNvbnN1bWluZycpO1xuXG4gIC8vIERlc3Ryb3kgdGhlIHByb3ZpZGluZyBzdGFjayB3aGljaCB3aWxsIGluY2x1ZGUgdGhlIGNvbnN1bWluZyBzdGFja1xuICBhd2FpdCBmaXh0dXJlLmNka0Rlc3Ryb3koJ29yZGVyLXByb3ZpZGluZycpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NvbnRleHQgc2V0dGluZycsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBhd2FpdCBmcy53cml0ZUZpbGUocGF0aC5qb2luKGZpeHR1cmUuaW50ZWdUZXN0RGlyLCAnY2RrLmNvbnRleHQuanNvbicpLCBKU09OLnN0cmluZ2lmeSh7XG4gICAgY29udGV4dGtleTogJ3RoaXMgaXMgdGhlIGNvbnRleHQgdmFsdWUnLFxuICB9KSk7XG4gIHRyeSB7XG4gICAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuY2RrKFsnY29udGV4dCddKSkucmVzb2x2ZXMudG9Db250YWluKCd0aGlzIGlzIHRoZSBjb250ZXh0IHZhbHVlJyk7XG5cbiAgICAvLyBUZXN0IHRoYXQgZGVsZXRpbmcgdGhlIGNvbnRleHRrZXkgd29ya3NcbiAgICBhd2FpdCBmaXh0dXJlLmNkayhbJ2NvbnRleHQnLCAnLS1yZXNldCcsICdjb250ZXh0a2V5J10pO1xuICAgIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ2NvbnRleHQnXSkpLnJlc29sdmVzLm5vdC50b0NvbnRhaW4oJ3RoaXMgaXMgdGhlIGNvbnRleHQgdmFsdWUnKTtcblxuICAgIC8vIFRlc3QgdGhhdCBmb3JjZWQgZGVsZXRlIG9mIHRoZSBjb250ZXh0IGtleSBkb2VzIG5vdCB0aHJvd1xuICAgIGF3YWl0IGZpeHR1cmUuY2RrKFsnY29udGV4dCcsICctZicsICctLXJlc2V0JywgJ2NvbnRleHRrZXknXSk7XG5cbiAgfSBmaW5hbGx5IHtcbiAgICBhd2FpdCBmcy51bmxpbmsocGF0aC5qb2luKGZpeHR1cmUuaW50ZWdUZXN0RGlyLCAnY2RrLmNvbnRleHQuanNvbicpKTtcbiAgfVxufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NvbnRleHQgaW4gc3RhZ2UgcHJvcGFnYXRlcyB0byB0b3AnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuY2RrU3ludGgoe1xuICAgIC8vIFRoaXMgd2lsbCBtYWtlIGl0IGVycm9yIHRvIHByb3ZlIHRoYXQgdGhlIGNvbnRleHQgYnViYmxlcyB1cCwgYW5kIGFsc28gdGhhdCB3ZSBjYW4gZmFpbCBvbiBjb21tYW5kXG4gICAgb3B0aW9uczogWyctLW5vLWxvb2t1cHMnXSxcbiAgICBtb2RFbnY6IHtcbiAgICAgIElOVEVHX1NUQUNLX1NFVDogJ3N0YWdlLXVzaW5nLWNvbnRleHQnLFxuICAgIH0sXG4gICAgYWxsb3dFcnJFeGl0OiB0cnVlLFxuICB9KSkucmVzb2x2ZXMudG9Db250YWluKCdDb250ZXh0IGxvb2t1cHMgaGF2ZSBiZWVuIGRpc2FibGVkJyk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95Jywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IHN0YWNrQXJuID0gYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3Rlc3QtMicsIHsgY2FwdHVyZVN0ZGVycjogZmFsc2UgfSk7XG5cbiAgLy8gdmVyaWZ5IHRoZSBudW1iZXIgb2YgcmVzb3VyY2VzIGluIHRoZSBzdGFja1xuICBjb25zdCByZXNwb25zZSA9IGF3YWl0IGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrUmVzb3VyY2VzJywge1xuICAgIFN0YWNrTmFtZTogc3RhY2tBcm4sXG4gIH0pO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tSZXNvdXJjZXM/Lmxlbmd0aCkudG9FcXVhbCgyKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdkZXBsb3kgYWxsJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IGFybnMgPSBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgndGVzdC0qJywgeyBjYXB0dXJlU3RkZXJyOiBmYWxzZSB9KTtcblxuICAvLyB2ZXJpZnkgdGhhdCB3ZSBvbmx5IGRlcGxveWVkIGEgc2luZ2xlIHN0YWNrICh0aGVyZSdzIGEgc2luZ2xlIEFSTiBpbiB0aGUgb3V0cHV0KVxuICBleHBlY3QoYXJucy5zcGxpdCgnXFxuJykubGVuZ3RoKS50b0VxdWFsKDIpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ25lc3RlZCBzdGFjayB3aXRoIHBhcmFtZXRlcnMnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gU1RBQ0tfTkFNRV9QUkVGSVggaXMgdXNlZCBpbiBNeVRvcGljUGFyYW0gdG8gYWxsb3cgbXVsdGlwbGUgaW5zdGFuY2VzXG4gIC8vIG9mIHRoaXMgdGVzdCB0byBydW4gaW4gcGFyYWxsZWwsIG90aGV3aXNlIHRoZXkgd2lsbCBhdHRlbXB0IHRvIGNyZWF0ZSB0aGUgc2FtZSBTTlMgdG9waWMuXG4gIGNvbnN0IHN0YWNrQXJuID0gYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3dpdGgtbmVzdGVkLXN0YWNrLXVzaW5nLXBhcmFtZXRlcnMnLCB7XG4gICAgb3B0aW9uczogWyctLXBhcmFtZXRlcnMnLCBgTXlUb3BpY1BhcmFtPSR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9VGhlcmVJc05vU3Bvb25gXSxcbiAgICBjYXB0dXJlU3RkZXJyOiBmYWxzZSxcbiAgfSk7XG5cbiAgLy8gdmVyaWZ5IHRoYXQgd2Ugb25seSBkZXBsb3llZCBhIHNpbmdsZSBzdGFjayAodGhlcmUncyBhIHNpbmdsZSBBUk4gaW4gdGhlIG91dHB1dClcbiAgZXhwZWN0KHN0YWNrQXJuLnNwbGl0KCdcXG4nKS5sZW5ndGgpLnRvRXF1YWwoMSk7XG5cbiAgLy8gdmVyaWZ5IHRoZSBudW1iZXIgb2YgcmVzb3VyY2VzIGluIHRoZSBzdGFja1xuICBjb25zdCByZXNwb25zZSA9IGF3YWl0IGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrUmVzb3VyY2VzJywge1xuICAgIFN0YWNrTmFtZTogc3RhY2tBcm4sXG4gIH0pO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tSZXNvdXJjZXM/Lmxlbmd0aCkudG9FcXVhbCgxKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdkZXBsb3kgd2l0aG91dCBleGVjdXRlIGEgbmFtZWQgY2hhbmdlIHNldCcsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBjaGFuZ2VTZXROYW1lID0gJ2N1c3RvbS1jaGFuZ2Utc2V0LW5hbWUnO1xuICBjb25zdCBzdGFja0FybiA9IGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCd0ZXN0LTInLCB7XG4gICAgb3B0aW9uczogWyctLW5vLWV4ZWN1dGUnLCAnLS1jaGFuZ2Utc2V0LW5hbWUnLCBjaGFuZ2VTZXROYW1lXSxcbiAgICBjYXB0dXJlU3RkZXJyOiBmYWxzZSxcbiAgfSk7XG4gIC8vIHZlcmlmeSB0aGF0IHdlIG9ubHkgZGVwbG95ZWQgYSBzaW5nbGUgc3RhY2sgKHRoZXJlJ3MgYSBzaW5nbGUgQVJOIGluIHRoZSBvdXRwdXQpXG4gIGV4cGVjdChzdGFja0Fybi5zcGxpdCgnXFxuJykubGVuZ3RoKS50b0VxdWFsKDEpO1xuXG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3MuY2xvdWRGb3JtYXRpb24oJ2Rlc2NyaWJlU3RhY2tzJywge1xuICAgIFN0YWNrTmFtZTogc3RhY2tBcm4sXG4gIH0pO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uU3RhY2tTdGF0dXMpLnRvRXF1YWwoJ1JFVklFV19JTl9QUk9HUkVTUycpO1xuXG4gIC8vdmVyaWZ5IGEgY2hhbmdlIHNldCB3YXMgY3JlYXRlZCB3aXRoIHRoZSBwcm92aWRlZCBuYW1lXG4gIGNvbnN0IGNoYW5nZVNldFJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3MuY2xvdWRGb3JtYXRpb24oJ2xpc3RDaGFuZ2VTZXRzJywge1xuICAgIFN0YWNrTmFtZTogc3RhY2tBcm4sXG4gIH0pO1xuICBjb25zdCBjaGFuZ2VTZXRzID0gY2hhbmdlU2V0UmVzcG9uc2UuU3VtbWFyaWVzIHx8IFtdO1xuICBleHBlY3QoY2hhbmdlU2V0cy5sZW5ndGgpLnRvRXF1YWwoMSk7XG4gIGV4cGVjdChjaGFuZ2VTZXRzWzBdLkNoYW5nZVNldE5hbWUpLnRvRXF1YWwoY2hhbmdlU2V0TmFtZSk7XG4gIGV4cGVjdChjaGFuZ2VTZXRzWzBdLlN0YXR1cykudG9FcXVhbCgnQ1JFQVRFX0NPTVBMRVRFJyk7XG59KSk7XG5cbmludGVnVGVzdCgnc2VjdXJpdHkgcmVsYXRlZCBjaGFuZ2VzIHdpdGhvdXQgYSBDTEkgYXJlIGV4cGVjdGVkIHRvIGZhaWwnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gcmVkaXJlY3QgL2Rldi9udWxsIHRvIHN0ZGluLCB3aGljaCBtZWFucyB0aGVyZSB3aWxsIG5vdCBiZSB0dHkgYXR0YWNoZWRcbiAgLy8gc2luY2UgdGhpcyBzdGFjayBpbmNsdWRlcyBzZWN1cml0eS1yZWxhdGVkIGNoYW5nZXMsIHRoZSBkZXBsb3ltZW50IHNob3VsZFxuICAvLyBpbW1lZGlhdGVseSBmYWlsIGJlY2F1c2Ugd2UgY2FuJ3QgY29uZmlybSB0aGUgY2hhbmdlc1xuICBjb25zdCBzdGFja05hbWUgPSAnaWFtLXRlc3QnO1xuICBhd2FpdCBleHBlY3QoZml4dHVyZS5jZGtEZXBsb3koc3RhY2tOYW1lLCB7XG4gICAgb3B0aW9uczogWyc8JywgJy9kZXYvbnVsbCddLCAvLyBINHgsIHRoaXMgb25seSB3b3JrcyBiZWNhdXNlIEkgaGFwcGVuIHRvIGtub3cgd2UgcGFzcyBzaGVsbDogdHJ1ZS5cbiAgICBuZXZlclJlcXVpcmVBcHByb3ZhbDogZmFsc2UsXG4gIH0pKS5yZWplY3RzLnRvVGhyb3coJ2V4aXRlZCB3aXRoIGVycm9yJyk7XG5cbiAgLy8gRW5zdXJlIHN0YWNrIHdhcyBub3QgZGVwbG95ZWRcbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHtcbiAgICBTdGFja05hbWU6IGZpeHR1cmUuZnVsbFN0YWNrTmFtZShzdGFja05hbWUpLFxuICB9KSkucmVqZWN0cy50b1Rocm93KCdkb2VzIG5vdCBleGlzdCcpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2RlcGxveSB3aWxkY2FyZCB3aXRoIG91dHB1dHMnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3Qgb3V0cHV0c0ZpbGUgPSBwYXRoLmpvaW4oZml4dHVyZS5pbnRlZ1Rlc3REaXIsICdvdXRwdXRzJywgJ291dHB1dHMuanNvbicpO1xuICBhd2FpdCBmcy5ta2RpcihwYXRoLmRpcm5hbWUob3V0cHV0c0ZpbGUpLCB7IHJlY3Vyc2l2ZTogdHJ1ZSB9KTtcblxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveShbJ291dHB1dHMtdGVzdC0qJ10sIHtcbiAgICBvcHRpb25zOiBbJy0tb3V0cHV0cy1maWxlJywgb3V0cHV0c0ZpbGVdLFxuICB9KTtcblxuICBjb25zdCBvdXRwdXRzID0gSlNPTi5wYXJzZSgoYXdhaXQgZnMucmVhZEZpbGUob3V0cHV0c0ZpbGUsIHsgZW5jb2Rpbmc6ICd1dGYtOCcgfSkpLnRvU3RyaW5nKCkpO1xuICBleHBlY3Qob3V0cHV0cykudG9FcXVhbCh7XG4gICAgW2Ake2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1vdXRwdXRzLXRlc3QtMWBdOiB7XG4gICAgICBUb3BpY05hbWU6IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1vdXRwdXRzLXRlc3QtMU15VG9waWNgLFxuICAgIH0sXG4gICAgW2Ake2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1vdXRwdXRzLXRlc3QtMmBdOiB7XG4gICAgICBUb3BpY05hbWU6IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1vdXRwdXRzLXRlc3QtMk15T3RoZXJUb3BpY2AsXG4gICAgfSxcbiAgfSk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IHdpdGggcGFyYW1ldGVycycsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBzdGFja0FybiA9IGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdwYXJhbS10ZXN0LTEnLCB7XG4gICAgb3B0aW9uczogW1xuICAgICAgJy0tcGFyYW1ldGVycycsIGBUb3BpY05hbWVQYXJhbT0ke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fWJhemluZ2FgLFxuICAgIF0sXG4gICAgY2FwdHVyZVN0ZGVycjogZmFsc2UsXG4gIH0pO1xuXG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3MuY2xvdWRGb3JtYXRpb24oJ2Rlc2NyaWJlU3RhY2tzJywge1xuICAgIFN0YWNrTmFtZTogc3RhY2tBcm4sXG4gIH0pO1xuXG4gIGV4cGVjdChyZXNwb25zZS5TdGFja3M/LlswXS5QYXJhbWV0ZXJzKS50b0VxdWFsKFtcbiAgICB7XG4gICAgICBQYXJhbWV0ZXJLZXk6ICdUb3BpY05hbWVQYXJhbScsXG4gICAgICBQYXJhbWV0ZXJWYWx1ZTogYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9YmF6aW5nYWAsXG4gICAgfSxcbiAgXSk7XG59KSk7XG5cbmludGVnVGVzdCgndXBkYXRlIHRvIHN0YWNrIGluIFJPTExCQUNLX0NPTVBMRVRFIHN0YXRlIHdpbGwgZGVsZXRlIHN0YWNrIGFuZCBjcmVhdGUgYSBuZXcgb25lJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIC8vIEdJVkVOXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNka0RlcGxveSgncGFyYW0tdGVzdC0xJywge1xuICAgIG9wdGlvbnM6IFtcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgVG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1AYXd3YCxcbiAgICBdLFxuICAgIGNhcHR1cmVTdGRlcnI6IGZhbHNlLFxuICB9KSkucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpO1xuXG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3MuY2xvdWRGb3JtYXRpb24oJ2Rlc2NyaWJlU3RhY2tzJywge1xuICAgIFN0YWNrTmFtZTogZml4dHVyZS5mdWxsU3RhY2tOYW1lKCdwYXJhbS10ZXN0LTEnKSxcbiAgfSk7XG5cbiAgY29uc3Qgc3RhY2tBcm4gPSByZXNwb25zZS5TdGFja3M/LlswXS5TdGFja0lkO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uU3RhY2tTdGF0dXMpLnRvRXF1YWwoJ1JPTExCQUNLX0NPTVBMRVRFJyk7XG5cbiAgLy8gV0hFTlxuICBjb25zdCBuZXdTdGFja0FybiA9IGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdwYXJhbS10ZXN0LTEnLCB7XG4gICAgb3B0aW9uczogW1xuICAgICAgJy0tcGFyYW1ldGVycycsIGBUb3BpY05hbWVQYXJhbT0ke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fWFsbGdvb2RgLFxuICAgIF0sXG4gICAgY2FwdHVyZVN0ZGVycjogZmFsc2UsXG4gIH0pO1xuXG4gIGNvbnN0IG5ld1N0YWNrUmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBuZXdTdGFja0FybixcbiAgfSk7XG5cbiAgLy8gVEhFTlxuICBleHBlY3QgKHN0YWNrQXJuKS5ub3QudG9FcXVhbChuZXdTdGFja0Fybik7IC8vIG5ldyBzdGFjayB3YXMgY3JlYXRlZFxuICBleHBlY3QobmV3U3RhY2tSZXNwb25zZS5TdGFja3M/LlswXS5TdGFja1N0YXR1cykudG9FcXVhbCgnQ1JFQVRFX0NPTVBMRVRFJyk7XG4gIGV4cGVjdChuZXdTdGFja1Jlc3BvbnNlLlN0YWNrcz8uWzBdLlBhcmFtZXRlcnMpLnRvRXF1YWwoW1xuICAgIHtcbiAgICAgIFBhcmFtZXRlcktleTogJ1RvcGljTmFtZVBhcmFtJyxcbiAgICAgIFBhcmFtZXRlclZhbHVlOiBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1hbGxnb29kYCxcbiAgICB9LFxuICBdKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdzdGFjayBpbiBVUERBVEVfUk9MTEJBQ0tfQ09NUExFVEUgc3RhdGUgY2FuIGJlIHVwZGF0ZWQnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gR0lWRU5cbiAgY29uc3Qgc3RhY2tBcm4gPSBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgncGFyYW0tdGVzdC0xJywge1xuICAgIG9wdGlvbnM6IFtcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgVG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1uaWNlYCxcbiAgICBdLFxuICAgIGNhcHR1cmVTdGRlcnI6IGZhbHNlLFxuICB9KTtcblxuICBsZXQgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBzdGFja0FybixcbiAgfSk7XG5cbiAgZXhwZWN0KHJlc3BvbnNlLlN0YWNrcz8uWzBdLlN0YWNrU3RhdHVzKS50b0VxdWFsKCdDUkVBVEVfQ09NUExFVEUnKTtcblxuICAvLyBiYWQgcGFyYW1ldGVyIG5hbWUgd2l0aCBAIHdpbGwgcHV0IHN0YWNrIGludG8gVVBEQVRFX1JPTExCQUNLX0NPTVBMRVRFXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNka0RlcGxveSgncGFyYW0tdGVzdC0xJywge1xuICAgIG9wdGlvbnM6IFtcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgVG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1AYXd3YCxcbiAgICBdLFxuICAgIGNhcHR1cmVTdGRlcnI6IGZhbHNlLFxuICB9KSkucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpOztcblxuICByZXNwb25zZSA9IGF3YWl0IGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHtcbiAgICBTdGFja05hbWU6IHN0YWNrQXJuLFxuICB9KTtcblxuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uU3RhY2tTdGF0dXMpLnRvRXF1YWwoJ1VQREFURV9ST0xMQkFDS19DT01QTEVURScpO1xuXG4gIC8vIFdIRU5cbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3BhcmFtLXRlc3QtMScsIHtcbiAgICBvcHRpb25zOiBbXG4gICAgICAnLS1wYXJhbWV0ZXJzJywgYFRvcGljTmFtZVBhcmFtPSR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9YWxsZ29vZGAsXG4gICAgXSxcbiAgICBjYXB0dXJlU3RkZXJyOiBmYWxzZSxcbiAgfSk7XG5cbiAgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBzdGFja0FybixcbiAgfSk7XG5cbiAgLy8gVEhFTlxuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uU3RhY2tTdGF0dXMpLnRvRXF1YWwoJ1VQREFURV9DT01QTEVURScpO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uUGFyYW1ldGVycykudG9FcXVhbChbXG4gICAge1xuICAgICAgUGFyYW1ldGVyS2V5OiAnVG9waWNOYW1lUGFyYW0nLFxuICAgICAgUGFyYW1ldGVyVmFsdWU6IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fWFsbGdvb2RgLFxuICAgIH0sXG4gIF0pO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2RlcGxveSB3aXRoIHdpbGRjYXJkIGFuZCBwYXJhbWV0ZXJzJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdwYXJhbS10ZXN0LSonLCB7XG4gICAgb3B0aW9uczogW1xuICAgICAgJy0tcGFyYW1ldGVycycsIGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1wYXJhbS10ZXN0LTE6VG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1iYXppbmdhYCxcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH0tcGFyYW0tdGVzdC0yOk90aGVyVG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1UaGF0c015U3BvdGAsXG4gICAgICAnLS1wYXJhbWV0ZXJzJywgYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9LXBhcmFtLXRlc3QtMzpEaXNwbGF5TmFtZVBhcmFtPSR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9SGV5VGhlcmVgLFxuICAgICAgJy0tcGFyYW1ldGVycycsIGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1wYXJhbS10ZXN0LTM6T3RoZXJEaXNwbGF5TmFtZVBhcmFtPSR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9QW5vdGhlck9uZWAsXG4gICAgXSxcbiAgfSk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IHdpdGggcGFyYW1ldGVycyBtdWx0aScsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBwYXJhbVZhbDEgPSBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1iYXppbmdhYDtcbiAgY29uc3QgcGFyYW1WYWwyID0gYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9PWphZ3NoZW1hc2hgO1xuXG4gIGNvbnN0IHN0YWNrQXJuID0gYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3BhcmFtLXRlc3QtMycsIHtcbiAgICBvcHRpb25zOiBbXG4gICAgICAnLS1wYXJhbWV0ZXJzJywgYERpc3BsYXlOYW1lUGFyYW09JHtwYXJhbVZhbDF9YCxcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgT3RoZXJEaXNwbGF5TmFtZVBhcmFtPSR7cGFyYW1WYWwyfWAsXG4gICAgXSxcbiAgICBjYXB0dXJlU3RkZXJyOiBmYWxzZSxcbiAgfSk7XG5cbiAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBzdGFja0FybixcbiAgfSk7XG5cbiAgZXhwZWN0KHJlc3BvbnNlLlN0YWNrcz8uWzBdLlBhcmFtZXRlcnMpLnRvRXF1YWwoW1xuICAgIHtcbiAgICAgIFBhcmFtZXRlcktleTogJ0Rpc3BsYXlOYW1lUGFyYW0nLFxuICAgICAgUGFyYW1ldGVyVmFsdWU6IHBhcmFtVmFsMSxcbiAgICB9LFxuICAgIHtcbiAgICAgIFBhcmFtZXRlcktleTogJ090aGVyRGlzcGxheU5hbWVQYXJhbScsXG4gICAgICBQYXJhbWV0ZXJWYWx1ZTogcGFyYW1WYWwyLFxuICAgIH0sXG4gIF0pO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2RlcGxveSB3aXRoIG5vdGlmaWNhdGlvbiBBUk4nLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3QgdG9waWNOYW1lID0gYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9LXRlc3QtdG9waWNgO1xuXG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3Muc25zKCdjcmVhdGVUb3BpYycsIHsgTmFtZTogdG9waWNOYW1lIH0pO1xuICBjb25zdCB0b3BpY0FybiA9IHJlc3BvbnNlLlRvcGljQXJuITtcbiAgdHJ5IHtcbiAgICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgndGVzdC0yJywge1xuICAgICAgb3B0aW9uczogWyctLW5vdGlmaWNhdGlvbi1hcm5zJywgdG9waWNBcm5dLFxuICAgIH0pO1xuXG4gICAgLy8gdmVyaWZ5IHRoYXQgdGhlIHN0YWNrIHdlIGRlcGxveWVkIGhhcyBvdXIgbm90aWZpY2F0aW9uIEFSTlxuICAgIGNvbnN0IGRlc2NyaWJlUmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgICBTdGFja05hbWU6IGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0yJyksXG4gICAgfSk7XG4gICAgZXhwZWN0KGRlc2NyaWJlUmVzcG9uc2UuU3RhY2tzPy5bMF0uTm90aWZpY2F0aW9uQVJOcykudG9FcXVhbChbdG9waWNBcm5dKTtcbiAgfSBmaW5hbGx5IHtcbiAgICBhd2FpdCBmaXh0dXJlLmF3cy5zbnMoJ2RlbGV0ZVRvcGljJywge1xuICAgICAgVG9waWNBcm46IHRvcGljQXJuLFxuICAgIH0pO1xuICB9XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IHdpdGggcm9sZScsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCByb2xlTmFtZSA9IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS10ZXN0LXJvbGVgO1xuXG4gIGF3YWl0IGRlbGV0ZVJvbGUoKTtcblxuICBjb25zdCBjcmVhdGVSZXNwb25zZSA9IGF3YWl0IGZpeHR1cmUuYXdzLmlhbSgnY3JlYXRlUm9sZScsIHtcbiAgICBSb2xlTmFtZTogcm9sZU5hbWUsXG4gICAgQXNzdW1lUm9sZVBvbGljeURvY3VtZW50OiBKU09OLnN0cmluZ2lmeSh7XG4gICAgICBWZXJzaW9uOiAnMjAxMi0xMC0xNycsXG4gICAgICBTdGF0ZW1lbnQ6IFt7XG4gICAgICAgIEFjdGlvbjogJ3N0czpBc3N1bWVSb2xlJyxcbiAgICAgICAgUHJpbmNpcGFsOiB7IFNlcnZpY2U6ICdjbG91ZGZvcm1hdGlvbi5hbWF6b25hd3MuY29tJyB9LFxuICAgICAgICBFZmZlY3Q6ICdBbGxvdycsXG4gICAgICB9LCB7XG4gICAgICAgIEFjdGlvbjogJ3N0czpBc3N1bWVSb2xlJyxcbiAgICAgICAgUHJpbmNpcGFsOiB7IEFXUzogKGF3YWl0IGZpeHR1cmUuYXdzLnN0cygnZ2V0Q2FsbGVySWRlbnRpdHknLCB7fSkpLkFybiB9LFxuICAgICAgICBFZmZlY3Q6ICdBbGxvdycsXG4gICAgICB9XSxcbiAgICB9KSxcbiAgfSk7XG4gIGNvbnN0IHJvbGVBcm4gPSBjcmVhdGVSZXNwb25zZS5Sb2xlLkFybjtcbiAgdHJ5IHtcbiAgICBhd2FpdCBmaXh0dXJlLmF3cy5pYW0oJ3B1dFJvbGVQb2xpY3knLCB7XG4gICAgICBSb2xlTmFtZTogcm9sZU5hbWUsXG4gICAgICBQb2xpY3lOYW1lOiAnRGVmYXVsdFBvbGljeScsXG4gICAgICBQb2xpY3lEb2N1bWVudDogSlNPTi5zdHJpbmdpZnkoe1xuICAgICAgICBWZXJzaW9uOiAnMjAxMi0xMC0xNycsXG4gICAgICAgIFN0YXRlbWVudDogW3tcbiAgICAgICAgICBBY3Rpb246ICcqJyxcbiAgICAgICAgICBSZXNvdXJjZTogJyonLFxuICAgICAgICAgIEVmZmVjdDogJ0FsbG93JyxcbiAgICAgICAgfV0sXG4gICAgICB9KSxcbiAgICB9KTtcblxuICAgIGF3YWl0IHJldHJ5KGZpeHR1cmUub3V0cHV0LCAnVHJ5aW5nIHRvIGFzc3VtZSBmcmVzaCByb2xlJywgcmV0cnkuZm9yU2Vjb25kcygzMDApLCBhc3luYyAoKSA9PiB7XG4gICAgICBhd2FpdCBmaXh0dXJlLmF3cy5zdHMoJ2Fzc3VtZVJvbGUnLCB7XG4gICAgICAgIFJvbGVBcm46IHJvbGVBcm4sXG4gICAgICAgIFJvbGVTZXNzaW9uTmFtZTogJ3Rlc3RpbmcnLFxuICAgICAgfSk7XG4gICAgfSk7XG5cbiAgICAvLyBJbiBwcmluY2lwbGUsIHRoZSByb2xlIGhhcyByZXBsaWNhdGVkIGZyb20gJ3VzLWVhc3QtMScgdG8gd2hlcmV2ZXIgd2UncmUgdGVzdGluZy5cbiAgICAvLyBHaXZlIGl0IGEgbGl0dGxlIG1vcmUgc2xlZXAgdG8gbWFrZSBzdXJlIENsb3VkRm9ybWF0aW9uIGlzIG5vdCBoaXR0aW5nIGEgYm94XG4gICAgLy8gdGhhdCBkb2Vzbid0IGhhdmUgaXQgeWV0LlxuICAgIGF3YWl0IHNsZWVwKDUwMDApO1xuXG4gICAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3Rlc3QtMicsIHtcbiAgICAgIG9wdGlvbnM6IFsnLS1yb2xlLWFybicsIHJvbGVBcm5dLFxuICAgIH0pO1xuXG4gICAgLy8gSW1tZWRpYXRlbHkgZGVsZXRlIHRoZSBzdGFjayBhZ2FpbiBiZWZvcmUgd2UgZGVsZXRlIHRoZSByb2xlLlxuICAgIC8vXG4gICAgLy8gU2luY2Ugcm9sZXMgYXJlIHN0aWNreSwgaWYgd2UgZGVsZXRlIHRoZSByb2xlIGJlZm9yZSB0aGUgc3RhY2ssIHN1YnNlcXVlbnQgRGVsZXRlU3RhY2tcbiAgICAvLyBvcGVyYXRpb25zIHdpbGwgZmFpbCB3aGVuIENsb3VkRm9ybWF0aW9uIHRyaWVzIHRvIGFzc3VtZSB0aGUgcm9sZSB0aGF0J3MgYWxyZWFkeSBnb25lLlxuICAgIGF3YWl0IGZpeHR1cmUuY2RrRGVzdHJveSgndGVzdC0yJyk7XG5cbiAgfSBmaW5hbGx5IHtcbiAgICBhd2FpdCBkZWxldGVSb2xlKCk7XG4gIH1cblxuICBhc3luYyBmdW5jdGlvbiBkZWxldGVSb2xlKCkge1xuICAgIHRyeSB7XG4gICAgICBmb3IgKGNvbnN0IHBvbGljeU5hbWUgb2YgKGF3YWl0IGZpeHR1cmUuYXdzLmlhbSgnbGlzdFJvbGVQb2xpY2llcycsIHsgUm9sZU5hbWU6IHJvbGVOYW1lIH0pKS5Qb2xpY3lOYW1lcykge1xuICAgICAgICBhd2FpdCBmaXh0dXJlLmF3cy5pYW0oJ2RlbGV0ZVJvbGVQb2xpY3knLCB7XG4gICAgICAgICAgUm9sZU5hbWU6IHJvbGVOYW1lLFxuICAgICAgICAgIFBvbGljeU5hbWU6IHBvbGljeU5hbWUsXG4gICAgICAgIH0pO1xuICAgICAgfVxuICAgICAgYXdhaXQgZml4dHVyZS5hd3MuaWFtKCdkZWxldGVSb2xlJywgeyBSb2xlTmFtZTogcm9sZU5hbWUgfSk7XG4gICAgfSBjYXRjaCAoZSkge1xuICAgICAgaWYgKGUubWVzc2FnZS5pbmRleE9mKCdjYW5ub3QgYmUgZm91bmQnKSA+IC0xKSB7IHJldHVybjsgfVxuICAgICAgdGhyb3cgZTtcbiAgICB9XG4gIH1cbn0pKTtcblxuaW50ZWdUZXN0KCdjZGsgZGlmZicsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBkaWZmMSA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnZGlmZicsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0xJyldKTtcbiAgZXhwZWN0KGRpZmYxKS50b0NvbnRhaW4oJ0FXUzo6U05TOjpUb3BpYycpO1xuXG4gIGNvbnN0IGRpZmYyID0gYXdhaXQgZml4dHVyZS5jZGsoWydkaWZmJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0pO1xuICBleHBlY3QoZGlmZjIpLnRvQ29udGFpbignQVdTOjpTTlM6OlRvcGljJyk7XG5cbiAgLy8gV2UgY2FuIG1ha2UgaXQgZmFpbCBieSBwYXNzaW5nIC0tZmFpbFxuICBhd2FpdCBleHBlY3QoZml4dHVyZS5jZGsoWydkaWZmJywgJy0tZmFpbCcsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0xJyldKSlcbiAgICAucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NkayBkaWZmIC0tZmFpbCBvbiBtdWx0aXBsZSBzdGFja3MgZXhpdHMgd2l0aCBlcnJvciBpZiBhbnkgb2YgdGhlIHN0YWNrcyBjb250YWlucyBhIGRpZmYnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gR0lWRU5cbiAgY29uc3QgZGlmZjEgPSBhd2FpdCBmaXh0dXJlLmNkayhbJ2RpZmYnLCBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ3Rlc3QtMScpXSk7XG4gIGV4cGVjdChkaWZmMSkudG9Db250YWluKCdBV1M6OlNOUzo6VG9waWMnKTtcblxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgndGVzdC0yJyk7XG4gIGNvbnN0IGRpZmYyID0gYXdhaXQgZml4dHVyZS5jZGsoWydkaWZmJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0pO1xuICBleHBlY3QoZGlmZjIpLnRvQ29udGFpbignVGhlcmUgd2VyZSBubyBkaWZmZXJlbmNlcycpO1xuXG4gIC8vIFdIRU4gLyBUSEVOXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ2RpZmYnLCAnLS1mYWlsJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTEnKSwgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0pKS5yZWplY3RzLnRvVGhyb3coJ2V4aXRlZCB3aXRoIGVycm9yJyk7XG59KSk7XG5cbmludGVnVGVzdCgnY2RrIGRpZmYgLS1mYWlsIHdpdGggbXVsdGlwbGUgc3RhY2sgZXhpdHMgd2l0aCBpZiBhbnkgb2YgdGhlIHN0YWNrcyBjb250YWlucyBhIGRpZmYnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gR0lWRU5cbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3Rlc3QtMScpO1xuICBjb25zdCBkaWZmMSA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnZGlmZicsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0xJyldKTtcbiAgZXhwZWN0KGRpZmYxKS50b0NvbnRhaW4oJ1RoZXJlIHdlcmUgbm8gZGlmZmVyZW5jZXMnKTtcblxuICBjb25zdCBkaWZmMiA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnZGlmZicsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0yJyldKTtcbiAgZXhwZWN0KGRpZmYyKS50b0NvbnRhaW4oJ0FXUzo6U05TOjpUb3BpYycpO1xuXG4gIC8vIFdIRU4gLyBUSEVOXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ2RpZmYnLCAnLS1mYWlsJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTEnKSwgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0pKS5yZWplY3RzLnRvVGhyb3coJ2V4aXRlZCB3aXRoIGVycm9yJyk7XG59KSk7XG5cbmludGVnVGVzdCgnY2RrIGRpZmYgLS1zZWN1cml0eS1vbmx5IC0tZmFpbCBleGl0cyB3aGVuIHNlY3VyaXR5IGNoYW5nZXMgYXJlIHByZXNlbnQnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3Qgc3RhY2tOYW1lID0gJ2lhbS10ZXN0JztcbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuY2RrKFsnZGlmZicsICctLXNlY3VyaXR5LW9ubHknLCAnLS1mYWlsJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKHN0YWNrTmFtZSldKSkucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2RlcGxveSBzdGFjayB3aXRoIGRvY2tlciBhc3NldCcsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnZG9ja2VyJyk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IGFuZCB0ZXN0IHN0YWNrIHdpdGggbGFtYmRhIGFzc2V0Jywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IHN0YWNrQXJuID0gYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ2xhbWJkYScsIHsgY2FwdHVyZVN0ZGVycjogZmFsc2UgfSk7XG5cbiAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBzdGFja0FybixcbiAgfSk7XG4gIGNvbnN0IGxhbWJkYUFybiA9IHJlc3BvbnNlLlN0YWNrcz8uWzBdLk91dHB1dHM/LlswXS5PdXRwdXRWYWx1ZTtcbiAgaWYgKGxhbWJkYUFybiA9PT0gdW5kZWZpbmVkKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKCdTdGFjayBkaWQgbm90IGhhdmUgZXhwZWN0ZWQgTGFtYmRhIEFSTiBvdXRwdXQnKTtcbiAgfVxuXG4gIGNvbnN0IG91dHB1dCA9IGF3YWl0IGZpeHR1cmUuYXdzLmxhbWJkYSgnaW52b2tlJywge1xuICAgIEZ1bmN0aW9uTmFtZTogbGFtYmRhQXJuLFxuICB9KTtcblxuICBleHBlY3QoSlNPTi5zdHJpbmdpZnkob3V0cHV0LlBheWxvYWQpKS50b0NvbnRhaW4oJ2RlYXIgYXNzZXQnKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdjZGsgbHMnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3QgbGlzdGluZyA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnbHMnXSwgeyBjYXB0dXJlU3RkZXJyOiBmYWxzZSB9KTtcblxuICBjb25zdCBleHBlY3RlZFN0YWNrcyA9IFtcbiAgICAnY29uZGl0aW9uYWwtcmVzb3VyY2UnLFxuICAgICdkb2NrZXInLFxuICAgICdkb2NrZXItd2l0aC1jdXN0b20tZmlsZScsXG4gICAgJ2ZhaWxlZCcsXG4gICAgJ2lhbS10ZXN0JyxcbiAgICAnbGFtYmRhJyxcbiAgICAnbWlzc2luZy1zc20tcGFyYW1ldGVyJyxcbiAgICAnb3JkZXItcHJvdmlkaW5nJyxcbiAgICAnb3V0cHV0cy10ZXN0LTEnLFxuICAgICdvdXRwdXRzLXRlc3QtMicsXG4gICAgJ3BhcmFtLXRlc3QtMScsXG4gICAgJ3BhcmFtLXRlc3QtMicsXG4gICAgJ3BhcmFtLXRlc3QtMycsXG4gICAgJ3Rlcm1pbmF0aW9uLXByb3RlY3Rpb24nLFxuICAgICd0ZXN0LTEnLFxuICAgICd0ZXN0LTInLFxuICAgICd3aXRoLW5lc3RlZC1zdGFjaycsXG4gICAgJ3dpdGgtbmVzdGVkLXN0YWNrLXVzaW5nLXBhcmFtZXRlcnMnLFxuICAgICdvcmRlci1jb25zdW1pbmcnLFxuICBdO1xuXG4gIGZvciAoY29uc3Qgc3RhY2sgb2YgZXhwZWN0ZWRTdGFja3MpIHtcbiAgICBleHBlY3QobGlzdGluZykudG9Db250YWluKGZpeHR1cmUuZnVsbFN0YWNrTmFtZShzdGFjaykpO1xuICB9XG59KSk7XG5cbmludGVnVGVzdCgnc3ludGhpbmcgYSBzdGFnZSB3aXRoIGVycm9ycyBsZWFkcyB0byBmYWlsdXJlJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IG91dHB1dCA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnc3ludGgnXSwge1xuICAgIGFsbG93RXJyRXhpdDogdHJ1ZSxcbiAgICBtb2RFbnY6IHtcbiAgICAgIElOVEVHX1NUQUNLX1NFVDogJ3N0YWdlLXdpdGgtZXJyb3JzJyxcbiAgICB9LFxuICB9KTtcblxuICBleHBlY3Qob3V0cHV0KS50b0NvbnRhaW4oJ1RoaXMgaXMgYW4gZXJyb3InKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdzeW50aGluZyBhIHN0YWdlIHdpdGggZXJyb3JzIGNhbiBiZSBzdXBwcmVzc2VkJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGF3YWl0IGZpeHR1cmUuY2RrKFsnc3ludGgnLCAnLS1uby12YWxpZGF0aW9uJ10sIHtcbiAgICBtb2RFbnY6IHtcbiAgICAgIElOVEVHX1NUQUNLX1NFVDogJ3N0YWdlLXdpdGgtZXJyb3JzJyxcbiAgICB9LFxuICB9KTtcbn0pKTtcblxuaW50ZWdUZXN0KCdkZXBsb3kgc3RhY2sgd2l0aG91dCByZXNvdXJjZScsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICAvLyBEZXBsb3kgdGhlIHN0YWNrIHdpdGhvdXQgcmVzb3VyY2VzXG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdjb25kaXRpb25hbC1yZXNvdXJjZScsIHsgbW9kRW52OiB7IE5PX1JFU09VUkNFOiAnVFJVRScgfSB9KTtcblxuICAvLyBUaGlzIHNob3VsZCBoYXZlIHN1Y2NlZWRlZCBidXQgbm90IGRlcGxveWVkIHRoZSBzdGFjay5cbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHsgU3RhY2tOYW1lOiBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ2NvbmRpdGlvbmFsLXJlc291cmNlJykgfSkpXG4gICAgLnJlamVjdHMudG9UaHJvdygnY29uZGl0aW9uYWwtcmVzb3VyY2UgZG9lcyBub3QgZXhpc3QnKTtcblxuICAvLyBEZXBsb3kgdGhlIHN0YWNrIHdpdGggcmVzb3VyY2VzXG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdjb25kaXRpb25hbC1yZXNvdXJjZScpO1xuXG4gIC8vIFRoZW4gYWdhaW4gV0lUSE9VVCByZXNvdXJjZXMgKHRoaXMgc2hvdWxkIGRlc3Ryb3kgdGhlIHN0YWNrKVxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnY29uZGl0aW9uYWwtcmVzb3VyY2UnLCB7IG1vZEVudjogeyBOT19SRVNPVVJDRTogJ1RSVUUnIH0gfSk7XG5cbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHsgU3RhY2tOYW1lOiBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ2NvbmRpdGlvbmFsLXJlc291cmNlJykgfSkpXG4gICAgLnJlamVjdHMudG9UaHJvdygnY29uZGl0aW9uYWwtcmVzb3VyY2UgZG9lcyBub3QgZXhpc3QnKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdJQU0gZGlmZicsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBvdXRwdXQgPSBhd2FpdCBmaXh0dXJlLmNkayhbJ2RpZmYnLCBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ2lhbS10ZXN0JyldKTtcblxuICAvLyBSb3VnaGx5IGNoZWNrIGZvciBhIHRhYmxlIGxpa2UgdGhpczpcbiAgLy9cbiAgLy8g4pSM4pSA4pSA4pSA4pSs4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSs4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSs4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSs4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSALeKUgOKUgOKUrOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUkFxuICAvLyDilIIgICDilIIgUmVzb3VyY2UgICAgICAgIOKUgiBFZmZlY3Qg4pSCIEFjdGlvbiAgICAgICAgIOKUgiBQcmluY2lwYWwgICAgICAgICAgICAgICAgICAgICDilIIgQ29uZGl0aW9uIOKUglxuICAvLyDilJzilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilKRcbiAgLy8g4pSCICsg4pSCICR7U29tZVJvbGUuQXJufSDilIIgQWxsb3cgIOKUgiBzdHM6QXNzdW1lUm9sZSDilIIgU2VydmljZTplYzIuYW1hem9uYXdzLmNvbSAgICAg4pSCICAgICAgICAgICDilIJcbiAgLy8g4pSU4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSYXG5cbiAgZXhwZWN0KG91dHB1dCkudG9Db250YWluKCcke1NvbWVSb2xlLkFybn0nKTtcbiAgZXhwZWN0KG91dHB1dCkudG9Db250YWluKCdzdHM6QXNzdW1lUm9sZScpO1xuICBleHBlY3Qob3V0cHV0KS50b0NvbnRhaW4oJ2VjMi5hbWF6b25hd3MuY29tJyk7XG59KSk7XG5cbmludGVnVGVzdCgnZmFzdCBkZXBsb3knLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gd2UgYXJlIHVzaW5nIGEgc3RhY2sgd2l0aCBhIG5lc3RlZCBzdGFjayBiZWNhdXNlIENGTiB3aWxsIGFsd2F5cyBhdHRlbXB0IHRvXG4gIC8vIHVwZGF0ZSBhIG5lc3RlZCBzdGFjaywgd2hpY2ggd2lsbCBhbGxvdyB1cyB0byB2ZXJpZnkgdGhhdCB1cGRhdGVzIGFyZSBhY3R1YWxseVxuICAvLyBza2lwcGVkIHVubGVzcyAtLWZvcmNlIGlzIHNwZWNpZmllZC5cbiAgY29uc3Qgc3RhY2tBcm4gPSBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnd2l0aC1uZXN0ZWQtc3RhY2snLCB7IGNhcHR1cmVTdGRlcnI6IGZhbHNlIH0pO1xuICBjb25zdCBjaGFuZ2VTZXQxID0gYXdhaXQgZ2V0TGF0ZXN0Q2hhbmdlU2V0KCk7XG5cbiAgLy8gRGVwbG95IHRoZSBzYW1lIHN0YWNrIGFnYWluLCB0aGVyZSBzaG91bGQgYmUgbm8gbmV3IGNoYW5nZSBzZXQgY3JlYXRlZFxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnd2l0aC1uZXN0ZWQtc3RhY2snKTtcbiAgY29uc3QgY2hhbmdlU2V0MiA9IGF3YWl0IGdldExhdGVzdENoYW5nZVNldCgpO1xuICBleHBlY3QoY2hhbmdlU2V0Mi5DaGFuZ2VTZXRJZCkudG9FcXVhbChjaGFuZ2VTZXQxLkNoYW5nZVNldElkKTtcblxuICAvLyBEZXBsb3kgdGhlIHN0YWNrIGFnYWluIHdpdGggLS1mb3JjZSwgbm93IHdlIHNob3VsZCBjcmVhdGUgYSBjaGFuZ2VzZXRcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3dpdGgtbmVzdGVkLXN0YWNrJywgeyBvcHRpb25zOiBbJy0tZm9yY2UnXSB9KTtcbiAgY29uc3QgY2hhbmdlU2V0MyA9IGF3YWl0IGdldExhdGVzdENoYW5nZVNldCgpO1xuICBleHBlY3QoY2hhbmdlU2V0My5DaGFuZ2VTZXRJZCkubm90LnRvRXF1YWwoY2hhbmdlU2V0Mi5DaGFuZ2VTZXRJZCk7XG5cbiAgLy8gRGVwbG95IHRoZSBzdGFjayBhZ2FpbiB3aXRoIHRhZ3MsIGV4cGVjdGVkIHRvIGNyZWF0ZSBhIG5ldyBjaGFuZ2VzZXRcbiAgLy8gZXZlbiB0aG91Z2ggdGhlIHJlc291cmNlcyBkaWRuJ3QgY2hhbmdlLlxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnd2l0aC1uZXN0ZWQtc3RhY2snLCB7IG9wdGlvbnM6IFsnLS10YWdzJywgJ2tleT12YWx1ZSddIH0pO1xuICBjb25zdCBjaGFuZ2VTZXQ0ID0gYXdhaXQgZ2V0TGF0ZXN0Q2hhbmdlU2V0KCk7XG4gIGV4cGVjdChjaGFuZ2VTZXQ0LkNoYW5nZVNldElkKS5ub3QudG9FcXVhbChjaGFuZ2VTZXQzLkNoYW5nZVNldElkKTtcblxuICBhc3luYyBmdW5jdGlvbiBnZXRMYXRlc3RDaGFuZ2VTZXQoKSB7XG4gICAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7IFN0YWNrTmFtZTogc3RhY2tBcm4gfSk7XG4gICAgaWYgKCFyZXNwb25zZS5TdGFja3M/LlswXSkgeyB0aHJvdyBuZXcgRXJyb3IoJ0RpZCBub3QgZ2V0IGEgQ2hhbmdlU2V0IGF0IGFsbCcpOyB9XG4gICAgZml4dHVyZS5sb2coYEZvdW5kIENoYW5nZSBTZXQgJHtyZXNwb25zZS5TdGFja3M/LlswXS5DaGFuZ2VTZXRJZH1gKTtcbiAgICByZXR1cm4gcmVzcG9uc2UuU3RhY2tzPy5bMF07XG4gIH1cbn0pKTtcblxuaW50ZWdUZXN0KCdmYWlsZWQgZGVwbG95IGRvZXMgbm90IGhhbmcnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gdGhpcyB3aWxsIGhhbmcgaWYgd2UgaW50cm9kdWNlIGh0dHBzOi8vZ2l0aHViLmNvbS9hd3MvYXdzLWNkay9pc3N1ZXMvNjQwMyBhZ2Fpbi5cbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuY2RrRGVwbG95KCdmYWlsZWQnKSkucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NhbiBzdGlsbCBsb2FkIG9sZCBhc3NlbWJsaWVzJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IGN4QXNtRGlyID0gcGF0aC5qb2luKG9zLnRtcGRpcigpLCAnY2RrLWludGVnLWN4Jyk7XG5cbiAgY29uc3QgdGVzdEFzc2VtYmxpZXNEaXJlY3RvcnkgPSBwYXRoLmpvaW4oX19kaXJuYW1lLCAnY2xvdWQtYXNzZW1ibGllcycpO1xuICBmb3IgKGNvbnN0IGFzbWRpciBvZiBhd2FpdCBsaXN0Q2hpbGREaXJzKHRlc3RBc3NlbWJsaWVzRGlyZWN0b3J5KSkge1xuICAgIGZpeHR1cmUubG9nKGBBU1NFTUJMWSAke2FzbWRpcn1gKTtcbiAgICBhd2FpdCBjbG9uZURpcmVjdG9yeShhc21kaXIsIGN4QXNtRGlyKTtcblxuICAgIC8vIFNvbWUgZmlsZXMgaW4gdGhlIGFzbSBkaXJlY3RvcnkgdGhhdCBoYXZlIGEgLmpzIGV4dGVuc2lvbiBhcmVcbiAgICAvLyBhY3R1YWxseSB0cmVhdGVkIGFzIHRlbXBsYXRlcy4gRXZhbHVhdGUgdGhlbSB1c2luZyBOb2RlSlMuXG4gICAgY29uc3QgdGVtcGxhdGVzID0gYXdhaXQgbGlzdENoaWxkcmVuKGN4QXNtRGlyLCBmdWxsUGF0aCA9PiBQcm9taXNlLnJlc29sdmUoZnVsbFBhdGguZW5kc1dpdGgoJy5qcycpKSk7XG4gICAgZm9yIChjb25zdCB0ZW1wbGF0ZSBvZiB0ZW1wbGF0ZXMpIHtcbiAgICAgIGNvbnN0IHRhcmdldE5hbWUgPSB0ZW1wbGF0ZS5yZXBsYWNlKC8uanMkLywgJycpO1xuICAgICAgYXdhaXQgc2hlbGwoW3Byb2Nlc3MuZXhlY1BhdGgsIHRlbXBsYXRlLCAnPicsIHRhcmdldE5hbWVdLCB7XG4gICAgICAgIGN3ZDogY3hBc21EaXIsXG4gICAgICAgIG91dHB1dDogZml4dHVyZS5vdXRwdXQsXG4gICAgICAgIG1vZEVudjoge1xuICAgICAgICAgIFRFU1RfQUNDT1VOVDogYXdhaXQgZml4dHVyZS5hd3MuYWNjb3VudCgpLFxuICAgICAgICAgIFRFU1RfUkVHSU9OOiBmaXh0dXJlLmF3cy5yZWdpb24sXG4gICAgICAgIH0sXG4gICAgICB9KTtcbiAgICB9XG5cbiAgICAvLyBVc2UgdGhpcyBkaXJlY3RvcnkgYXMgYSBDbG91ZCBBc3NlbWJseVxuICAgIGNvbnN0IG91dHB1dCA9IGF3YWl0IGZpeHR1cmUuY2RrKFtcbiAgICAgICctLWFwcCcsIGN4QXNtRGlyLFxuICAgICAgJy12JyxcbiAgICAgICdzeW50aCcsXG4gICAgXSk7XG5cbiAgICAvLyBBc3NlcnQgdGhhdCB0aGVyZSB3YXMgbm8gcHJvdmlkZXJFcnJvciBpbiBDREsncyBzdGRlcnJcbiAgICAvLyBCZWNhdXNlIHdlIHJlbHkgb24gdGhlIGFwcC9mcmFtZXdvcmsgdG8gYWN0dWFsbHkgZXJyb3IgaW4gY2FzZSB0aGVcbiAgICAvLyBwcm92aWRlciBmYWlscywgd2UgaW5zcGVjdCB0aGUgbG9ncyBoZXJlLlxuICAgIGV4cGVjdChvdXRwdXQpLm5vdC50b0NvbnRhaW4oJyRwcm92aWRlckVycm9yJyk7XG4gIH1cbn0pKTtcblxuaW50ZWdUZXN0KCdnZW5lcmF0aW5nIGFuZCBsb2FkaW5nIGFzc2VtYmx5Jywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IGFzbU91dHB1dERpciA9IGAke2ZpeHR1cmUuaW50ZWdUZXN0RGlyfS1jZGstaW50ZWctYXNtYDtcbiAgYXdhaXQgZml4dHVyZS5zaGVsbChbJ3JtJywgJy1yZicsIGFzbU91dHB1dERpcl0pO1xuXG4gIC8vIFN5bnRoZXNpemUgYSBDbG91ZCBBc3NlbWJseSB0b3RoZSBkZWZhdWx0IGRpcmVjdG9yeSAoY2RrLm91dCkgYW5kIGEgc3BlY2lmaWMgZGlyZWN0b3J5LlxuICBhd2FpdCBmaXh0dXJlLmNkayhbJ3N5bnRoJ10pO1xuICBhd2FpdCBmaXh0dXJlLmNkayhbJ3N5bnRoJywgJy0tb3V0cHV0JywgYXNtT3V0cHV0RGlyXSk7XG5cbiAgLy8gY2RrLm91dCBpbiB0aGUgY3VycmVudCBkaXJlY3RvcnkgYW5kIHRoZSBpbmRpY2F0ZWQgLS1vdXRwdXQgc2hvdWxkIGJlIHRoZSBzYW1lXG4gIGF3YWl0IGZpeHR1cmUuc2hlbGwoWydkaWZmJywgJ2Nkay5vdXQnLCBhc21PdXRwdXREaXJdKTtcblxuICAvLyBDaGVjayB0aGF0IHdlIGNhbiAnbHMnIHRoZSBzeW50aGVzaXplZCBhc20uXG4gIC8vIENoYW5nZSB0byBzb21lIHJhbmRvbSBkaXJlY3RvcnkgdG8gbWFrZSBzdXJlIHdlJ3JlIG5vdCBhY2NpZGVudGFsbHkgbG9hZGluZyBjZGsuanNvblxuICBjb25zdCBsaXN0ID0gYXdhaXQgZml4dHVyZS5jZGsoWyctLWFwcCcsIGFzbU91dHB1dERpciwgJ2xzJ10sIHsgY3dkOiBvcy50bXBkaXIoKSB9KTtcbiAgLy8gU2FtZSBzdGFja3Mgd2Uga25vdyBhcmUgaW4gdGhlIGFwcFxuICBleHBlY3QobGlzdCkudG9Db250YWluKGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1sYW1iZGFgKTtcbiAgZXhwZWN0KGxpc3QpLnRvQ29udGFpbihgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH0tdGVzdC0xYCk7XG4gIGV4cGVjdChsaXN0KS50b0NvbnRhaW4oYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9LXRlc3QtMmApO1xuXG4gIC8vIENoZWNrIHRoYXQgd2UgY2FuIHVzZSAnLicgYW5kIGp1c3Qgc3ludGggLHRoZSBnZW5lcmF0ZWQgYXNtXG4gIGNvbnN0IHN0YWNrVGVtcGxhdGUgPSBhd2FpdCBmaXh0dXJlLmNkayhbJy0tYXBwJywgJy4nLCAnc3ludGgnLCBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ3Rlc3QtMicpXSwge1xuICAgIGN3ZDogYXNtT3V0cHV0RGlyLFxuICB9KTtcbiAgZXhwZWN0KHN0YWNrVGVtcGxhdGUpLnRvQ29udGFpbigndG9waWMxNTJEODRBMzcnKTtcblxuICAvLyBEZXBsb3kgYSBMYW1iZGEgZnJvbSB0aGUgY29waWVkIGFzbVxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnbGFtYmRhJywgeyBvcHRpb25zOiBbJy1hJywgJy4nXSwgY3dkOiBhc21PdXRwdXREaXIgfSk7XG5cbiAgLy8gUmVtb3ZlIChyZW5hbWUpIHRoZSBvcmlnaW5hbCBjdXN0b20gZG9ja2VyIGZpbGUgdGhhdCB3YXMgdXNlZCBkdXJpbmcgc3ludGguXG4gIC8vIHRoaXMgdmVyaWZpZXMgdGhhdCB0aGUgYXNzZW1seSBoYXMgYSBjb3B5IG9mIGl0IGFuZCB0aGF0IHRoZSBtYW5pZmVzdCB1c2VzXG4gIC8vIHJlbGF0aXZlIHBhdGhzIHRvIHJlZmVyZW5jZSB0byBpdC5cbiAgY29uc3QgY3VzdG9tRG9ja2VyRmlsZSA9IHBhdGguam9pbihmaXh0dXJlLmludGVnVGVzdERpciwgJ2RvY2tlcicsICdEb2NrZXJmaWxlLkN1c3RvbScpO1xuICBhd2FpdCBmcy5yZW5hbWUoY3VzdG9tRG9ja2VyRmlsZSwgYCR7Y3VzdG9tRG9ja2VyRmlsZX1+YCk7XG4gIHRyeSB7XG5cbiAgICAvLyBkZXBsb3kgYSBkb2NrZXIgaW1hZ2Ugd2l0aCBjdXN0b20gZmlsZSB3aXRob3V0IHN5bnRoICh1c2VzIGFzc2V0cylcbiAgICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnZG9ja2VyLXdpdGgtY3VzdG9tLWZpbGUnLCB7IG9wdGlvbnM6IFsnLWEnLCAnLiddLCBjd2Q6IGFzbU91dHB1dERpciB9KTtcblxuICB9IGZpbmFsbHkge1xuICAgIC8vIFJlbmFtZSBiYWNrIHRvIHJlc3RvcmUgZml4dHVyZSB0byBvcmlnaW5hbCBzdGF0ZVxuICAgIGF3YWl0IGZzLnJlbmFtZShgJHtjdXN0b21Eb2NrZXJGaWxlfX5gLCBjdXN0b21Eb2NrZXJGaWxlKTtcbiAgfVxufSkpO1xuXG5pbnRlZ1Rlc3QoJ3RlbXBsYXRlcyBvbiBkaXNrIGNvbnRhaW4gbWV0YWRhdGEgcmVzb3VyY2UsIGFsc28gaW4gbmVzdGVkIGFzc2VtYmxpZXMnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gU3ludGggZmlyc3QsIGFuZCBzd2l0Y2ggb24gdmVyc2lvbiByZXBvcnRpbmcgYmVjYXVzZSBjZGsuanNvbiBpcyBkaXNhYmxpbmcgaXRcbiAgYXdhaXQgZml4dHVyZS5jZGsoWydzeW50aCcsICctLXZlcnNpb24tcmVwb3J0aW5nPXRydWUnXSk7XG5cbiAgLy8gTG9hZCB0ZW1wbGF0ZSBmcm9tIGRpc2sgZnJvbSByb290IGFzc2VtYmx5XG4gIGNvbnN0IHRlbXBsYXRlQ29udGVudHMgPSBhd2FpdCBmaXh0dXJlLnNoZWxsKFsnY2F0JywgJ2Nkay5vdXQvKi1sYW1iZGEudGVtcGxhdGUuanNvbiddKTtcblxuICBleHBlY3QoSlNPTi5wYXJzZSh0ZW1wbGF0ZUNvbnRlbnRzKS5SZXNvdXJjZXMuQ0RLTWV0YWRhdGEpLnRvQmVUcnV0aHkoKTtcblxuICAvLyBMb2FkIHRlbXBsYXRlIGZyb20gbmVzdGVkIGFzc2VtYmx5XG4gIGNvbnN0IG5lc3RlZFRlbXBsYXRlQ29udGVudHMgPSBhd2FpdCBmaXh0dXJlLnNoZWxsKFsnY2F0JywgJ2Nkay5vdXQvYXNzZW1ibHktKi1zdGFnZS8qLXN0YWdlLVN0YWNrSW5TdGFnZS50ZW1wbGF0ZS5qc29uJ10pO1xuXG4gIGV4cGVjdChKU09OLnBhcnNlKG5lc3RlZFRlbXBsYXRlQ29udGVudHMpLlJlc291cmNlcy5DREtNZXRhZGF0YSkudG9CZVRydXRoeSgpO1xufSkpO1xuXG5hc3luYyBmdW5jdGlvbiBsaXN0Q2hpbGRyZW4ocGFyZW50OiBzdHJpbmcsIHByZWQ6ICh4OiBzdHJpbmcpID0+IFByb21pc2U8Ym9vbGVhbj4pIHtcbiAgY29uc3QgcmV0ID0gbmV3IEFycmF5PHN0cmluZz4oKTtcbiAgZm9yIChjb25zdCBjaGlsZCBvZiBhd2FpdCBmcy5yZWFkZGlyKHBhcmVudCwgeyBlbmNvZGluZzogJ3V0Zi04JyB9KSkge1xuICAgIGNvbnN0IGZ1bGxQYXRoID0gcGF0aC5qb2luKHBhcmVudCwgY2hpbGQudG9TdHJpbmcoKSk7XG4gICAgaWYgKGF3YWl0IHByZWQoZnVsbFBhdGgpKSB7XG4gICAgICByZXQucHVzaChmdWxsUGF0aCk7XG4gICAgfVxuICB9XG4gIHJldHVybiByZXQ7XG59XG5cbmFzeW5jIGZ1bmN0aW9uIGxpc3RDaGlsZERpcnMocGFyZW50OiBzdHJpbmcpIHtcbiAgcmV0dXJuIGxpc3RDaGlsZHJlbihwYXJlbnQsIGFzeW5jIChmdWxsUGF0aDogc3RyaW5nKSA9PiAoYXdhaXQgZnMuc3RhdChmdWxsUGF0aCkpLmlzRGlyZWN0b3J5KCkpO1xufVxuXG5hc3luYyBmdW5jdGlvbiByZWFkVGVtcGxhdGUoZml4dHVyZTogVGVzdEZpeHR1cmUsIHN0YWNrTmFtZTogc3RyaW5nKTogUHJvbWlzZTxhbnk+IHtcbiAgY29uc3QgZnVsbFN0YWNrTmFtZSA9IGZpeHR1cmUuZnVsbFN0YWNrTmFtZShzdGFja05hbWUpO1xuICBjb25zdCB0ZW1wbGF0ZVBhdGggPSBwYXRoLmpvaW4oZml4dHVyZS5pbnRlZ1Rlc3REaXIsICdjZGsub3V0JywgYCR7ZnVsbFN0YWNrTmFtZX0udGVtcGxhdGUuanNvbmApO1xuICByZXR1cm4gSlNPTi5wYXJzZSgoYXdhaXQgZnMucmVhZEZpbGUodGVtcGxhdGVQYXRoLCB7IGVuY29kaW5nOiAndXRmLTgnIH0pKS50b1N0cmluZygpKTtcbn1cbiJdfQ== \ No newline at end of file From ae34d4a69a5073d8f0175b5282fa8bf92139fab5 Mon Sep 17 00:00:00 2001 From: Ken Dirschl Date: Wed, 25 Aug 2021 16:46:09 -0400 Subject: [PATCH 18/91] feat(codecommit): make Repository a source for CodeStar Notifications (#15739) ---- Fixes #15653 Notification rule can be created for CodeCommit Repository events using @aws-cdk/aws-codestarnotifications *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-codecommit/README.md | 13 + .../@aws-cdk/aws-codecommit/lib/repository.ts | 266 +++++++++++++++++- packages/@aws-cdk/aws-codecommit/package.json | 2 + ...nteg.repository-notification.expected.json | 87 ++++++ .../test/integ.repository-notification.ts | 19 ++ .../test/test.notification-rule.ts | 66 +++++ .../lib/notification-rule.ts | 2 +- .../aws-codestarnotifications/test/helpers.ts | 11 + .../test/notification-rule.test.ts | 21 ++ 9 files changed, 476 insertions(+), 11 deletions(-) create mode 100644 packages/@aws-cdk/aws-codecommit/test/integ.repository-notification.expected.json create mode 100644 packages/@aws-cdk/aws-codecommit/test/integ.repository-notification.ts create mode 100644 packages/@aws-cdk/aws-codecommit/test/test.notification-rule.ts diff --git a/packages/@aws-cdk/aws-codecommit/README.md b/packages/@aws-cdk/aws-codecommit/README.md index 3d4ef1c3ba46c..7dd08eef3bb82 100644 --- a/packages/@aws-cdk/aws-codecommit/README.md +++ b/packages/@aws-cdk/aws-codecommit/README.md @@ -55,3 +55,16 @@ const rule = repo.onCommentOnPullRequest('CommentOnPullRequest', { target: new targets.SnsTopic(myTopic), }); ``` + +## CodeStar Notifications + +To define CodeStar Notification rules for Repositories, use one of the `notifyOnXxx()` methods. +They are very similar to `onXxx()` methods for CloudWatch events: + +```ts +const target = new chatbot.SlackChannelConfiguration(stack, 'MySlackChannel', { + slackChannelConfigurationName: 'YOUR_CHANNEL_NAME', + slackWorkspaceId: 'YOUR_SLACK_WORKSPACE_ID', + slackChannelId: 'YOUR_SLACK_CHANNEL_ID', +}); +const rule = repository.notifyOnPullRequestCreated('NotifyOnPullRequestCreated', target); diff --git a/packages/@aws-cdk/aws-codecommit/lib/repository.ts b/packages/@aws-cdk/aws-codecommit/lib/repository.ts index 495be2f981d86..e55053c0249be 100644 --- a/packages/@aws-cdk/aws-codecommit/lib/repository.ts +++ b/packages/@aws-cdk/aws-codecommit/lib/repository.ts @@ -1,10 +1,23 @@ +import * as notifications from '@aws-cdk/aws-codestarnotifications'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import { IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnRepository } from './codecommit.generated'; -export interface IRepository extends IResource { +/** + * Additional options to pass to the notification rule. + */ +export interface RepositoryNotifyOnOptions extends notifications.NotificationRuleOptions { + /** + * A list of event types associated with this notification rule for CodeCommit repositories. + * For a complete list of event types and IDs, see Notification concepts in the Developer Tools Console User Guide. + * @see https://docs.aws.amazon.com/dtconsole/latest/userguide/concepts.html#concepts-api + */ + readonly events: RepositoryNotificationEvents[]; +} + +export interface IRepository extends IResource, notifications.INotificationRuleSource { /** * The ARN of this Repository. * @attribute @@ -18,19 +31,19 @@ export interface IRepository extends IResource { readonly repositoryName: string; /** - * The HTTP clone URL + * The HTTP clone URL. * @attribute */ readonly repositoryCloneUrlHttp: string; /** - * The SSH clone URL + * The SSH clone URL. * @attribute */ readonly repositoryCloneUrlSsh: string; /** - * The HTTPS (GRC) clone URL + * The HTTPS (GRC) clone URL. * * HTTPS (GRC) is the protocol to use with git-remote-codecommit (GRC). * @@ -92,7 +105,7 @@ export interface IRepository extends IResource { onCommit(id: string, options?: OnCommitOptions): events.Rule; /** - * Grant the given principal identity permissions to perform the actions on this repository + * Grant the given principal identity permissions to perform the actions on this repository. */ grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant; @@ -110,13 +123,90 @@ export interface IRepository extends IResource { * Grant the given identity permissions to read this repository. */ grantRead(grantee: iam.IGrantable): iam.Grant; + + /** + * Defines a CodeStar Notification rule triggered when the project + * events specified by you are emitted. Similar to `onEvent` API. + * + * You can also use the methods to define rules for the specific event emitted. + * eg: `notifyOnPullRequstCreated`. + * + * @returns CodeStar Notifications rule associated with this repository. + */ + notifyOn( + id: string, + target: notifications.INotificationRuleTarget, + options: RepositoryNotifyOnOptions, + ): notifications.INotificationRule; + + /** + * Defines a CodeStar Notification rule which triggers when a comment is made on a pull request. + */ + notifyOnPullRequestComment( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule; + + /** + * Defines a CodeStar Notification rule which triggers when an approval status is changed. + */ + notifyOnApprovalStatusChanged( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule; + + /** + * Defines a CodeStar Notification rule which triggers when an approval rule is overridden. + */ + notifyOnApprovalRuleOverridden( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule; + + /** + * Defines a CodeStar Notification rule which triggers when a pull request is created. + */ + notifyOnPullRequestCreated( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule; + + /** + * Defines a CodeStar Notification rule which triggers when a pull request is merged. + */ + notifiyOnPullRequestMerged( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule; + + /** + * Defines a CodeStar Notification rule which triggers when a new branch or tag is created. + */ + notifyOnBranchOrTagCreated( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule; + + /** + * Defines a CodeStar Notification rule which triggers when a branch or tag is deleted. + */ + notifyOnBranchOrTagDeleted( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule; } /** - * Options for the onCommit() method + * Options for the onCommit() method. */ export interface OnCommitOptions extends events.OnEventOptions { - /** * The branch to monitor. * @@ -268,6 +358,101 @@ abstract class RepositoryBase extends Resource implements IRepository { 'codecommit:Describe*', ); } + + public notifyOn( + id: string, + target: notifications.INotificationRuleTarget, + options: RepositoryNotifyOnOptions, + ): notifications.INotificationRule { + return new notifications.NotificationRule(this, id, { + ...options, + source: this, + targets: [target], + }); + } + + public notifyOnPullRequestComment( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule { + return this.notifyOn(id, target, { + ...options, + events: [RepositoryNotificationEvents.PULL_REQUEST_COMMENT], + }); + } + + public notifyOnApprovalStatusChanged( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule { + return this.notifyOn(id, target, { + ...options, + events: [RepositoryNotificationEvents.APPROVAL_STATUS_CHANGED], + }); + } + + public notifyOnApprovalRuleOverridden( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule { + return this.notifyOn(id, target, { + ...options, + events: [RepositoryNotificationEvents.APPROVAL_RULE_OVERRIDDEN], + }); + } + + public notifyOnPullRequestCreated( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule { + return this.notifyOn(id, target, { + ...options, + events: [RepositoryNotificationEvents.PULL_REQUEST_CREATED], + }); + } + + public notifiyOnPullRequestMerged( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule { + return this.notifyOn(id, target, { + ...options, + events: [RepositoryNotificationEvents.PULL_REQUEST_MERGED], + }); + } + + public notifyOnBranchOrTagCreated( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule { + return this.notifyOn(id, target, { + ...options, + events: [RepositoryNotificationEvents.BRANCH_OR_TAG_CREATED], + }); + } + + public notifyOnBranchOrTagDeleted( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule { + return this.notifyOn(id, target, { + ...options, + events: [RepositoryNotificationEvents.BRANCH_OR_TAG_DELETED], + }); + } + + public bindAsNotificationRuleSource(_scope: Construct): notifications.NotificationRuleSourceConfig { + return { + sourceArn: this.repositoryArn, + }; + } } export interface RepositoryProps { @@ -288,7 +473,7 @@ export interface RepositoryProps { } /** - * Provides a CodeCommit Repository + * Provides a CodeCommit Repository. */ export class Repository extends RepositoryBase { @@ -401,7 +586,7 @@ export class Repository extends RepositoryBase { */ export interface RepositoryTriggerOptions { /** - * A name for the trigger.Triggers on a repository must have unique names + * A name for the trigger.Triggers on a repository must have unique names. */ readonly name?: string; @@ -437,7 +622,7 @@ export enum RepositoryEventTrigger { } /** - * Returns the clone URL for a protocol + * Returns the clone URL for a protocol. */ function makeCloneUrl(stack: Stack, repositoryName: string, protocol: 'https' | 'ssh' | 'grc', region?: string) { switch (protocol) { @@ -448,3 +633,64 @@ function makeCloneUrl(stack: Stack, repositoryName: string, protocol: 'https' | return `codecommit::${region ?? stack.region}://${repositoryName}`; } } + +/** + * List of event types for AWS CodeCommit + * @see https://docs.aws.amazon.com/dtconsole/latest/userguide/concepts.html#events-ref-repositories + */ +export enum RepositoryNotificationEvents { + /** + * Trigger notication when comment made on commit. + */ + COMMIT_COMMENT = 'codecommit-repository-comments-on-commits', + + /** + * Trigger notification when comment made on pull request. + */ + PULL_REQUEST_COMMENT = 'codecommit-repository-comments-on-pull-requests', + + /** + * Trigger notification when approval status changed. + */ + APPROVAL_STATUS_CHANGED = 'codecommit-repository-approvals-status-changed', + + /** + * Trigger notifications when approval rule is overridden. + */ + APPROVAL_RULE_OVERRIDDEN = 'codecommit-repository-approvals-rule-override', + + /** + * Trigger notification when pull request created. + */ + PULL_REQUEST_CREATED = 'codecommit-repository-pull-request-created', + + /** + * Trigger notification when pull request source updated. + */ + PULL_REQUEST_SOURCE_UPDATED = 'codecommit-repository-pull-request-source-updated', + + /** + * Trigger notification when pull request status is changed. + */ + PULL_REQUEST_STATUS_CHANGED = 'codecommit-repository-pull-request-status-changed', + + /** + * Trigger notification when pull requset is merged. + */ + PULL_REQUEST_MERGED = 'codecommit-repository-pull-request-merged', + + /** + * Trigger notification when a branch or tag is created. + */ + BRANCH_OR_TAG_CREATED = 'codecommit-repository-branches-and-tags-created', + + /** + * Trigger notification when a branch or tag is deleted. + */ + BRANCH_OR_TAG_DELETED = 'codecommit-repository-branches-and-tags-deleted', + + /** + * Trigger notification when a branch or tag is updated. + */ + BRANCH_OR_TAG_UPDATED = 'codecommit-repository-branches-and-tags-updated', +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codecommit/package.json b/packages/@aws-cdk/aws-codecommit/package.json index 6d622f85d076a..325c75e846904 100644 --- a/packages/@aws-cdk/aws-codecommit/package.json +++ b/packages/@aws-cdk/aws-codecommit/package.json @@ -88,6 +88,7 @@ "@aws-cdk/assert-internal": "0.0.0" }, "dependencies": { + "@aws-cdk/aws-codestarnotifications": "0.0.0", "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/core": "0.0.0", @@ -95,6 +96,7 @@ }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { + "@aws-cdk/aws-codestarnotifications": "0.0.0", "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-codecommit/test/integ.repository-notification.expected.json b/packages/@aws-cdk/aws-codecommit/test/integ.repository-notification.expected.json new file mode 100644 index 0000000000000..c2f386ccebe0e --- /dev/null +++ b/packages/@aws-cdk/aws-codecommit/test/integ.repository-notification.expected.json @@ -0,0 +1,87 @@ +{ + "Resources": { + "MyCodecommitRepository26DB372B": { + "Type": "AWS::CodeCommit::Repository", + "Properties": { + "RepositoryName": "my-test-repository" + } + }, + "MyCodecommitRepositoryNotifyOnPullRequestCreated4CAB0621": { + "Type": "AWS::CodeStarNotifications::NotificationRule", + "Properties": { + "DetailType": "FULL", + "EventTypeIds": [ + "codecommit-repository-pull-request-created" + ], + "Name": "decommitMyCodecommitRepositoryNotifyOnPullRequestCreated65969BCB", + "Resource": { + "Fn::GetAtt": [ + "MyCodecommitRepository26DB372B", + "Arn" + ] + }, + "Targets": [ + { + "TargetAddress": { + "Ref": "MyTopic86869434" + }, + "TargetType": "SNS" + } + ] + } + }, + "MyCodecommitRepositoryNotifyOnPullRequestMerged80574FED": { + "Type": "AWS::CodeStarNotifications::NotificationRule", + "Properties": { + "DetailType": "FULL", + "EventTypeIds": [ + "codecommit-repository-pull-request-merged" + ], + "Name": "odecommitMyCodecommitRepositoryNotifyOnPullRequestMergedF426197C", + "Resource": { + "Fn::GetAtt": [ + "MyCodecommitRepository26DB372B", + "Arn" + ] + }, + "Targets": [ + { + "TargetAddress": { + "Ref": "MyTopic86869434" + }, + "TargetType": "SNS" + } + ] + } + }, + "MyTopic86869434": { + "Type": "AWS::SNS::Topic" + }, + "MyTopicPolicy12A5EC17": { + "Type": "AWS::SNS::TopicPolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Principal": { + "Service": "codestar-notifications.amazonaws.com" + }, + "Resource": { + "Ref": "MyTopic86869434" + }, + "Sid": "0" + } + ], + "Version": "2012-10-17" + }, + "Topics": [ + { + "Ref": "MyTopic86869434" + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codecommit/test/integ.repository-notification.ts b/packages/@aws-cdk/aws-codecommit/test/integ.repository-notification.ts new file mode 100644 index 0000000000000..4f7a5926a47ac --- /dev/null +++ b/packages/@aws-cdk/aws-codecommit/test/integ.repository-notification.ts @@ -0,0 +1,19 @@ +#!/usr/bin/env node +import * as sns from '@aws-cdk/aws-sns'; +import * as cdk from '@aws-cdk/core'; +import * as codecommit from '../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-codecommit'); + +const repository = new codecommit.Repository(stack, 'MyCodecommitRepository', { + repositoryName: 'my-test-repository', +}); + +const target = new sns.Topic(stack, 'MyTopic'); + +repository.notifyOnPullRequestCreated('NotifyOnPullRequestCreated', target); +repository.notifiyOnPullRequestMerged('NotifyOnPullRequestMerged', target); + +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codecommit/test/test.notification-rule.ts b/packages/@aws-cdk/aws-codecommit/test/test.notification-rule.ts new file mode 100644 index 0000000000000..aa0e48a51a3ac --- /dev/null +++ b/packages/@aws-cdk/aws-codecommit/test/test.notification-rule.ts @@ -0,0 +1,66 @@ +import { expect, haveResource } from '@aws-cdk/assert-internal'; +import * as sns from '@aws-cdk/aws-sns'; +import * as cdk from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import * as codecommit from '../lib'; + +export = { + 'CodeCommit Repositories - can create notification rule'(test: Test) { + const stack = new cdk.Stack(); + const repository = new codecommit.Repository(stack, 'MyCodecommitRepository', { + repositoryName: 'my-test-repository', + }); + + const target = new sns.Topic(stack, 'MyTopic'); + + repository.notifyOnPullRequestCreated('NotifyOnPullRequestCreated', target); + + repository.notifiyOnPullRequestMerged('NotifyOnPullRequestMerged', target); + + expect(stack).to(haveResource('AWS::CodeStarNotifications::NotificationRule', { + Name: 'MyCodecommitRepositoryNotifyOnPullRequestCreatedBB14EA32', + DetailType: 'FULL', + EventTypeIds: [ + 'codecommit-repository-pull-request-created', + ], + Resource: { + 'Fn::GetAtt': [ + 'MyCodecommitRepository26DB372B', + 'Arn', + ], + }, + Targets: [ + { + TargetAddress: { + Ref: 'MyTopic86869434', + }, + TargetType: 'SNS', + }, + ], + })); + + expect(stack).to(haveResource('AWS::CodeStarNotifications::NotificationRule', { + Name: 'MyCodecommitRepositoryNotifyOnPullRequestMerged34A7EDF1', + DetailType: 'FULL', + EventTypeIds: [ + 'codecommit-repository-pull-request-merged', + ], + Resource: { + 'Fn::GetAtt': [ + 'MyCodecommitRepository26DB372B', + 'Arn', + ], + }, + Targets: [ + { + TargetAddress: { + Ref: 'MyTopic86869434', + }, + TargetType: 'SNS', + }, + ], + })); + + test.done(); + }, +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codestarnotifications/lib/notification-rule.ts b/packages/@aws-cdk/aws-codestarnotifications/lib/notification-rule.ts index 0796557e38a77..f30cf11f58a99 100644 --- a/packages/@aws-cdk/aws-codestarnotifications/lib/notification-rule.ts +++ b/packages/@aws-cdk/aws-codestarnotifications/lib/notification-rule.ts @@ -62,7 +62,7 @@ export interface NotificationRuleProps extends NotificationRuleOptions { /** * The Amazon Resource Name (ARN) of the resource to associate with the notification rule. - * Currently, Supported sources include pipelines in AWS CodePipeline and build projects in AWS CodeBuild in this L2 constructor. + * Currently, Supported sources include pipelines in AWS CodePipeline, build projects in AWS CodeBuild, and repositories in AWS CodeCommit in this L2 constructor. * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codestarnotifications-notificationrule.html#cfn-codestarnotifications-notificationrule-resource */ readonly source: INotificationRuleSource; diff --git a/packages/@aws-cdk/aws-codestarnotifications/test/helpers.ts b/packages/@aws-cdk/aws-codestarnotifications/test/helpers.ts index bf1d335c94d9f..3acf5e33ca334 100644 --- a/packages/@aws-cdk/aws-codestarnotifications/test/helpers.ts +++ b/packages/@aws-cdk/aws-codestarnotifications/test/helpers.ts @@ -22,6 +22,17 @@ export class FakeCodePipeline implements notifications.INotificationRuleSource { } } +export class FakeCodeCommit implements notifications.INotificationRuleSource { + readonly repositoryArn = 'arn:aws:codecommit::1234567890:MyCodecommitProject'; + readonly repositoryName = 'test-repository'; + + bindAsNotificationRuleSource(): notifications.NotificationRuleSourceConfig { + return { + sourceArn: this.repositoryArn, + }; + } +} + export class FakeSnsTopicTarget implements notifications.INotificationRuleTarget { readonly topicArn = 'arn:aws:sns::1234567890:MyTopic'; diff --git a/packages/@aws-cdk/aws-codestarnotifications/test/notification-rule.test.ts b/packages/@aws-cdk/aws-codestarnotifications/test/notification-rule.test.ts index a155b3583a524..332e808ca3176 100644 --- a/packages/@aws-cdk/aws-codestarnotifications/test/notification-rule.test.ts +++ b/packages/@aws-cdk/aws-codestarnotifications/test/notification-rule.test.ts @@ -4,6 +4,7 @@ import * as notifications from '../lib'; import { FakeCodeBuild, FakeCodePipeline, + FakeCodeCommit, FakeSlackTarget, FakeSnsTopicTarget, } from './helpers'; @@ -29,6 +30,26 @@ describe('NotificationRule', () => { }); }); + test('created new notification rule from repository source', () => { + const repository = new FakeCodeCommit(); + + new notifications.NotificationRule(stack, 'MyNotificationRule', { + source: repository, + events: [ + 'codecommit-repository-pull-request-created', + 'codecommit-repository-pull-request-merged', + ], + }); + + expect(stack).toHaveResourceLike('AWS::CodeStarNotifications::NotificationRule', { + Resource: repository.repositoryArn, + EventTypeIds: [ + 'codecommit-repository-pull-request-created', + 'codecommit-repository-pull-request-merged', + ], + }); + }); + test('created new notification rule with all parameters in constructor props', () => { const project = new FakeCodeBuild(); const slack = new FakeSlackTarget(); From 1d54a456cd5e2ff65251097f9a684e1ac200cc52 Mon Sep 17 00:00:00 2001 From: kaizen3031593 <36202692+kaizen3031593@users.noreply.github.com> Date: Wed, 25 Aug 2021 17:33:23 -0400 Subject: [PATCH 19/91] feat(rds): support 's3export' for Postgres database instances (#16124) As mentioned in #14546, Postgres database instances did not support `s3export` when most of the `s3 import/export` features were added in #10370. This PR fills in that gap now that `s3export` is [available](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/postgresql-s3-export.html) for Postgres. The supported versions are documented [here](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/postgresql-s3-export.html) and I manually verified via AWS CLI that Postgres 13+ supports `s3export` as well. Looking into the test suite, I think it makes more sense to modify the existing test than to create an entirely new one, similar to how other database instances test s3 import/export at the same time. But I can also move it to its own test if necessary. I also fixed a very minor doc typo in `cluster-instance.ts`. Closes #14546. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-rds/README.md | 7 ++-- .../@aws-cdk/aws-rds/lib/cluster-engine.ts | 2 +- .../@aws-cdk/aws-rds/lib/instance-engine.ts | 40 +++++++++++-------- .../aws-rds/test/instance-engine.test.ts | 6 +-- 4 files changed, 32 insertions(+), 23 deletions(-) diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index c604a2ad5bce8..054a660f3529e 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -359,11 +359,12 @@ You can read more about loading data to (or from) S3 here: * Aurora MySQL - [import](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Integrating.LoadFromS3.html) and [export](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Integrating.SaveIntoS3.html). -* Aurora PostgreSQL - [import](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraPostgreSQL.Migrating.html) +* Aurora PostgreSQL - [import](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraPostgreSQL.Migrating.html#USER_PostgreSQL.S3Import) and [export](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/postgresql-s3-export.html). -* Microsoft SQL Server - [import & export](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/SQLServer.Procedural.Importing.html) +* Microsoft SQL Server - [import and export](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/SQLServer.Procedural.Importing.html) * PostgreSQL - [import](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/PostgreSQL.Procedural.Importing.html) -* Oracle - [import & export](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/oracle-s3-integration.html) + and [export](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/postgresql-s3-export.html) +* Oracle - [import and export](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/oracle-s3-integration.html) The following snippet sets up a database cluster with different S3 buckets where the data is imported and exported - diff --git a/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts b/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts index f92e738b2f90b..973f682e932c7 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts @@ -403,7 +403,7 @@ export interface AuroraPostgresEngineFeatures { readonly s3Import?: boolean; /** - * Whether this version of the Aurora Postgres cluster engine supports the S3 data import feature. + * Whether this version of the Aurora Postgres cluster engine supports the S3 data export feature. * * @default false */ diff --git a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts index 7b1c8fad83eaf..eacb8097da55e 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts @@ -545,6 +545,13 @@ export interface PostgresEngineFeatures { * @default false */ readonly s3Import?: boolean; + + /** + * Whether this version of the Postgres engine supports the S3 data export feature. + * + * @default false + */ + readonly s3Export?: boolean; } /** @@ -779,13 +786,13 @@ export class PostgresEngineVersion { /** Version "10.13". */ public static readonly VER_10_13 = PostgresEngineVersion.of('10.13', '10', { s3Import: true }); /** Version "10.14". */ - public static readonly VER_10_14 = PostgresEngineVersion.of('10.14', '10', { s3Import: true }); + public static readonly VER_10_14 = PostgresEngineVersion.of('10.14', '10', { s3Import: true, s3Export: true }); /** Version "10.15". */ - public static readonly VER_10_15 = PostgresEngineVersion.of('10.15', '10', { s3Import: true }); + public static readonly VER_10_15 = PostgresEngineVersion.of('10.15', '10', { s3Import: true, s3Export: true }); /** Version "10.16". */ - public static readonly VER_10_16 = PostgresEngineVersion.of('10.16', '10', { s3Import: true }); + public static readonly VER_10_16 = PostgresEngineVersion.of('10.16', '10', { s3Import: true, s3Export: true }); /** Version "10.17". */ - public static readonly VER_10_17 = PostgresEngineVersion.of('10.17', '10', { s3Import: true }); + public static readonly VER_10_17 = PostgresEngineVersion.of('10.17', '10', { s3Import: true, s3Export: true }); /** Version "11" (only a major version, without a specific minor version). */ public static readonly VER_11 = PostgresEngineVersion.of('11', '11', { s3Import: true }); @@ -804,13 +811,13 @@ export class PostgresEngineVersion { /** Version "11.8". */ public static readonly VER_11_8 = PostgresEngineVersion.of('11.8', '11', { s3Import: true }); /** Version "11.9". */ - public static readonly VER_11_9 = PostgresEngineVersion.of('11.9', '11', { s3Import: true }); + public static readonly VER_11_9 = PostgresEngineVersion.of('11.9', '11', { s3Import: true, s3Export: true }); /** Version "11.10". */ - public static readonly VER_11_10 = PostgresEngineVersion.of('11.10', '11', { s3Import: true }); + public static readonly VER_11_10 = PostgresEngineVersion.of('11.10', '11', { s3Import: true, s3Export: true }); /** Version "11.11". */ - public static readonly VER_11_11 = PostgresEngineVersion.of('11.11', '11', { s3Import: true }); + public static readonly VER_11_11 = PostgresEngineVersion.of('11.11', '11', { s3Import: true, s3Export: true }); /** Version "11.12". */ - public static readonly VER_11_12 = PostgresEngineVersion.of('11.12', '11', { s3Import: true }); + public static readonly VER_11_12 = PostgresEngineVersion.of('11.12', '11', { s3Import: true, s3Export: true }); /** Version "12" (only a major version, without a specific minor version). */ public static readonly VER_12 = PostgresEngineVersion.of('12', '12', { s3Import: true }); @@ -819,22 +826,22 @@ export class PostgresEngineVersion { /** Version "12.3". */ public static readonly VER_12_3 = PostgresEngineVersion.of('12.3', '12', { s3Import: true }); /** Version "12.4". */ - public static readonly VER_12_4 = PostgresEngineVersion.of('12.4', '12', { s3Import: true }); + public static readonly VER_12_4 = PostgresEngineVersion.of('12.4', '12', { s3Import: true, s3Export: true }); /** Version "12.5". */ - public static readonly VER_12_5 = PostgresEngineVersion.of('12.5', '12', { s3Import: true }); + public static readonly VER_12_5 = PostgresEngineVersion.of('12.5', '12', { s3Import: true, s3Export: true }); /** Version "12.6". */ - public static readonly VER_12_6 = PostgresEngineVersion.of('12.6', '12', { s3Import: true }); + public static readonly VER_12_6 = PostgresEngineVersion.of('12.6', '12', { s3Import: true, s3Export: true }); /** Version "12.7". */ - public static readonly VER_12_7 = PostgresEngineVersion.of('12.7', '12', { s3Import: true }); + public static readonly VER_12_7 = PostgresEngineVersion.of('12.7', '12', { s3Import: true, s3Export: true }); /** Version "13" (only a major version, without a specific minor version). */ - public static readonly VER_13 = PostgresEngineVersion.of('13', '13', { s3Import: true }); + public static readonly VER_13 = PostgresEngineVersion.of('13', '13', { s3Import: true, s3Export: true }); /** Version "13.1". */ - public static readonly VER_13_1 = PostgresEngineVersion.of('13.1', '13', { s3Import: true }); + public static readonly VER_13_1 = PostgresEngineVersion.of('13.1', '13', { s3Import: true, s3Export: true }); /** Version "13.2". */ - public static readonly VER_13_2 = PostgresEngineVersion.of('13.2', '13', { s3Import: true }); + public static readonly VER_13_2 = PostgresEngineVersion.of('13.2', '13', { s3Import: true, s3Export: true }); /** Version "13.3". */ - public static readonly VER_13_3 = PostgresEngineVersion.of('13.3', '13', { s3Import: true }); + public static readonly VER_13_3 = PostgresEngineVersion.of('13.3', '13', { s3Import: true, s3Export: true }); /** * Create a new PostgresEngineVersion with an arbitrary version. @@ -864,6 +871,7 @@ export class PostgresEngineVersion { this.postgresMajorVersion = postgresMajorVersion; this._features = { s3Import: postgresFeatures?.s3Import ? 's3Import' : undefined, + s3Export: postgresFeatures?.s3Export ? 's3Export' : undefined, }; } } diff --git a/packages/@aws-cdk/aws-rds/test/instance-engine.test.ts b/packages/@aws-cdk/aws-rds/test/instance-engine.test.ts index 830ad0c97bd8e..e3b02c48770d4 100644 --- a/packages/@aws-cdk/aws-rds/test/instance-engine.test.ts +++ b/packages/@aws-cdk/aws-rds/test/instance-engine.test.ts @@ -274,12 +274,12 @@ nodeunitShim({ test.done(); }, - 'returns s3 import feature if the version supports it'(test: Test) { - const engineNewerVersion = rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_12_3 }); + 'returns s3 import/export feature if the version supports it'(test: Test) { + const engineNewerVersion = rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_13_3 }); const engineConfig = engineNewerVersion.bindToInstance(new cdk.Stack(), {}); test.equals(engineConfig.features?.s3Import, 's3Import'); - test.equals(engineConfig.features?.s3Export, undefined); + test.equals(engineConfig.features?.s3Export, 's3Export'); test.done(); }, From cd66fe4b101d7a377ceb733fe4d72d4cd6cec753 Mon Sep 17 00:00:00 2001 From: AWS CDK Team Date: Thu, 26 Aug 2021 09:31:38 +0000 Subject: [PATCH 20/91] chore(release): 1.120.0 --- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++++++++ version.v1.json | 2 +- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c207fc6012f7c..a17afe6a8cc59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,47 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.120.0](https://github.com/aws/aws-cdk/compare/v1.119.0...v1.120.0) (2021-08-26) + + +### Features + +* **assertions:** queries and assertions against the Outputs and Mappings sections ([#15892](https://github.com/aws/aws-cdk/issues/15892)) ([90f95e1](https://github.com/aws/aws-cdk/commit/90f95e10f4dd9e4992731d6262dcfc767b65ab3f)) +* **aws-stepfunctions:** add support to heartbeat error inside catch block ([#16078](https://github.com/aws/aws-cdk/issues/16078)) ([2372b3c](https://github.com/aws/aws-cdk/commit/2372b3c360d13fb0224fc981a7bb1ae318581265)), closes [#16084](https://github.com/aws/aws-cdk/issues/16084) +* **cfnspec:** cloudformation spec v39.10.0 ([#16114](https://github.com/aws/aws-cdk/issues/16114)) ([7e0ad5d](https://github.com/aws/aws-cdk/commit/7e0ad5d17b30150922d0dfd81f42da11fadb8beb)) +* **cfnspec:** cloudformation spec v40.0.0 ([#16183](https://github.com/aws/aws-cdk/issues/16183)) ([b059124](https://github.com/aws/aws-cdk/commit/b059124b238e27751217cbdaaa01c38b00e80fc9)) +* **cloudwatch:** add support for cross-account alarms ([#16007](https://github.com/aws/aws-cdk/issues/16007)) ([e547ba0](https://github.com/aws/aws-cdk/commit/e547ba0d1491af0abe703132fa06fe786ffd7070)), closes [#15959](https://github.com/aws/aws-cdk/issues/15959) +* **codecommit:** make Repository a source for CodeStar Notifications ([#15739](https://github.com/aws/aws-cdk/issues/15739)) ([ae34d4a](https://github.com/aws/aws-cdk/commit/ae34d4a69a5073d8f0175b5282fa8bf92139fab5)) +* **cognito:** user pools - device tracking ([#16055](https://github.com/aws/aws-cdk/issues/16055)) ([64019bb](https://github.com/aws/aws-cdk/commit/64019bbf090e156261feb626a5a4bd7ff4f26545)), closes [#15013](https://github.com/aws/aws-cdk/issues/15013) +* **docdb:** cluster - deletion protection ([#15216](https://github.com/aws/aws-cdk/issues/15216)) ([0f7beb2](https://github.com/aws/aws-cdk/commit/0f7beb29be18d809052f4d46e415a0394c9299ab)) +* **ecs:** add support for Bottlerocket on ARM64 ([#15454](https://github.com/aws/aws-cdk/issues/15454)) ([cd280a8](https://github.com/aws/aws-cdk/commit/cd280a8f4f46eb50be3a25d80c00a807881832c4)), closes [#14466](https://github.com/aws/aws-cdk/issues/14466) +* **lambda:** nodejs14.x supports inline code ([#16131](https://github.com/aws/aws-cdk/issues/16131)) ([305f683](https://github.com/aws/aws-cdk/commit/305f683e86cca221705c0138572faa38043396eb)) +* **rds:** support 's3export' for Postgres database instances ([#16124](https://github.com/aws/aws-cdk/issues/16124)) ([1d54a45](https://github.com/aws/aws-cdk/commit/1d54a456cd5e2ff65251097f9a684e1ac200cc52)), closes [#14546](https://github.com/aws/aws-cdk/issues/14546) [#10370](https://github.com/aws/aws-cdk/issues/10370) [#14546](https://github.com/aws/aws-cdk/issues/14546) +* **s3-deployment:** exclude and include filters ([#16054](https://github.com/aws/aws-cdk/issues/16054)) ([d42e89e](https://github.com/aws/aws-cdk/commit/d42e89e01034dcba08c8f8ac0390a743143c4531)), closes [#14362](https://github.com/aws/aws-cdk/issues/14362) [#14362](https://github.com/aws/aws-cdk/issues/14362) + + +### Bug Fixes + +* **apigatewayv2:** http api - disallow empty string as domain name ([#16044](https://github.com/aws/aws-cdk/issues/16044)) ([9c39bcb](https://github.com/aws/aws-cdk/commit/9c39bcb970fc791e94d199b962cc006fca1a3320)) +* **appsync:** addSubscription only allows for field type ([#16097](https://github.com/aws/aws-cdk/issues/16097)) ([000d151](https://github.com/aws/aws-cdk/commit/000d151bec2215aa530819c3cf2c8c432352fec3)), closes [#10078](https://github.com/aws/aws-cdk/issues/10078) [#16071](https://github.com/aws/aws-cdk/issues/16071) +* **cfnspec:** changes to resource-level documentation not supported ([#16170](https://github.com/aws/aws-cdk/issues/16170)) ([82e4b4f](https://github.com/aws/aws-cdk/commit/82e4b4f07be202e2d6c6afa4f9ed0d9d6146f0a8)) +* **cli:** 'deploy' and 'diff' silently does nothing when given unknown stack name ([#16073](https://github.com/aws/aws-cdk/issues/16073)) ([f35b032](https://github.com/aws/aws-cdk/commit/f35b032cea4354992d3320e78c1ed0e2878a3fe7)), closes [#15866](https://github.com/aws/aws-cdk/issues/15866) +* **cli:** Python init template does not work in directory with '-' ([#15939](https://github.com/aws/aws-cdk/issues/15939)) ([3b2c790](https://github.com/aws/aws-cdk/commit/3b2c790c2b7d210868576540feab4e088376ab6c)), closes [#15938](https://github.com/aws/aws-cdk/issues/15938) +* **cli:** unknown command pytest in build container fails integration tests ([#16134](https://github.com/aws/aws-cdk/issues/16134)) ([0f7c0b4](https://github.com/aws/aws-cdk/commit/0f7c0b421327f1ffed28de79692191af187f23ca)), closes [#15939](https://github.com/aws/aws-cdk/issues/15939) +* **resourcegroups:** ResourceGroup not using TagType.STANDARD, causes deploy failure ([#16211](https://github.com/aws/aws-cdk/issues/16211)) ([cdee1af](https://github.com/aws/aws-cdk/commit/cdee1af03c34a1c08988e672bae6edc2538a8877)), closes [#12986](https://github.com/aws/aws-cdk/issues/12986) +* **s3:** bucket is not emptied before update when the name changes ([#16203](https://github.com/aws/aws-cdk/issues/16203)) ([b1d69d7](https://github.com/aws/aws-cdk/commit/b1d69d7b06cd2a2ae8f578e217bdf7fef50a0163)), closes [#14011](https://github.com/aws/aws-cdk/issues/14011) +* **ses:** drop spam rule appears in the incorrect order ([#16146](https://github.com/aws/aws-cdk/issues/16146)) ([677fedc](https://github.com/aws/aws-cdk/commit/677fedcc5351b8b5346970fac03e5e342f36265b)), closes [#16091](https://github.com/aws/aws-cdk/issues/16091) +* **sqs:** unable to import a FIFO queue when the queue ARN is a token ([#15976](https://github.com/aws/aws-cdk/issues/15976)) ([a1a65bc](https://github.com/aws/aws-cdk/commit/a1a65bc9a38b06ec51dff462e52b1beb8d421a56)), closes [#12466](https://github.com/aws/aws-cdk/issues/12466) +* **ssm:** StringParameter.fromStringParameterAttributes cannot accept version as a numeric Token ([#16048](https://github.com/aws/aws-cdk/issues/16048)) ([eb54cd4](https://github.com/aws/aws-cdk/commit/eb54cd416a48708898e30986058491e21125b2f7)), closes [#11913](https://github.com/aws/aws-cdk/issues/11913) +* (aws-ec2): fix vpc endpoint incorrect issue in China region ([#16139](https://github.com/aws/aws-cdk/issues/16139)) ([0d0db38](https://github.com/aws/aws-cdk/commit/0d0db38e3cdb557b4a641c5993068400847cc7df)), closes [#9864](https://github.com/aws/aws-cdk/issues/9864) +* KubectlHandler - insecure kubeconfig warning ([#16063](https://github.com/aws/aws-cdk/issues/16063)) ([82dd282](https://github.com/aws/aws-cdk/commit/82dd2822a86431d0aa0be896550d421810b80c67)), closes [#14560](https://github.com/aws/aws-cdk/issues/14560) + + +### Reverts + +* **cli:** 'deploy' and 'diff' silently does nothing when given unknown stack name ([#16125](https://github.com/aws/aws-cdk/issues/16125)) ([f2d77d3](https://github.com/aws/aws-cdk/commit/f2d77d336d535ef718813b4ed6b88b5d2af05cb9)), closes [aws/aws-cdk#16073](https://github.com/aws/aws-cdk/issues/16073) +* temporarily transfer [@skinny85](https://github.com/skinny85) module ownership ([#16206](https://github.com/aws/aws-cdk/issues/16206)) ([e678f10](https://github.com/aws/aws-cdk/commit/e678f104df4fb0377c6ad5c8abc4132433363871)) + ## [1.119.0](https://github.com/aws/aws-cdk/compare/v1.118.0...v1.119.0) (2021-08-17) diff --git a/version.v1.json b/version.v1.json index e02425c0f7b3d..7ff9a577d56ba 100644 --- a/version.v1.json +++ b/version.v1.json @@ -1,3 +1,3 @@ { - "version": "1.119.0" + "version": "1.120.0" } \ No newline at end of file From 7543f49407e0e3bbd75eb3146b9570a93fe17aa8 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 26 Aug 2021 13:29:54 +0300 Subject: [PATCH 21/91] Update CHANGELOG.md --- CHANGELOG.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a17afe6a8cc59..09152b13c5766 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,6 @@ All notable changes to this project will be documented in this file. See [standa * **apigatewayv2:** http api - disallow empty string as domain name ([#16044](https://github.com/aws/aws-cdk/issues/16044)) ([9c39bcb](https://github.com/aws/aws-cdk/commit/9c39bcb970fc791e94d199b962cc006fca1a3320)) * **appsync:** addSubscription only allows for field type ([#16097](https://github.com/aws/aws-cdk/issues/16097)) ([000d151](https://github.com/aws/aws-cdk/commit/000d151bec2215aa530819c3cf2c8c432352fec3)), closes [#10078](https://github.com/aws/aws-cdk/issues/10078) [#16071](https://github.com/aws/aws-cdk/issues/16071) * **cfnspec:** changes to resource-level documentation not supported ([#16170](https://github.com/aws/aws-cdk/issues/16170)) ([82e4b4f](https://github.com/aws/aws-cdk/commit/82e4b4f07be202e2d6c6afa4f9ed0d9d6146f0a8)) -* **cli:** 'deploy' and 'diff' silently does nothing when given unknown stack name ([#16073](https://github.com/aws/aws-cdk/issues/16073)) ([f35b032](https://github.com/aws/aws-cdk/commit/f35b032cea4354992d3320e78c1ed0e2878a3fe7)), closes [#15866](https://github.com/aws/aws-cdk/issues/15866) * **cli:** Python init template does not work in directory with '-' ([#15939](https://github.com/aws/aws-cdk/issues/15939)) ([3b2c790](https://github.com/aws/aws-cdk/commit/3b2c790c2b7d210868576540feab4e088376ab6c)), closes [#15938](https://github.com/aws/aws-cdk/issues/15938) * **cli:** unknown command pytest in build container fails integration tests ([#16134](https://github.com/aws/aws-cdk/issues/16134)) ([0f7c0b4](https://github.com/aws/aws-cdk/commit/0f7c0b421327f1ffed28de79692191af187f23ca)), closes [#15939](https://github.com/aws/aws-cdk/issues/15939) * **resourcegroups:** ResourceGroup not using TagType.STANDARD, causes deploy failure ([#16211](https://github.com/aws/aws-cdk/issues/16211)) ([cdee1af](https://github.com/aws/aws-cdk/commit/cdee1af03c34a1c08988e672bae6edc2538a8877)), closes [#12986](https://github.com/aws/aws-cdk/issues/12986) @@ -38,10 +37,6 @@ All notable changes to this project will be documented in this file. See [standa * KubectlHandler - insecure kubeconfig warning ([#16063](https://github.com/aws/aws-cdk/issues/16063)) ([82dd282](https://github.com/aws/aws-cdk/commit/82dd2822a86431d0aa0be896550d421810b80c67)), closes [#14560](https://github.com/aws/aws-cdk/issues/14560) -### Reverts - -* **cli:** 'deploy' and 'diff' silently does nothing when given unknown stack name ([#16125](https://github.com/aws/aws-cdk/issues/16125)) ([f2d77d3](https://github.com/aws/aws-cdk/commit/f2d77d336d535ef718813b4ed6b88b5d2af05cb9)), closes [aws/aws-cdk#16073](https://github.com/aws/aws-cdk/issues/16073) -* temporarily transfer [@skinny85](https://github.com/skinny85) module ownership ([#16206](https://github.com/aws/aws-cdk/issues/16206)) ([e678f10](https://github.com/aws/aws-cdk/commit/e678f104df4fb0377c6ad5c8abc4132433363871)) ## [1.119.0](https://github.com/aws/aws-cdk/compare/v1.118.0...v1.119.0) (2021-08-17) From 0d96a6772da79fbafcb586b982e5bc5316d45589 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 26 Aug 2021 13:37:35 +0300 Subject: [PATCH 22/91] Apply suggestions from code review Co-authored-by: Niranjan Jayakar --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09152b13c5766..5b5e333536dde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ All notable changes to this project will be documented in this file. See [standa ### Features * **assertions:** queries and assertions against the Outputs and Mappings sections ([#15892](https://github.com/aws/aws-cdk/issues/15892)) ([90f95e1](https://github.com/aws/aws-cdk/commit/90f95e10f4dd9e4992731d6262dcfc767b65ab3f)) -* **aws-stepfunctions:** add support to heartbeat error inside catch block ([#16078](https://github.com/aws/aws-cdk/issues/16078)) ([2372b3c](https://github.com/aws/aws-cdk/commit/2372b3c360d13fb0224fc981a7bb1ae318581265)), closes [#16084](https://github.com/aws/aws-cdk/issues/16084) +* **stepfunctions:** add support to heartbeat error inside catch block ([#16078](https://github.com/aws/aws-cdk/issues/16078)) ([2372b3c](https://github.com/aws/aws-cdk/commit/2372b3c360d13fb0224fc981a7bb1ae318581265)), closes [#16084](https://github.com/aws/aws-cdk/issues/16084) * **cfnspec:** cloudformation spec v39.10.0 ([#16114](https://github.com/aws/aws-cdk/issues/16114)) ([7e0ad5d](https://github.com/aws/aws-cdk/commit/7e0ad5d17b30150922d0dfd81f42da11fadb8beb)) * **cfnspec:** cloudformation spec v40.0.0 ([#16183](https://github.com/aws/aws-cdk/issues/16183)) ([b059124](https://github.com/aws/aws-cdk/commit/b059124b238e27751217cbdaaa01c38b00e80fc9)) * **cloudwatch:** add support for cross-account alarms ([#16007](https://github.com/aws/aws-cdk/issues/16007)) ([e547ba0](https://github.com/aws/aws-cdk/commit/e547ba0d1491af0abe703132fa06fe786ffd7070)), closes [#15959](https://github.com/aws/aws-cdk/issues/15959) From 00b9c84da7351a2eeaff0a0f31728508597fe400 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 26 Aug 2021 13:40:47 +0300 Subject: [PATCH 23/91] Update CHANGELOG.md --- CHANGELOG.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b5e333536dde..dd16881355b70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,6 @@ All notable changes to this project will be documented in this file. See [standa * **assertions:** queries and assertions against the Outputs and Mappings sections ([#15892](https://github.com/aws/aws-cdk/issues/15892)) ([90f95e1](https://github.com/aws/aws-cdk/commit/90f95e10f4dd9e4992731d6262dcfc767b65ab3f)) * **stepfunctions:** add support to heartbeat error inside catch block ([#16078](https://github.com/aws/aws-cdk/issues/16078)) ([2372b3c](https://github.com/aws/aws-cdk/commit/2372b3c360d13fb0224fc981a7bb1ae318581265)), closes [#16084](https://github.com/aws/aws-cdk/issues/16084) -* **cfnspec:** cloudformation spec v39.10.0 ([#16114](https://github.com/aws/aws-cdk/issues/16114)) ([7e0ad5d](https://github.com/aws/aws-cdk/commit/7e0ad5d17b30150922d0dfd81f42da11fadb8beb)) * **cfnspec:** cloudformation spec v40.0.0 ([#16183](https://github.com/aws/aws-cdk/issues/16183)) ([b059124](https://github.com/aws/aws-cdk/commit/b059124b238e27751217cbdaaa01c38b00e80fc9)) * **cloudwatch:** add support for cross-account alarms ([#16007](https://github.com/aws/aws-cdk/issues/16007)) ([e547ba0](https://github.com/aws/aws-cdk/commit/e547ba0d1491af0abe703132fa06fe786ffd7070)), closes [#15959](https://github.com/aws/aws-cdk/issues/15959) * **codecommit:** make Repository a source for CodeStar Notifications ([#15739](https://github.com/aws/aws-cdk/issues/15739)) ([ae34d4a](https://github.com/aws/aws-cdk/commit/ae34d4a69a5073d8f0175b5282fa8bf92139fab5)) @@ -33,8 +32,8 @@ All notable changes to this project will be documented in this file. See [standa * **ses:** drop spam rule appears in the incorrect order ([#16146](https://github.com/aws/aws-cdk/issues/16146)) ([677fedc](https://github.com/aws/aws-cdk/commit/677fedcc5351b8b5346970fac03e5e342f36265b)), closes [#16091](https://github.com/aws/aws-cdk/issues/16091) * **sqs:** unable to import a FIFO queue when the queue ARN is a token ([#15976](https://github.com/aws/aws-cdk/issues/15976)) ([a1a65bc](https://github.com/aws/aws-cdk/commit/a1a65bc9a38b06ec51dff462e52b1beb8d421a56)), closes [#12466](https://github.com/aws/aws-cdk/issues/12466) * **ssm:** StringParameter.fromStringParameterAttributes cannot accept version as a numeric Token ([#16048](https://github.com/aws/aws-cdk/issues/16048)) ([eb54cd4](https://github.com/aws/aws-cdk/commit/eb54cd416a48708898e30986058491e21125b2f7)), closes [#11913](https://github.com/aws/aws-cdk/issues/11913) -* (aws-ec2): fix vpc endpoint incorrect issue in China region ([#16139](https://github.com/aws/aws-cdk/issues/16139)) ([0d0db38](https://github.com/aws/aws-cdk/commit/0d0db38e3cdb557b4a641c5993068400847cc7df)), closes [#9864](https://github.com/aws/aws-cdk/issues/9864) -* KubectlHandler - insecure kubeconfig warning ([#16063](https://github.com/aws/aws-cdk/issues/16063)) ([82dd282](https://github.com/aws/aws-cdk/commit/82dd2822a86431d0aa0be896550d421810b80c67)), closes [#14560](https://github.com/aws/aws-cdk/issues/14560) +* **ec2:** fix vpc endpoint incorrect issue in China region ([#16139](https://github.com/aws/aws-cdk/issues/16139)) ([0d0db38](https://github.com/aws/aws-cdk/commit/0d0db38e3cdb557b4a641c5993068400847cc7df)), closes [#9864](https://github.com/aws/aws-cdk/issues/9864) +* **eks:** insecure kubeconfig warning ([#16063](https://github.com/aws/aws-cdk/issues/16063)) ([82dd282](https://github.com/aws/aws-cdk/commit/82dd2822a86431d0aa0be896550d421810b80c67)), closes [#14560](https://github.com/aws/aws-cdk/issues/14560) From 0889564a0c1b04d33909dd3fdb42147f23d67cbd Mon Sep 17 00:00:00 2001 From: Yihui Han <53243835+readybuilderone@users.noreply.github.com> Date: Fri, 27 Aug 2021 00:50:13 +0800 Subject: [PATCH 24/91] fix(apigatewayv2): api mapping key with two hyphens is disallowed (#16204) fix(apigatewayv2): fix api mapping key too strict issue. This PR fix the issue that Validation of apiMappingKey in ApiMappingProps is too strict. Closes: #15948 ---- # Considerations: The validation rule of the apiMappingKey was added in [PR #9141](https://github.com/aws/aws-cdk/pull/9141) to fix [issue #8983](https://github.com/aws/aws-cdk/issues/8983). However, as time changed, it looks like the validation rules have changed as well. But the validation rules are not clearly written out in AWS documentation, so I did [various validations](https://github.com/aws/aws-cdk/issues/15948#issuecomment-899520871) in the AWS console based on the unit test, and found the rules to be very complex. Thanks to @pahud and @nija-at 's suggestions, there is no need to stand in the way between CDK users and API Gateway supporting more possibilities in the future, I just removed the validation logic and the relative unit tests. *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/common/api-mapping.ts | 5 - .../test/common/api-mapping.test.ts | 114 ------------------ 2 files changed, 119 deletions(-) diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts index f9ebfc89b52ca..a4031ddf783e2 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts @@ -103,11 +103,6 @@ export class ApiMapping extends Resource implements IApiMapping { } } - const paramRe = '^[a-zA-Z0-9]*[-_.+!,$]?[a-zA-Z0-9]*$'; - if (props.apiMappingKey && !new RegExp(paramRe).test(props.apiMappingKey)) { - throw new Error('An ApiMapping key may contain only letters, numbers and one of $-_.+!*\'(),'); - } - if (props.apiMappingKey === '') { throw new Error('empty string for api mapping key not allowed'); } diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/common/api-mapping.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/common/api-mapping.test.ts index 607afb5f8238f..ff53d3dad11fc 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/common/api-mapping.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/common/api-mapping.test.ts @@ -83,120 +83,6 @@ describe('ApiMapping', () => { }).toThrow(/empty string for api mapping key not allowed/); }); - test('apiMappingKey validation - single slash not allowed', () => { - - const stack = new Stack(); - const api = new HttpApi(stack, 'Api'); - - const dn = new DomainName(stack, 'DomainName', { - domainName, - certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), - }); - - expect(() => { - new ApiMapping(stack, 'Mapping', { - api, - domainName: dn, - apiMappingKey: '/', - }); - }).toThrow(/An ApiMapping key may contain only letters, numbers and one of/); - }); - - test('apiMappingKey validation - prefix slash not allowd', () => { - - const stack = new Stack(); - const api = new HttpApi(stack, 'Api'); - - const dn = new DomainName(stack, 'DomainName', { - domainName, - certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), - }); - - expect(() => { - new ApiMapping(stack, 'Mapping', { - api, - domainName: dn, - apiMappingKey: '/foo', - }); - }).toThrow(/An ApiMapping key may contain only letters, numbers and one of/); - }); - - test('apiMappingKey validation - slash in the middle not allowed', () => { - - const stack = new Stack(); - const api = new HttpApi(stack, 'Api'); - - const dn = new DomainName(stack, 'DomainName', { - domainName, - certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), - }); - - expect(() => { - new ApiMapping(stack, 'Mapping', { - api, - domainName: dn, - apiMappingKey: 'foo/bar', - }); - }).toThrow(/An ApiMapping key may contain only letters, numbers and one of/); - }); - - test('apiMappingKey validation - trailing slash not allowed', () => { - - const stack = new Stack(); - const api = new HttpApi(stack, 'Api'); - - const dn = new DomainName(stack, 'DomainName', { - domainName, - certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), - }); - - expect(() => { - new ApiMapping(stack, 'Mapping', { - api, - domainName: dn, - apiMappingKey: 'foo/', - }); - }).toThrow(/An ApiMapping key may contain only letters, numbers and one of/); - }); - - test('apiMappingKey validation - special character in the prefix not allowed', () => { - - const stack = new Stack(); - const api = new HttpApi(stack, 'Api'); - - const dn = new DomainName(stack, 'DomainName', { - domainName, - certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), - }); - - expect(() => { - new ApiMapping(stack, 'Mapping', { - api, - domainName: dn, - apiMappingKey: '^foo', - }); - }).toThrow(/An ApiMapping key may contain only letters, numbers and one of/); - }); - - test('apiMappingKey validation - multiple special character not allowed', () => { - - const stack = new Stack(); - const api = new HttpApi(stack, 'Api'); - - const dn = new DomainName(stack, 'DomainName', { - domainName, - certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), - }); - - expect(() => { - new ApiMapping(stack, 'Mapping', { - api, - domainName: dn, - apiMappingKey: 'foo.*$', - }); - }).toThrow(/An ApiMapping key may contain only letters, numbers and one of/); - }); - test('import mapping', () => { const stack = new Stack(); From 5cec2f8c4f2e08838e20b9757cda446f428b80c9 Mon Sep 17 00:00:00 2001 From: Wouter Klijn Date: Thu, 26 Aug 2021 19:30:15 +0200 Subject: [PATCH 25/91] chore(rds): add new versions of MariaDB, MySQL and SQL Server engines (#16115) Added every change as a separate commit, feel free to rebase as you see fit. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-rds/lib/instance-engine.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts index eacb8097da55e..c070b0988e314 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts @@ -247,6 +247,8 @@ export class MariaDbEngineVersion { public static readonly VER_10_2_32 = MariaDbEngineVersion.of('10.2.32', '10.2'); /** Version "10.2.37". */ public static readonly VER_10_2_37 = MariaDbEngineVersion.of('10.2.37', '10.2'); + /** Version "10.2.39". */ + public static readonly VER_10_2_39 = MariaDbEngineVersion.of('10.2.39', '10.2'); /** Version "10.3" (only a major version, without a specific minor version). */ public static readonly VER_10_3 = MariaDbEngineVersion.of('10.3', '10.3'); @@ -462,6 +464,8 @@ export class MysqlEngineVersion { public static readonly VER_5_7_31 = MysqlEngineVersion.of('5.7.31', '5.7'); /** Version "5.7.33". */ public static readonly VER_5_7_33 = MysqlEngineVersion.of('5.7.33', '5.7'); + /** Version "5.7.34". */ + public static readonly VER_5_7_34 = MysqlEngineVersion.of('5.7.34', '5.7'); /** Version "8.0" (only a major version, without a specific minor version). */ public static readonly VER_8_0 = MysqlEngineVersion.of('8.0', '8.0'); @@ -1330,6 +1334,10 @@ export class SqlServerEngineVersion { public static readonly VER_13_00_5598_27_V1 = SqlServerEngineVersion.of('13.00.5598.27.v1', '13.00'); /** Version "13.00.5820.21.v1". */ public static readonly VER_13_00_5820_21_V1 = SqlServerEngineVersion.of('13.00.5820.21.v1', '13.00'); + /** Version "13.00.5850.14.v1". */ + public static readonly VER_13_00_5850_14_V1 = SqlServerEngineVersion.of('13.00.5850.14.v1', '13.00'); + /** Version "13.00.5882.1.v1". */ + public static readonly VER_13_00_5882_1_V1 = SqlServerEngineVersion.of('13.00.5882.1.v1', '13.00'); /** Version "14.00" (only a major version, without a specific minor version). */ public static readonly VER_14 = SqlServerEngineVersion.of('14.00', '14.00'); @@ -1358,8 +1366,13 @@ export class SqlServerEngineVersion { public static readonly VER_15 = SqlServerEngineVersion.of('15.00', '15.00'); /** Version "15.00.4043.16.v1". */ public static readonly VER_15_00_4043_16_V1 = SqlServerEngineVersion.of('15.00.4043.16.v1', '15.00'); - /** Version "15.00.4043.23.v1". */ + /** + * Version "15.00.4043.23.v1". + * @deprecated This version is erroneous. You might be looking for {@link SqlServerEngineVersion.VER_15_00_4073_23_V1}, instead. + */ public static readonly VER_15_00_4043_23_V1 = SqlServerEngineVersion.of('15.00.4043.23.v1', '15.00'); + /** Version "15.00.4073.23.v1". */ + public static readonly VER_15_00_4073_23_V1 = SqlServerEngineVersion.of('15.00.4073.23.v1', '15.00'); /** * Create a new SqlServerEngineVersion with an arbitrary version. From f40e8d6a502dd42e0a52d81f72abecaa2cdd920a Mon Sep 17 00:00:00 2001 From: Yuto Osawa Date: Fri, 27 Aug 2021 03:11:57 +0900 Subject: [PATCH 26/91] feat(ecs-patterns): add capacity provider strategies to queue processing service pattern (#15684) closes #14781 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-ecs-patterns/README.md | 51 +++++++++++++++++++ .../lib/base/queue-processing-service-base.ts | 10 +++- .../lib/ecs/queue-processing-ecs-service.ts | 1 + .../queue-processing-fargate-service.ts | 1 + .../@aws-cdk/aws-ecs-patterns/package.json | 2 + .../ec2/test.queue-processing-ecs-service.ts | 45 ++++++++++++++++ .../test.queue-processing-fargate-service.ts | 45 ++++++++++++++++ 7 files changed, 154 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs-patterns/README.md b/packages/@aws-cdk/aws-ecs-patterns/README.md index 58dba9f6786cb..fbeb84acb49e6 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/README.md +++ b/packages/@aws-cdk/aws-ecs-patterns/README.md @@ -484,6 +484,57 @@ const queueProcessingFargateService = new QueueProcessingFargateService(stack, ' }); ``` +### Set capacityProviderStrategies for QueueProcessingFargateService + +```ts +const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 }); +const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); +cluster.enableFargateCapacityProviders(); + +const queueProcessingFargateService = new QueueProcessingFargateService(stack, 'Service', { + cluster, + memoryLimitMiB: 512, + image: ecs.ContainerImage.fromRegistry('test'), + capacityProviderStrategies: [ + { + capacityProvider: 'FARGATE_SPOT', + weight: 2, + }, + { + capacityProvider: 'FARGATE', + weight: 1, + }, + ], +}); +``` + +### Set capacityProviderStrategies for QueueProcessingEc2Service + +```ts +const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 }); +const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); +const autoScalingGroup = new autoscaling.AutoScalingGroup(stack, 'asg', { + vpc, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.MICRO), + machineImage: ecs.EcsOptimizedImage.amazonLinux2(), +}); +const capacityProvider = new ecs.AsgCapacityProvider(stack, 'provider', { + autoScalingGroup, +}); +cluster.addAsgCapacityProvider(capacityProvider); + +const queueProcessingFargateService = new QueueProcessingFargateService(stack, 'Service', { + cluster, + memoryLimitMiB: 512, + image: ecs.ContainerImage.fromRegistry('test'), + capacityProviderStrategies: [ + { + capacityProvider: capacityProvider.capacityProviderName, + }, + ], +}); +``` + ### Select specific vpc subnets for ApplicationLoadBalancedFargateService ```ts diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts index b2da8f97e6bc4..2c293dc3173de 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts @@ -1,7 +1,7 @@ import { ScalingInterval } from '@aws-cdk/aws-applicationautoscaling'; import { IVpc } from '@aws-cdk/aws-ec2'; import { - AwsLogDriver, BaseService, Cluster, ContainerImage, DeploymentController, DeploymentCircuitBreaker, + AwsLogDriver, BaseService, CapacityProviderStrategy, Cluster, ContainerImage, DeploymentController, DeploymentCircuitBreaker, ICluster, LogDriver, PropagatedTagSource, Secret, } from '@aws-cdk/aws-ecs'; import { IQueue, Queue } from '@aws-cdk/aws-sqs'; @@ -207,6 +207,14 @@ export interface QueueProcessingServiceBaseProps { * @default - disabled */ readonly circuitBreaker?: DeploymentCircuitBreaker; + + /** + * A list of Capacity Provider strategies used to place a service. + * + * @default - undefined + * + */ + readonly capacityProviderStrategies?: CapacityProviderStrategy[]; } /** diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts index d927284797b5b..0d9f612abfb76 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts @@ -123,6 +123,7 @@ export class QueueProcessingEc2Service extends QueueProcessingServiceBase { enableECSManagedTags: props.enableECSManagedTags, deploymentController: props.deploymentController, circuitBreaker: props.circuitBreaker, + capacityProviderStrategies: props.capacityProviderStrategies, }); this.configureAutoscalingForService(this.service); diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts index cb4b8d77a8188..e6f8b89c736b3 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts @@ -149,6 +149,7 @@ export class QueueProcessingFargateService extends QueueProcessingServiceBase { vpcSubnets: props.taskSubnets, assignPublicIp: props.assignPublicIp, circuitBreaker: props.circuitBreaker, + capacityProviderStrategies: props.capacityProviderStrategies, }); this.configureAutoscalingForService(this.service); diff --git a/packages/@aws-cdk/aws-ecs-patterns/package.json b/packages/@aws-cdk/aws-ecs-patterns/package.json index 2bed9fe6b9cc8..c9d3b46b8f8b2 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/package.json +++ b/packages/@aws-cdk/aws-ecs-patterns/package.json @@ -76,6 +76,7 @@ }, "dependencies": { "@aws-cdk/aws-applicationautoscaling": "0.0.0", + "@aws-cdk/aws-autoscaling": "0.0.0", "@aws-cdk/aws-certificatemanager": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-ecs": "0.0.0", @@ -94,6 +95,7 @@ "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { "@aws-cdk/aws-applicationautoscaling": "0.0.0", + "@aws-cdk/aws-autoscaling": "0.0.0", "@aws-cdk/aws-certificatemanager": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-ecs": "0.0.0", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts index 763cef9f33114..97bdb47c7b5b3 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts @@ -1,4 +1,5 @@ import { ABSENT, expect, haveResource, haveResourceLike } from '@aws-cdk/assert-internal'; +import * as autoscaling from '@aws-cdk/aws-autoscaling'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; import * as sqs from '@aws-cdk/aws-sqs'; @@ -351,4 +352,48 @@ export = { test.done(); }, + + 'can set capacity provider strategies'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + const autoScalingGroup = new autoscaling.AutoScalingGroup(stack, 'asg', { + vpc, + instanceType: new ec2.InstanceType('bogus'), + machineImage: ecs.EcsOptimizedImage.amazonLinux2(), + }); + const capacityProvider = new ecs.AsgCapacityProvider(stack, 'provider', { + autoScalingGroup, + }); + cluster.addAsgCapacityProvider(capacityProvider); + + // WHEN + new ecsPatterns.QueueProcessingEc2Service(stack, 'Service', { + cluster, + image: ecs.ContainerImage.fromRegistry('test'), + memoryLimitMiB: 512, + capacityProviderStrategies: [ + { + capacityProvider: capacityProvider.capacityProviderName, + }, + ], + }); + + // THEN + expect(stack).to( + haveResource('AWS::ECS::Service', { + LaunchType: ABSENT, + CapacityProviderStrategy: [ + { + CapacityProvider: { + Ref: 'providerD3FF4D3A', + }, + }, + ], + }), + ); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts index 7a0f33e3a0a0c..6b5a05ccaaf7b 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts @@ -529,4 +529,49 @@ export = { test.done(); }, + + 'can set capacity provider strategies'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { + vpc, + }); + cluster.enableFargateCapacityProviders(); + + // WHEN + new ecsPatterns.QueueProcessingFargateService(stack, 'Service', { + cluster, + image: ecs.ContainerImage.fromRegistry('test'), + capacityProviderStrategies: [ + { + capacityProvider: 'FARGATE_SPOT', + weight: 2, + }, + { + capacityProvider: 'FARGATE', + weight: 1, + }, + ], + }); + + // THEN + expect(stack).to( + haveResource('AWS::ECS::Service', { + LaunchType: ABSENT, + CapacityProviderStrategy: [ + { + CapacityProvider: 'FARGATE_SPOT', + Weight: 2, + }, + { + CapacityProvider: 'FARGATE', + Weight: 1, + }, + ], + }), + ); + + test.done(); + }, }; From 04d45474d80d3687a3fdf27f4d76dd1c8521eff0 Mon Sep 17 00:00:00 2001 From: benhawley7 <31001948+benhawley7@users.noreply.github.com> Date: Thu, 26 Aug 2021 19:53:35 +0100 Subject: [PATCH 27/91] fix(docs): unnecessary log group in Step Functions state machine x-ray example (#16159) Removes an unnecessary log group definition from the code example for enabling X-Ray in the State Machine. fixes https://github.com/aws/aws-cdk/issues/16158 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-stepfunctions/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions/README.md b/packages/@aws-cdk/aws-stepfunctions/README.md index 4faf512ad0509..6171bac75805a 100644 --- a/packages/@aws-cdk/aws-stepfunctions/README.md +++ b/packages/@aws-cdk/aws-stepfunctions/README.md @@ -593,8 +593,6 @@ new sfn.StateMachine(stack, 'MyStateMachine', { Enable X-Ray tracing for StateMachine: ```ts -const logGroup = new logs.LogGroup(stack, 'MyLogGroup'); - new sfn.StateMachine(stack, 'MyStateMachine', { definition: sfn.Chain.start(new sfn.Pass(stack, 'Pass')), tracingEnabled: true From a42a1ea5a122f864936cdb0113b16fe92cc7205e Mon Sep 17 00:00:00 2001 From: Robert Djurasaj Date: Thu, 26 Aug 2021 14:12:50 -0600 Subject: [PATCH 28/91] feat(ec2): add m6i instances (#16081) https://aws.amazon.com/about-aws/whats-new/2021/08/amazon-ec2-m6i-instances/ ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-ec2/lib/instance-types.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/@aws-cdk/aws-ec2/lib/instance-types.ts b/packages/@aws-cdk/aws-ec2/lib/instance-types.ts index a12dfb92061c6..013f7d3389f3c 100644 --- a/packages/@aws-cdk/aws-ec2/lib/instance-types.ts +++ b/packages/@aws-cdk/aws-ec2/lib/instance-types.ts @@ -442,6 +442,16 @@ export enum InstanceClass { */ M6G = 'm6g', + /** + * Standard instances based on Intel (Ice Lake), 6th generation. + */ + STANDARD6_INTEL = 'm6i', + + /** + * Standard instances based on Intel (Ice Lake), 6th generation. + */ + M6I = 'm6i', + /** * Standard instances, 6th generation with Graviton2 processors and local NVME drive */ From 2c3d21e2f1117a54510ba92748588ee95ab3631c Mon Sep 17 00:00:00 2001 From: ABevier Date: Thu, 26 Aug 2021 19:16:26 -0400 Subject: [PATCH 29/91] feat(ecs-patterns): Allow configuration of SSL policy for listeners created by ECS patterns (#15210) This pr adds the ability to specify the SSL policy of the listener created by an ECS L3 pattern. The Listener's SSL Policy is immutable once it is created so I felt like this is best way to add this feature. I plan to follow this PR up with another that implements #11841 and allows further configuration of the listener after it has been created. closes #8816 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-ecs-patterns/README.md | 25 +++++++++++++++++++ .../application-load-balanced-service-base.ts | 10 +++++++- ...ion-multiple-target-groups-service-base.ts | 24 +++++++++++++++--- .../aws-ecs-patterns/test/ec2/test.l3s-v2.ts | 12 ++++++++- .../aws-ecs-patterns/test/ec2/test.l3s.ts | 4 ++- 5 files changed, 68 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs-patterns/README.md b/packages/@aws-cdk/aws-ecs-patterns/README.md index fbeb84acb49e6..00212e704e294 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/README.md +++ b/packages/@aws-cdk/aws-ecs-patterns/README.md @@ -330,6 +330,31 @@ const scheduledFargateTask = new ScheduledFargateTask(stack, 'ScheduledFargateTa In addition to using the constructs, users can also add logic to customize these constructs: +### Configure HTTPS on an ApplicationLoadBalancedFargateService + +```ts +import { ApplicationLoadBalancedFargateService } from './application-load-balanced-fargate-service'; +import { HostedZone } from '@aws-cdk/aws-route53'; +import { Certificate } from '@aws-cdk/aws-certificatemanager'; +import { SslPolicy } from '@aws-cdk/aws-elasticloadbalancingv2'; + +const domainZone = HostedZone.fromLookup(this, 'Zone', { domainName: 'example.com' }); +const certificate = Certificate.fromCertificateArn(this, 'Cert', 'arn:aws:acm:us-east-1:123456:certificate/abcdefg'); + +const loadBalancedFargateService = new ApplicationLoadBalancedFargateService(stack, 'Service', { + vpc + cluster, + certificate, + sslPolicy: SslPolicy.RECOMMENDED, + domainName: 'api.example.com', + domainZone, + redirectHTTP: true, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + }, +}); +``` + ### Add Schedule-Based Auto-Scaling to an ApplicationLoadBalancedFargateService ```ts diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts index 3c9005af19daa..b4c41ed5aa990 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts @@ -6,7 +6,7 @@ import { } from '@aws-cdk/aws-ecs'; import { ApplicationListener, ApplicationLoadBalancer, ApplicationProtocol, ApplicationProtocolVersion, ApplicationTargetGroup, - IApplicationLoadBalancer, ListenerCertificate, ListenerAction, AddApplicationTargetsProps, + IApplicationLoadBalancer, ListenerCertificate, ListenerAction, AddApplicationTargetsProps, SslPolicy, } from '@aws-cdk/aws-elasticloadbalancingv2'; import { IRole } from '@aws-cdk/aws-iam'; import { ARecord, IHostedZone, RecordTarget, CnameRecord } from '@aws-cdk/aws-route53'; @@ -190,6 +190,13 @@ export interface ApplicationLoadBalancedServiceBaseProps { */ readonly listenerPort?: number; + /** + * The security policy that defines which ciphers and protocols are supported by the ALB Listener. + * + * @default - The recommended elastic load balancing security policy + */ + readonly sslPolicy?: SslPolicy; + /** * Specifies whether to propagate the tags from the task definition or the service to the tasks in the service. * Tags can only be propagated to the tasks within the service during service creation. @@ -441,6 +448,7 @@ export abstract class ApplicationLoadBalancedServiceBase extends CoreConstruct { protocol, port: props.listenerPort, open: props.openListener ?? true, + sslPolicy: props.sslPolicy, }); this.targetGroup = this.listener.addTargets('ECS', targetProps); diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts index 9d9c21c9e7b5e..e2cafab14e71a 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts @@ -4,7 +4,7 @@ import { AwsLogDriver, BaseService, CloudMapOptions, Cluster, ContainerDefinition, ContainerImage, ICluster, LogDriver, PropagatedTagSource, Protocol, Secret, } from '@aws-cdk/aws-ecs'; -import { ApplicationListener, ApplicationLoadBalancer, ApplicationProtocol, ApplicationTargetGroup } from '@aws-cdk/aws-elasticloadbalancingv2'; +import { ApplicationListener, ApplicationLoadBalancer, ApplicationProtocol, ApplicationTargetGroup, SslPolicy } from '@aws-cdk/aws-elasticloadbalancingv2'; import { IRole } from '@aws-cdk/aws-iam'; import { ARecord, IHostedZone, RecordTarget } from '@aws-cdk/aws-route53'; import { LoadBalancerTarget } from '@aws-cdk/aws-route53-targets'; @@ -332,6 +332,13 @@ export interface ApplicationListenerProps { * created for the load balancer's specified domain name. */ readonly certificate?: ICertificate; + + /** + * The security policy that defines which ciphers and protocols are supported by the ALB Listener. + * + * @default - The recommended elastic load balancing security policy + */ + readonly sslPolicy?: SslPolicy; } /** @@ -407,6 +414,7 @@ export abstract class ApplicationMultipleTargetGroupsServiceBase extends CoreCon listenerName: listenerProps.name, loadBalancer: lb, port: listenerProps.port, + sslPolicy: listenerProps.sslPolicy, }); this.listeners.push(listener); } @@ -503,7 +511,7 @@ export abstract class ApplicationMultipleTargetGroupsServiceBase extends CoreCon } private configListener(protocol: ApplicationProtocol, props: ListenerConfig): ApplicationListener { - const listener = this.createListener(props.listenerName, props.loadBalancer, protocol, props.port); + const listener = this.createListener(props, protocol); let certificate; if (protocol === ApplicationProtocol.HTTPS) { certificate = this.createListenerCertificate(props.listenerName, props.certificate, props.domainName, props.domainZone); @@ -567,11 +575,12 @@ export abstract class ApplicationMultipleTargetGroupsServiceBase extends CoreCon } } - private createListener(name: string, lb: ApplicationLoadBalancer, protocol?: ApplicationProtocol, port?: number): ApplicationListener { - return lb.addListener(name, { + private createListener({ loadBalancer, listenerName, port, sslPolicy }: ListenerConfig, protocol?: ApplicationProtocol): ApplicationListener { + return loadBalancer.addListener(listenerName, { protocol, open: true, port, + sslPolicy, }); } @@ -622,6 +631,13 @@ interface ListenerConfig { */ readonly certificate?: ICertificate; + /** + * SSL Policy for the listener + * + * @default null + */ + readonly sslPolicy?: SslPolicy; + /** * The domain name for the service, e.g. "api.example.com." * diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s-v2.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s-v2.ts index 0946123907fe2..5c3c5889c62b6 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s-v2.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s-v2.ts @@ -2,7 +2,7 @@ import { expect, haveResource, haveResourceLike, SynthUtils } from '@aws-cdk/ass import { Certificate } from '@aws-cdk/aws-certificatemanager'; import { InstanceType, Vpc } from '@aws-cdk/aws-ec2'; import { AwsLogDriver, Cluster, ContainerImage, Ec2TaskDefinition, PropagatedTagSource, Protocol } from '@aws-cdk/aws-ecs'; -import { ApplicationProtocol } from '@aws-cdk/aws-elasticloadbalancingv2'; +import { ApplicationProtocol, SslPolicy } from '@aws-cdk/aws-elasticloadbalancingv2'; import { CompositePrincipal, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import { PublicHostedZone } from '@aws-cdk/aws-route53'; import { NamespaceType } from '@aws-cdk/aws-servicediscovery'; @@ -124,6 +124,7 @@ export = { name: 'listener', protocol: ApplicationProtocol.HTTPS, certificate: Certificate.fromCertificateArn(stack, 'Cert', 'helloworld'), + sslPolicy: SslPolicy.TLS12_EXT, }, ], }, @@ -240,6 +241,15 @@ export = { }, })); + expect(stack).to(haveResourceLike('AWS::ElasticLoadBalancingV2::Listener', { + Port: 443, + Protocol: 'HTTPS', + Certificates: [{ + CertificateArn: 'helloworld', + }], + SslPolicy: SslPolicy.TLS12_EXT, + })); + test.done(); }, diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts index 5ef253d71bdb1..8e2f527fbe509 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts @@ -2,7 +2,7 @@ import { ABSENT, arrayWith, expect, haveResource, haveResourceLike, objectLike } import { Certificate } from '@aws-cdk/aws-certificatemanager'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; -import { ApplicationLoadBalancer, ApplicationProtocol, ApplicationProtocolVersion, NetworkLoadBalancer } from '@aws-cdk/aws-elasticloadbalancingv2'; +import { ApplicationLoadBalancer, ApplicationProtocol, ApplicationProtocolVersion, NetworkLoadBalancer, SslPolicy } from '@aws-cdk/aws-elasticloadbalancingv2'; import { PublicHostedZone } from '@aws-cdk/aws-route53'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import * as cdk from '@aws-cdk/core'; @@ -508,6 +508,7 @@ export = { domainName: 'api.example.com', domainZone: zone, certificate: Certificate.fromCertificateArn(stack, 'Cert', 'helloworld'), + sslPolicy: SslPolicy.TLS12_EXT, }); // THEN - stack contains a load balancer and a service @@ -519,6 +520,7 @@ export = { Certificates: [{ CertificateArn: 'helloworld', }], + SslPolicy: SslPolicy.TLS12_EXT, })); expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { From f42b233a76ae810634fa43a25604dbc65bdd63b9 Mon Sep 17 00:00:00 2001 From: Robert Djurasaj Date: Fri, 27 Aug 2021 04:18:44 -0600 Subject: [PATCH 30/91] feat(aws-cloudfront-origins): add custom headers to S3Origin (#16161) Closes #16160. @njlynch Can you please review this when time permits and see if it needs any further polishing. cc @nwitte-rocketloans ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-cloudfront-origins/README.md | 18 +++++++ .../aws-cloudfront-origins/lib/s3-origin.ts | 9 +--- .../test/s3-origin.test.ts | 17 +++++-- .../@aws-cdk/aws-cloudfront/lib/origin.ts | 32 ++++++++++++ .../aws-cloudfront/test/origin.test.ts | 50 ++++++++++++++++++- 5 files changed, 114 insertions(+), 12 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudfront-origins/README.md b/packages/@aws-cdk/aws-cloudfront-origins/README.md index 05d12bc1ae6f4..cb7af64ff8618 100644 --- a/packages/@aws-cdk/aws-cloudfront-origins/README.md +++ b/packages/@aws-cdk/aws-cloudfront-origins/README.md @@ -33,6 +33,24 @@ CloudFront's redirect and error handling will be used. In the latter case, the O underlying bucket. This can be used in conjunction with a bucket that is not public to require that your users access your content using CloudFront URLs and not S3 URLs directly. Alternatively, a custom origin access identity can be passed to the S3 origin in the properties. +### Adding Custom Headers + +You can configure CloudFront to add custom headers to the requests that it sends to your origin. These custom headers enable you to send and gather information from your origin that you don’t get with typical viewer requests. These headers can even be customized for each origin. CloudFront supports custom headers for both for custom and Amazon S3 origins. + +```ts +import * as cloudfront from '@aws-cdk/aws-cloudfront'; +import * as origins from '@aws-cdk/aws-cloudfront-origins'; + +const myBucket = new s3.Bucket(this, 'myBucket'); +new cloudfront.Distribution(this, 'myDist', { + defaultBehavior: { origin: new origins.S3Origin(myBucket, { + customHeaders: { + Foo: 'bar', + }, + })}, +}); +``` + ## ELBv2 Load Balancer An Elastic Load Balancing (ELB) v2 load balancer may be used as an origin. In order for a load balancer to serve as an origin, it must be publicly diff --git a/packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts b/packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts index a0ac4037d9741..4bdd39a2e5888 100644 --- a/packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts +++ b/packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts @@ -11,14 +11,7 @@ import { Construct } from '@aws-cdk/core'; /** * Properties to use to customize an S3 Origin. */ -export interface S3OriginProps { - /** - * An optional path that CloudFront appends to the origin domain name when CloudFront requests content from the origin. - * Must begin, but not end, with '/' (e.g., '/production/images'). - * - * @default '/' - */ - readonly originPath?: string; +export interface S3OriginProps extends cloudfront.OriginProps { /** * An optional Origin Access Identity of the origin identity cloudfront will use when calling your s3 bucket. * diff --git a/packages/@aws-cdk/aws-cloudfront-origins/test/s3-origin.test.ts b/packages/@aws-cdk/aws-cloudfront-origins/test/s3-origin.test.ts index c9e0ecb797464..e8e2c4b2c41b9 100644 --- a/packages/@aws-cdk/aws-cloudfront-origins/test/s3-origin.test.ts +++ b/packages/@aws-cdk/aws-cloudfront-origins/test/s3-origin.test.ts @@ -1,7 +1,7 @@ import '@aws-cdk/assert-internal/jest'; import * as cloudfront from '@aws-cdk/aws-cloudfront'; import * as s3 from '@aws-cdk/aws-s3'; -import { App, Stack } from '@aws-cdk/core'; +import { App, Duration, Stack } from '@aws-cdk/core'; import { S3Origin } from '../lib'; let app: App; @@ -34,16 +34,27 @@ describe('With bucket', () => { }); }); - test('can customize originPath property', () => { + test('can customize base origin properties', () => { const bucket = new s3.Bucket(stack, 'Bucket'); - const origin = new S3Origin(bucket, { originPath: '/assets' }); + const origin = new S3Origin(bucket, { + originPath: '/assets', + connectionTimeout: Duration.seconds(5), + connectionAttempts: 2, + customHeaders: { AUTH: 'NONE' }, + }); const originBindConfig = origin.bind(stack, { originId: 'StackOrigin029E19582' }); expect(stack.resolve(originBindConfig.originProperty)).toEqual({ id: 'StackOrigin029E19582', domainName: { 'Fn::GetAtt': ['Bucket83908E77', 'RegionalDomainName'] }, originPath: '/assets', + connectionTimeout: 5, + connectionAttempts: 2, + originCustomHeaders: [{ + headerName: 'AUTH', + headerValue: 'NONE', + }], s3OriginConfig: { originAccessIdentity: { 'Fn::Join': ['', diff --git a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts index 9b841e413c405..0b7ab7796b97f 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts @@ -120,6 +120,7 @@ export abstract class OriginBase implements IOrigin { protected constructor(domainName: string, props: OriginProps = {}) { validateIntInRangeOrUndefined('connectionTimeout', 1, 10, props.connectionTimeout?.toSeconds()); validateIntInRangeOrUndefined('connectionAttempts', 1, 3, props.connectionAttempts, false); + validateCustomHeaders(props.customHeaders); this.domainName = domainName; this.originPath = this.validateOriginPath(props.originPath); @@ -205,3 +206,34 @@ function validateIntInRangeOrUndefined(name: string, min: number, max: number, v throw new Error(`${name}: Must be an int between ${min} and ${max}${seconds} (inclusive); received ${value}.`); } } + +/** + * Throws an error if custom header assignment is prohibited by CloudFront. + * @link: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/add-origin-custom-headers.html#add-origin-custom-headers-denylist + */ +function validateCustomHeaders(customHeaders?: Record) { + if (!customHeaders || Object.entries(customHeaders).length === 0) { return; } + const customHeaderKeys = Object.keys(customHeaders); + const prohibitedHeaderKeys = [ + 'Cache-Control', 'Connection', 'Content-Length', 'Cookie', 'Host', 'If-Match', 'If-Modified-Since', 'If-None-Match', 'If-Range', 'If-Unmodified-Since', + 'Max-Forwards', 'Pragma', 'Proxy-Authorization', 'Proxy-Connection', 'Range', 'Request-Range', 'TE', 'Trailer', 'Transfer-Encoding', 'Upgrade', 'Via', + 'X-Real-Ip', + ]; + const prohibitedHeaderKeyPrefixes = [ + 'X-Amz-', 'X-Edge-', + ]; + + const prohibitedHeadersKeysMatches = customHeaderKeys.filter(customKey => { + return prohibitedHeaderKeys.map((prohibitedKey) => prohibitedKey.toLowerCase()).includes(customKey.toLowerCase()); + }); + const prohibitedHeaderPrefixMatches = customHeaderKeys.filter(customKey => { + return prohibitedHeaderKeyPrefixes.some(prohibitedKeyPrefix => customKey.toLowerCase().startsWith(prohibitedKeyPrefix.toLowerCase())); + }); + + if (prohibitedHeadersKeysMatches.length !== 0) { + throw new Error(`The following headers cannot be configured as custom origin headers: ${prohibitedHeadersKeysMatches.join(', ')}`); + } + if (prohibitedHeaderPrefixMatches.length !== 0) { + throw new Error(`The following headers cannot be used as prefixes for custom origin headers: ${prohibitedHeaderPrefixMatches.join(', ')}`); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts b/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts index b30362c7fe652..e6a59ff6179d6 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts @@ -57,4 +57,52 @@ test.each(['us-east-1', 'ap-southeast-2', 'eu-west-3', 'me-south-1']) enabled: true, originShieldRegion, }); -}); \ No newline at end of file +}); + +test('throw an error if Custom Headers keys are not permitted', () => { + // case sensitive + expect(() => { + new TestOrigin('example.com', { + customHeaders: { + Host: 'bad', + Cookie: 'bad', + Connection: 'bad', + TS: 'bad', + }, + }); + }).toThrow(/The following headers cannot be configured as custom origin headers: (.*?)/); + + // case insensitive + expect(() => { + new TestOrigin('example.com', { + customHeaders: { + hOst: 'bad', + cOOkIe: 'bad', + Connection: 'bad', + Ts: 'bad', + }, + }); + }).toThrow(/The following headers cannot be configured as custom origin headers: (.*?)/); +}); + +test('throw an error if Custom Headers are pre-fixed with non-permitted keys', () => { + // case sensitive + expect(() => { + new TestOrigin('example.com', { + customHeaders: { + 'X-Amz-dummy': 'bad', + 'X-Edge-dummy': 'bad', + }, + }); + }).toThrow(/The following headers cannot be used as prefixes for custom origin headers: (.*?)/); + + // case insensitive + expect(() => { + new TestOrigin('example.com', { + customHeaders: { + 'x-amZ-dummy': 'bad', + 'x-eDgE-dummy': 'bad', + }, + }); + }).toThrow(/The following headers cannot be used as prefixes for custom origin headers: (.*?)/); +}); From fe81be78322e3f1c23d2b02e59b56faa3b06e554 Mon Sep 17 00:00:00 2001 From: AWS CDK Automation <43080478+aws-cdk-automation@users.noreply.github.com> Date: Fri, 27 Aug 2021 14:00:20 +0300 Subject: [PATCH 31/91] feat(cfnspec): cloudformation spec v40.1.0 (#16254) Co-authored-by: AWS CDK Team Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- packages/@aws-cdk/cfnspec/CHANGELOG.md | 20 +++++++++++++++++++ packages/@aws-cdk/cfnspec/cfn.version | 2 +- ...0_CloudFormationResourceSpecification.json | 16 +++++++++++++-- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/cfnspec/CHANGELOG.md b/packages/@aws-cdk/cfnspec/CHANGELOG.md index 85274a4543fbf..5def28ee631e9 100644 --- a/packages/@aws-cdk/cfnspec/CHANGELOG.md +++ b/packages/@aws-cdk/cfnspec/CHANGELOG.md @@ -1,3 +1,23 @@ +# CloudFormation Resource Specification v40.1.0 + +## New Resource Types + + +## Attribute Changes + + +## Property Changes + +* AWS::CE::CostCategory SplitChargeRules (__added__) + +## Property Type Changes + +* AWS::EFS::FileSystem.LifecyclePolicy TransitionToPrimaryStorageClass (__added__) +* AWS::EFS::FileSystem.LifecyclePolicy TransitionToIA.Required (__changed__) + * Old: true + * New: false + + # CloudFormation Resource Specification v40.0.0 ## New Resource Types diff --git a/packages/@aws-cdk/cfnspec/cfn.version b/packages/@aws-cdk/cfnspec/cfn.version index e9340fd6f7115..312a84c9209ab 100644 --- a/packages/@aws-cdk/cfnspec/cfn.version +++ b/packages/@aws-cdk/cfnspec/cfn.version @@ -1 +1 @@ -40.0.0 +40.1.0 diff --git a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json index 1bb4dde7c70f4..33d10e45ecb92 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json +++ b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json @@ -23470,7 +23470,13 @@ "TransitionToIA": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-efs-filesystem-lifecyclepolicy.html#cfn-efs-filesystem-lifecyclepolicy-transitiontoia", "PrimitiveType": "String", - "Required": true, + "Required": false, + "UpdateType": "Mutable" + }, + "TransitionToPrimaryStorageClass": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-efs-filesystem-lifecyclepolicy.html#cfn-efs-filesystem-lifecyclepolicy-transitiontoprimarystorageclass", + "PrimitiveType": "String", + "Required": false, "UpdateType": "Mutable" } } @@ -61896,7 +61902,7 @@ } } }, - "ResourceSpecificationVersion": "40.0.0", + "ResourceSpecificationVersion": "40.1.0", "ResourceTypes": { "AWS::ACMPCA::Certificate": { "Attributes": { @@ -67257,6 +67263,12 @@ "PrimitiveType": "String", "Required": true, "UpdateType": "Mutable" + }, + "SplitChargeRules": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ce-costcategory.html#cfn-ce-costcategory-splitchargerules", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" } } }, From b838f95f0905316fe706779381c93bedaa9ad504 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Fri, 27 Aug 2021 14:48:32 +0100 Subject: [PATCH 32/91] feat(assertions): 'not' matcher (#16240) Supply a 'not' matcher that can be used to invert the matching pattern relates #15868 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/assertions/README.md | 29 +++++ packages/@aws-cdk/assertions/lib/match.ts | 39 +++++-- .../@aws-cdk/assertions/test/match.test.ts | 101 +++++++++++++++++- 3 files changed, 155 insertions(+), 14 deletions(-) diff --git a/packages/@aws-cdk/assertions/README.md b/packages/@aws-cdk/assertions/README.md index 67109b1156964..d651ab72c1bc3 100644 --- a/packages/@aws-cdk/assertions/README.md +++ b/packages/@aws-cdk/assertions/README.md @@ -238,6 +238,35 @@ target array. Out of order will be recorded as a match failure. Alternatively, the `Match.arrayEquals()` API can be used to assert that the target is exactly equal to the pattern array. +### Not Matcher + +The not matcher inverts the search pattern and matches all patterns in the path that does +not match the pattern specified. + +```ts +// Given a template - +// { +// "Resources": { +// "MyBar": { +// "Type": "Foo::Bar", +// "Properties": { +// "Fred": ["Flob", "Cat"] +// } +// } +// } +// } + +// The following will NOT throw an assertion error +assert.hasResourceProperties('Foo::Bar', { + Fred: Match.not(['Flob']), +}); + +// The following will throw an assertion error +assert.hasResourceProperties('Foo::Bar', Match.objectLike({ + Fred: Match.not(['Flob', 'Cat']); +}}); +``` + ## Strongly typed languages Some of the APIs documented above, such as `templateMatches()` and diff --git a/packages/@aws-cdk/assertions/lib/match.ts b/packages/@aws-cdk/assertions/lib/match.ts index 2a88bada59bac..802acdc603e70 100644 --- a/packages/@aws-cdk/assertions/lib/match.ts +++ b/packages/@aws-cdk/assertions/lib/match.ts @@ -55,6 +55,14 @@ export abstract class Match { public static objectEquals(pattern: {[key: string]: any}): Matcher { return new ObjectMatch('objectEquals', pattern, { partial: false }); } + + /** + * Matches any target which does NOT follow the specified pattern. + * @param pattern the pattern to NOT match + */ + public static not(pattern: any): Matcher { + return new NotMatch('not', pattern); + } } /** @@ -82,7 +90,6 @@ class LiteralMatch extends Matcher { super(); this.partialObjects = options.partialObjects ?? false; - this.name = 'exact'; if (Matcher.isMatcher(this.pattern)) { throw new Error('LiteralMatch cannot directly contain another matcher. ' + @@ -143,11 +150,6 @@ class ArrayMatch extends Matcher { super(); this.partial = options.subsequence ?? true; - if (this.partial) { - this.name = 'arrayWith'; - } else { - this.name = 'arrayEquals'; - } } public test(actual: any): MatchResult { @@ -211,11 +213,6 @@ class ObjectMatch extends Matcher { super(); this.partial = options.partial ?? true; - if (this.partial) { - this.name = 'objectLike'; - } else { - this.name = 'objectEquals'; - } } public test(actual: any): MatchResult { @@ -254,6 +251,26 @@ class ObjectMatch extends Matcher { } } +class NotMatch extends Matcher { + constructor( + public readonly name: string, + private readonly pattern: {[key: string]: any}) { + + super(); + } + + public test(actual: any): MatchResult { + const matcher = Matcher.isMatcher(this.pattern) ? this.pattern : new LiteralMatch(this.name, this.pattern); + + const innerResult = matcher.test(actual); + const result = new MatchResult(actual); + if (innerResult.failCount === 0) { + result.push(this, [], `Found unexpected match: ${JSON.stringify(actual, undefined, 2)}`); + } + return result; + } +} + function getType(obj: any): string { return Array.isArray(obj) ? 'array' : typeof obj; } \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/test/match.test.ts b/packages/@aws-cdk/assertions/test/match.test.ts index 697c46e139c11..b46eb0d53d204 100644 --- a/packages/@aws-cdk/assertions/test/match.test.ts +++ b/packages/@aws-cdk/assertions/test/match.test.ts @@ -194,17 +194,112 @@ describe('Matchers', () => { expectFailure(matcher, { foo: 'bar', baz: 'qux' }, [/Unexpected key at \/baz/]); }); }); + + describe('not()', () => { + let matcher: Matcher; + + test('literal', () => { + matcher = Match.not('foo'); + expectPass(matcher, 'bar'); + expectPass(matcher, 3); + + expectFailure(matcher, 'foo', ['Found unexpected match: "foo"']); + }); + + test('object', () => { + matcher = Match.not({ foo: 'bar' }); + expectPass(matcher, 'bar'); + expectPass(matcher, 3); + expectPass(matcher, { foo: 'baz' }); + expectPass(matcher, { bar: 'foo' }); + + const msg = [ + 'Found unexpected match: {', + ' "foo": "bar"', + '}', + ].join('\n'); + expectFailure(matcher, { foo: 'bar' }, [msg]); + }); + + test('array', () => { + matcher = Match.not(['foo', 'bar']); + expectPass(matcher, 'foo'); + expectPass(matcher, []); + expectPass(matcher, ['bar']); + expectPass(matcher, ['foo', 3]); + + const msg = [ + 'Found unexpected match: [', + ' "foo",', + ' "bar"', + ']', + ].join('\n'); + expectFailure(matcher, ['foo', 'bar'], [msg]); + }); + + test('as a nested matcher', () => { + matcher = Match.exact({ + foo: { bar: Match.not([1, 2]) }, + }); + + expectPass(matcher, { + foo: { bar: [1] }, + }); + expectPass(matcher, { + foo: { bar: ['baz'] }, + }); + + const msg = [ + 'Found unexpected match: [', + ' 1,', + ' 2', + '] at /foo/bar', + ].join('\n'); + expectFailure(matcher, { + foo: { bar: [1, 2] }, + }, [msg]); + }); + + test('with nested matcher', () => { + matcher = Match.not({ + foo: { bar: Match.arrayWith([1]) }, + }); + + expectPass(matcher, { + foo: { bar: [2] }, + }); + expectPass(matcher, 'foo'); + + const msg = [ + 'Found unexpected match: {', + ' "foo": {', + ' "bar": [', + ' 1,', + ' 2', + ' ]', + ' }', + '}', + ].join('\n'); + expectFailure(matcher, { + foo: { bar: [1, 2] }, + }, [msg]); + }); + }); }); function expectPass(matcher: Matcher, target: any): void { expect(matcher.test(target).hasFailed()).toEqual(false); } -function expectFailure(matcher: Matcher, target: any, expected: (string | RegExp)[]): void { - const actual = matcher.test(target).toHumanStrings(); +function expectFailure(matcher: Matcher, target: any, expected: (string | RegExp)[] = []): void { + const result = matcher.test(target); + expect(result.failCount).toBeGreaterThan(0); + const actual = result.toHumanStrings(); + if (expected.length > 0) { + expect(actual.length).toEqual(expected.length); + } for (let i = 0; i < expected.length; i++) { const e = expected[i]; expect(actual[i]).toMatch(e); } - expect(expected.length).toEqual(actual.length); } \ No newline at end of file From de218ba3a294b5b98f93fc75a04ce42294e95008 Mon Sep 17 00:00:00 2001 From: kaizen3031593 <36202692+kaizen3031593@users.noreply.github.com> Date: Fri, 27 Aug 2021 11:40:18 -0400 Subject: [PATCH 33/91] feat(synthetics): add Python runtime and latest Nodejs runtime (#16069) This PR addresses the fact that the current synthetics module was built to support nodejs runtimes only by opening support for python runtimes. Closes #15138 and #16177. **Breaking Changes** - `Runtime('customRuntimeHere')` becomes `Runtime('customRuntime', 'runtimeFamily')` - `Code.fromAnything('path').bind(this, 'handler')` becomes `Code.fromAnything('path').bind(this, 'handler', 'runtimeFamily')` **Whats in this PR?** - Adds latest Nodejs runtime (`syn-nodejs-puppeteer-3.2`) and updates integ test to it. - Adds generic python script to the folder `test/canaries/python` in order to run unit & integration tests on it. - Adds new `RuntimeFamily` enum that is required by the `Runtime` object to differentiate between Python and Node. - Verifies the correct folder structure for Python runtimes (`python/.py`). - Updates readme. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-synthetics/README.md | 11 +- .../@aws-cdk/aws-synthetics/lib/canary.ts | 78 +------ packages/@aws-cdk/aws-synthetics/lib/code.ts | 26 ++- packages/@aws-cdk/aws-synthetics/lib/index.ts | 1 + .../@aws-cdk/aws-synthetics/lib/runtime.ts | 123 +++++++++++ .../test/canaries/python/canary.py | 61 ++++++ .../aws-synthetics/test/canary.test.ts | 21 +- .../@aws-cdk/aws-synthetics/test/code.test.ts | 59 ++++-- .../test/integ.canary.expected.json | 191 ++++++++++++++++-- .../aws-synthetics/test/integ.canary.ts | 15 +- 10 files changed, 471 insertions(+), 115 deletions(-) create mode 100644 packages/@aws-cdk/aws-synthetics/lib/runtime.ts create mode 100644 packages/@aws-cdk/aws-synthetics/test/canaries/python/canary.py diff --git a/packages/@aws-cdk/aws-synthetics/README.md b/packages/@aws-cdk/aws-synthetics/README.md index 6dcd911f7da5e..bd4fcaede272c 100644 --- a/packages/@aws-cdk/aws-synthetics/README.md +++ b/packages/@aws-cdk/aws-synthetics/README.md @@ -130,7 +130,7 @@ new synthetics.Canary(this, 'Bucket Canary', { }); ``` -> **Note:** For `code.fromAsset()` and `code.fromBucket()`, the canary resource requires the following folder structure: +> **Note:** Synthetics have a specified folder structure for canaries. For Node scripts supplied via `code.fromAsset()` or `code.fromBucket()`, the canary resource requires the following folder structure: > > ```plaintext > canary/ @@ -139,6 +139,15 @@ new synthetics.Canary(this, 'Bucket Canary', { > ├── .js > ``` > +> +> For Python scripts supplied via `code.fromAsset()` or `code.fromBucket()`, the canary resource requires the following folder structure: +> +> ```plaintext +> canary/ +> ├── python/ +> ├── .py +> ``` +> > See Synthetics [docs](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary.html). ### Alarms diff --git a/packages/@aws-cdk/aws-synthetics/lib/canary.ts b/packages/@aws-cdk/aws-synthetics/lib/canary.ts index e5061c4c999c2..c512c4d636a78 100644 --- a/packages/@aws-cdk/aws-synthetics/lib/canary.ts +++ b/packages/@aws-cdk/aws-synthetics/lib/canary.ts @@ -5,6 +5,7 @@ import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { Code } from './code'; +import { Runtime } from './runtime'; import { Schedule } from './schedule'; import { CloudWatchSyntheticsMetrics } from './synthetics-canned-metrics.generated'; import { CfnCanary } from './synthetics.generated'; @@ -64,81 +65,6 @@ export interface CustomTestOptions { readonly handler: string, } -/** - * Runtime options for a canary - */ -export class Runtime { - /** - * `syn-1.0` includes the following: - * - * - Synthetics library 1.0 - * - Synthetics handler code 1.0 - * - Lambda runtime Node.js 10.x - * - Puppeteer-core version 1.14.0 - * - The Chromium version that matches Puppeteer-core 1.14.0 - * - * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-1.0 - */ - public static readonly SYNTHETICS_1_0 = new Runtime('syn-1.0'); - - /** - * `syn-nodejs-2.0` includes the following: - * - Lambda runtime Node.js 10.x - * - Puppeteer-core version 3.3.0 - * - Chromium version 83.0.4103.0 - * - * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-2.0 - */ - public static readonly SYNTHETICS_NODEJS_2_0 = new Runtime('syn-nodejs-2.0'); - - - /** - * `syn-nodejs-2.1` includes the following: - * - Lambda runtime Node.js 10.x - * - Puppeteer-core version 3.3.0 - * - Chromium version 83.0.4103.0 - * - * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-2.1 - */ - public static readonly SYNTHETICS_NODEJS_2_1 = new Runtime('syn-nodejs-2.1'); - - /** - * `syn-nodejs-2.2` includes the following: - * - Lambda runtime Node.js 10.x - * - Puppeteer-core version 3.3.0 - * - Chromium version 83.0.4103.0 - * - * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-2.2 - */ - public static readonly SYNTHETICS_NODEJS_2_2 = new Runtime('syn-nodejs-2.2'); - - /** - * `syn-nodejs-puppeteer-3.0` includes the following: - * - Lambda runtime Node.js 12.x - * - Puppeteer-core version 5.5.0 - * - Chromium version 88.0.4298.0 - * - * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-nodejs-puppeteer-3.0 - */ - public static readonly SYNTHETICS_NODEJS_PUPPETEER_3_0 = new Runtime('syn-nodejs-puppeteer-3.0'); - - /** - * `syn-nodejs-puppeteer-3.1` includes the following: - * - Lambda runtime Node.js 12.x - * - Puppeteer-core version 5.5.0 - * - Chromium version 88.0.4298.0 - * - * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-nodejs-puppeteer-3.1 - */ - public static readonly SYNTHETICS_NODEJS_PUPPETEER_3_1 = new Runtime('syn-nodejs-puppeteer-3.1'); - - /** - * @param name The name of the runtime version - */ - public constructor(public readonly name: string) { - } -} - /** * Options for specifying the s3 location that stores the data of each canary run. The artifacts bucket location **cannot** * be updated once the canary is created. @@ -398,7 +324,7 @@ export class Canary extends cdk.Resource { private createCode(props: CanaryProps): CfnCanary.CodeProperty { const codeConfig = { handler: props.test.handler, - ...props.test.code.bind(this, props.test.handler), + ...props.test.code.bind(this, props.test.handler, props.runtime.family), }; return { handler: codeConfig.handler, diff --git a/packages/@aws-cdk/aws-synthetics/lib/code.ts b/packages/@aws-cdk/aws-synthetics/lib/code.ts index dd75815098ec1..9eef28d4674c6 100644 --- a/packages/@aws-cdk/aws-synthetics/lib/code.ts +++ b/packages/@aws-cdk/aws-synthetics/lib/code.ts @@ -2,7 +2,8 @@ import * as fs from 'fs'; import * as path from 'path'; import * as s3 from '@aws-cdk/aws-s3'; import * as s3_assets from '@aws-cdk/aws-s3-assets'; -import { Construct } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { RuntimeFamily } from './runtime'; /** * The code the canary should execute @@ -56,7 +57,7 @@ export abstract class Code { * * @returns a bound `CodeConfig`. */ - public abstract bind(scope: Construct, handler: string): CodeConfig; + public abstract bind(scope: Construct, handler: string, family: RuntimeFamily): CodeConfig; } /** @@ -95,8 +96,8 @@ export class AssetCode extends Code { } } - public bind(scope: Construct, handler: string): CodeConfig { - this.validateCanaryAsset(handler); + public bind(scope: Construct, handler: string, family: RuntimeFamily): CodeConfig { + this.validateCanaryAsset(handler, family); // If the same AssetCode is used multiple times, retain only the first instantiation. if (!this.asset) { @@ -126,14 +127,19 @@ export class AssetCode extends Code { * * @param handler the canary handler */ - private validateCanaryAsset(handler: string) { + private validateCanaryAsset(handler: string, family: RuntimeFamily) { if (path.extname(this.assetPath) !== '.zip') { if (!fs.lstatSync(this.assetPath).isDirectory()) { throw new Error(`Asset must be a .zip file or a directory (${this.assetPath})`); } - const filename = `${handler.split('.')[0]}.js`; - if (!fs.existsSync(path.join(this.assetPath, 'nodejs', 'node_modules', filename))) { - throw new Error(`The canary resource requires that the handler is present at "nodejs/node_modules/${filename}" but not found at ${this.assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary.html#CloudWatch_Synthetics_Canaries_write_from_scratch)`); + const filename = handler.split('.')[0]; + const nodeFilename = `${filename}.js`; + const pythonFilename = `${filename}.py`; + if (family === RuntimeFamily.NODEJS && !fs.existsSync(path.join(this.assetPath, 'nodejs', 'node_modules', nodeFilename))) { + throw new Error(`The canary resource requires that the handler is present at "nodejs/node_modules/${nodeFilename}" but not found at ${this.assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary_Nodejs.html)`); + } + if (family === RuntimeFamily.PYTHON && !fs.existsSync(path.join(this.assetPath, 'python', pythonFilename))) { + throw new Error(`The canary resource requires that the handler is present at "python/${pythonFilename}" but not found at ${this.assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary_Python.html)`); } } } @@ -151,7 +157,7 @@ export class InlineCode extends Code { } } - public bind(_scope: Construct, handler: string): CodeConfig { + public bind(_scope: Construct, handler: string, _family: RuntimeFamily): CodeConfig { if (handler !== 'index.handler') { throw new Error(`The handler for inline code must be "index.handler" (got "${handler}")`); @@ -171,7 +177,7 @@ export class S3Code extends Code { super(); } - public bind(_scope: Construct, _handler: string): CodeConfig { + public bind(_scope: Construct, _handler: string, _family: RuntimeFamily): CodeConfig { return { s3Location: { bucketName: this.bucket.bucketName, diff --git a/packages/@aws-cdk/aws-synthetics/lib/index.ts b/packages/@aws-cdk/aws-synthetics/lib/index.ts index f769a0309352e..ee024834f2bf1 100644 --- a/packages/@aws-cdk/aws-synthetics/lib/index.ts +++ b/packages/@aws-cdk/aws-synthetics/lib/index.ts @@ -1,5 +1,6 @@ export * from './canary'; export * from './code'; +export * from './runtime'; export * from './schedule'; // AWS::Synthetics CloudFormation Resources: diff --git a/packages/@aws-cdk/aws-synthetics/lib/runtime.ts b/packages/@aws-cdk/aws-synthetics/lib/runtime.ts new file mode 100644 index 0000000000000..c710d68a34e35 --- /dev/null +++ b/packages/@aws-cdk/aws-synthetics/lib/runtime.ts @@ -0,0 +1,123 @@ +/** + * All known Lambda runtime families. + */ +export enum RuntimeFamily { + /** + * All Lambda runtimes that depend on Node.js. + */ + NODEJS, + + /** + * All lambda runtimes that depend on Python. + */ + PYTHON, + + /** + * Any future runtime family. + */ + OTHER, +} + +/** + * Runtime options for a canary + */ +export class Runtime { + /** + * **Deprecated by AWS Synthetics. You can't create canaries with deprecated runtimes.** + * + * `syn-1.0` includes the following: + * + * - Synthetics library 1.0 + * - Synthetics handler code 1.0 + * - Lambda runtime Node.js 10.x + * - Puppeteer-core version 1.14.0 + * - The Chromium version that matches Puppeteer-core 1.14.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-1.0 + */ + public static readonly SYNTHETICS_1_0 = new Runtime('syn-1.0', RuntimeFamily.NODEJS); + + /** + * **Deprecated by AWS Synthetics. You can't create canaries with deprecated runtimes.** + * + * `syn-nodejs-2.0` includes the following: + * - Lambda runtime Node.js 10.x + * - Puppeteer-core version 3.3.0 + * - Chromium version 83.0.4103.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-2.0 + */ + public static readonly SYNTHETICS_NODEJS_2_0 = new Runtime('syn-nodejs-2.0', RuntimeFamily.NODEJS); + + + /** + * **Deprecated by AWS Synthetics. You can't create canaries with deprecated runtimes.** + * + * `syn-nodejs-2.1` includes the following: + * - Lambda runtime Node.js 10.x + * - Puppeteer-core version 3.3.0 + * - Chromium version 83.0.4103.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-2.1 + */ + public static readonly SYNTHETICS_NODEJS_2_1 = new Runtime('syn-nodejs-2.1', RuntimeFamily.NODEJS); + + /** + * **Deprecated by AWS Synthetics. You can't create canaries with deprecated runtimes.** + * + * `syn-nodejs-2.2` includes the following: + * - Lambda runtime Node.js 10.x + * - Puppeteer-core version 3.3.0 + * - Chromium version 83.0.4103.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-2.2 + */ + public static readonly SYNTHETICS_NODEJS_2_2 = new Runtime('syn-nodejs-2.2', RuntimeFamily.NODEJS); + + /** + * `syn-nodejs-puppeteer-3.0` includes the following: + * - Lambda runtime Node.js 12.x + * - Puppeteer-core version 5.5.0 + * - Chromium version 88.0.4298.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-nodejs-puppeteer-3.0 + */ + public static readonly SYNTHETICS_NODEJS_PUPPETEER_3_0 = new Runtime('syn-nodejs-puppeteer-3.0', RuntimeFamily.NODEJS); + + /** + * `syn-nodejs-puppeteer-3.1` includes the following: + * - Lambda runtime Node.js 12.x + * - Puppeteer-core version 5.5.0 + * - Chromium version 88.0.4298.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-nodejs-puppeteer-3.1 + */ + public static readonly SYNTHETICS_NODEJS_PUPPETEER_3_1 = new Runtime('syn-nodejs-puppeteer-3.1', RuntimeFamily.NODEJS); + + /** + * `syn-nodejs-puppeteer-3.2` includes the following: + * - Lambda runtime Node.js 12.x + * - Puppeteer-core version 5.5.0 + * - Chromium version 88.0.4298.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-nodejs-puppeteer-3.2 + */ + public static readonly SYNTHETICS_NODEJS_PUPPETEER_3_2 = new Runtime('syn-nodejs-puppeteer-3.2', RuntimeFamily.NODEJS); + + /** + * `syn-python-selenium-1.0` includes the following: + * - Lambda runtime Python 3.8 + * - Selenium version 3.141.0 + * - Chromium version 83.0.4103.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_python_selenium.html + */ + public static readonly SYNTHETICS_PYTHON_SELENIUM_1_0 = new Runtime('syn-python-selenium-1.0', RuntimeFamily.PYTHON); + + /** + * @param name The name of the runtime version + * @param family The Lambda runtime family + */ + public constructor(public readonly name: string, public readonly family: RuntimeFamily) { + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-synthetics/test/canaries/python/canary.py b/packages/@aws-cdk/aws-synthetics/test/canaries/python/canary.py new file mode 100644 index 0000000000000..2dbed4e312afe --- /dev/null +++ b/packages/@aws-cdk/aws-synthetics/test/canaries/python/canary.py @@ -0,0 +1,61 @@ +# This example comes from the AWS Synthetics service console "API canary" blueprint + +import json +import http.client +import urllib.parse +from aws_synthetics.selenium import synthetics_webdriver as syn_webdriver +from aws_synthetics.common import synthetics_logger as logger + + +def verify_request(method, url, post_data=None, headers={}): + parsed_url = urllib.parse.urlparse(url) + user_agent = str(syn_webdriver.get_canary_user_agent_string()) + if "User-Agent" in headers: + headers["User-Agent"] = " ".join([user_agent, headers["User-Agent"]]) + else: + headers["User-Agent"] = "{}".format(user_agent) + + logger.info("Making request with Method: '%s' URL: %s: Data: %s Headers: %s" % ( + method, url, json.dumps(post_data), json.dumps(headers))) + + if parsed_url.scheme == "https": + conn = http.client.HTTPSConnection(parsed_url.hostname, parsed_url.port) + else: + conn = http.client.HTTPConnection(parsed_url.hostname, parsed_url.port) + + conn.request(method, url, str(post_data), headers) + response = conn.getresponse() + logger.info("Status Code: %s " % response.status) + logger.info("Response Headers: %s" % json.dumps(response.headers.as_string())) + + if not response.status or response.status < 200 or response.status > 299: + try: + logger.error("Response: %s" % response.read().decode()) + finally: + if response.reason: + conn.close() + raise Exception("Failed: %s" % response.reason) + else: + conn.close() + raise Exception("Failed with status code: %s" % response.status) + + logger.info("Response: %s" % response.read().decode()) + logger.info("HTTP request successfully executed") + conn.close() + + +def main(): + + url = 'https://example.com/' + method = 'GET' + postData = "" + headers = {} + + verify_request(method, url, None, headers) + + logger.info("Canary successfully executed") + + +def handler(event, context): + logger.info("Selenium Python API canary") + main() \ No newline at end of file diff --git a/packages/@aws-cdk/aws-synthetics/test/canary.test.ts b/packages/@aws-cdk/aws-synthetics/test/canary.test.ts index 8a0baa8cb0c29..c4583ef5494cf 100644 --- a/packages/@aws-cdk/aws-synthetics/test/canary.test.ts +++ b/packages/@aws-cdk/aws-synthetics/test/canary.test.ts @@ -169,6 +169,25 @@ test('Runtime can be specified', () => { }); }); +test('Python runtime can be specified', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new synthetics.Canary(stack, 'Canary', { + runtime: synthetics.Runtime.SYNTHETICS_PYTHON_SELENIUM_1_0, + test: synthetics.Test.custom({ + handler: 'index.handler', + code: synthetics.Code.fromInline('# Synthetics handler code'), + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Synthetics::Canary', { + RuntimeVersion: 'syn-python-selenium-1.0', + }); +}); + test('environment variables can be specified', () => { // GIVEN const stack = new Stack(); @@ -220,7 +239,7 @@ test('Runtime can be customized', () => { // WHEN new synthetics.Canary(stack, 'Canary', { - runtime: new synthetics.Runtime('fancy-future-runtime-1337.42'), + runtime: new synthetics.Runtime('fancy-future-runtime-1337.42', synthetics.RuntimeFamily.OTHER), test: synthetics.Test.custom({ handler: 'index.handler', code: synthetics.Code.fromInline('/* Synthetics handler code */'), diff --git a/packages/@aws-cdk/aws-synthetics/test/code.test.ts b/packages/@aws-cdk/aws-synthetics/test/code.test.ts index 95c7883b42dbf..84bb7ec61ded1 100644 --- a/packages/@aws-cdk/aws-synthetics/test/code.test.ts +++ b/packages/@aws-cdk/aws-synthetics/test/code.test.ts @@ -3,6 +3,7 @@ import { Template } from '@aws-cdk/assertions'; import * as s3 from '@aws-cdk/aws-s3'; import { App, Stack } from '@aws-cdk/core'; import * as synthetics from '../lib'; +import { RuntimeFamily } from '../lib'; describe(synthetics.Code.fromInline, () => { test('fromInline works', () => { @@ -16,7 +17,7 @@ describe(synthetics.Code.fromInline, () => { };`); // THEN - expect(inline.bind(stack, 'index.handler').inlineCode).toEqual(` + expect(inline.bind(stack, 'index.handler', RuntimeFamily.NODEJS).inlineCode).toEqual(` exports.handler = async () => { console.log(\'hello world\'); };`); @@ -32,13 +33,13 @@ describe(synthetics.Code.fromInline, () => { const stack = new Stack(new App(), 'canaries'); // THEN - expect(() => synthetics.Code.fromInline('code').bind(stack, 'canary.handler')) + expect(() => synthetics.Code.fromInline('code').bind(stack, 'canary.handler', RuntimeFamily.NODEJS)) .toThrowError('The handler for inline code must be "index.handler" (got "canary.handler")'); }); }); describe(synthetics.Code.fromAsset, () => { - test('fromAsset works', () => { + test('fromAsset works for node runtimes', () => { // GIVEN const stack = new Stack(new App(), 'canaries'); @@ -56,8 +57,32 @@ describe(synthetics.Code.fromAsset, () => { Template.fromStack(stack).hasResourceProperties('AWS::Synthetics::Canary', { Code: { Handler: 'canary.handler', - S3Bucket: stack.resolve(directoryAsset.bind(stack, 'canary.handler').s3Location?.bucketName), - S3Key: stack.resolve(directoryAsset.bind(stack, 'canary.handler').s3Location?.objectKey), + S3Bucket: stack.resolve(directoryAsset.bind(stack, 'canary.handler', synthetics.RuntimeFamily.NODEJS).s3Location?.bucketName), + S3Key: stack.resolve(directoryAsset.bind(stack, 'canary.handler', synthetics.RuntimeFamily.NODEJS).s3Location?.objectKey), + }, + }); + }); + + test('fromAsset works for python runtimes', () => { + // GIVEN + const stack = new Stack(new App(), 'canaries'); + + // WHEN + const directoryAsset = synthetics.Code.fromAsset(path.join(__dirname, 'canaries')); + new synthetics.Canary(stack, 'Canary', { + test: synthetics.Test.custom({ + handler: 'canary.handler', + code: directoryAsset, + }), + runtime: synthetics.Runtime.SYNTHETICS_PYTHON_SELENIUM_1_0, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Synthetics::Canary', { + Code: { + Handler: 'canary.handler', + S3Bucket: stack.resolve(directoryAsset.bind(stack, 'canary.handler', synthetics.RuntimeFamily.PYTHON).s3Location?.bucketName), + S3Key: stack.resolve(directoryAsset.bind(stack, 'canary.handler', synthetics.RuntimeFamily.PYTHON).s3Location?.objectKey), }, }); }); @@ -104,18 +129,28 @@ describe(synthetics.Code.fromAsset, () => { // THEN const assetPath = path.join(__dirname, 'canaries', 'nodejs', 'node_modules', 'canary.js'); - expect(() => synthetics.Code.fromAsset(assetPath).bind(stack, 'canary.handler')) + expect(() => synthetics.Code.fromAsset(assetPath).bind(stack, 'canary.handler', synthetics.RuntimeFamily.NODEJS)) .toThrowError(`Asset must be a .zip file or a directory (${assetPath})`); }); - test('fails if "nodejs/node_modules" folder structure not used', () => { + test('fails if node runtime and "nodejs/node_modules" folder structure not used', () => { // GIVEN const stack = new Stack(new App(), 'canaries'); // THEN const assetPath = path.join(__dirname, 'canaries', 'nodejs', 'node_modules'); - expect(() => synthetics.Code.fromAsset(assetPath).bind(stack, 'canary.handler')) - .toThrowError(`The canary resource requires that the handler is present at "nodejs/node_modules/canary.js" but not found at ${assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary.html#CloudWatch_Synthetics_Canaries_write_from_scratch)`); + expect(() => synthetics.Code.fromAsset(assetPath).bind(stack, 'canary.handler', synthetics.RuntimeFamily.NODEJS)) + .toThrowError(`The canary resource requires that the handler is present at "nodejs/node_modules/canary.js" but not found at ${assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary_Nodejs.html)`); + }); + + test('fails if python runtime and "python" folder structure not used', () => { + // GIVEN + const stack = new Stack(new App(), 'canaries'); + + // THEN + const assetPath = path.join(__dirname, 'canaries', 'python'); + expect(() => synthetics.Code.fromAsset(assetPath).bind(stack, 'canary.handler', synthetics.RuntimeFamily.PYTHON)) + .toThrowError(`The canary resource requires that the handler is present at "python/canary.py" but not found at ${assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary_Python.html)`); }); test('fails if handler is specified incorrectly', () => { @@ -124,8 +159,8 @@ describe(synthetics.Code.fromAsset, () => { // THEN const assetPath = path.join(__dirname, 'canaries', 'nodejs', 'node_modules'); - expect(() => synthetics.Code.fromAsset(assetPath).bind(stack, 'incorrect.handler')) - .toThrowError(`The canary resource requires that the handler is present at "nodejs/node_modules/incorrect.js" but not found at ${assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary.html#CloudWatch_Synthetics_Canaries_write_from_scratch)`); + expect(() => synthetics.Code.fromAsset(assetPath).bind(stack, 'incorrect.handler', synthetics.RuntimeFamily.NODEJS)) + .toThrowError(`The canary resource requires that the handler is present at "nodejs/node_modules/incorrect.js" but not found at ${assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary_Nodejs.html)`); }); }); @@ -138,7 +173,7 @@ describe(synthetics.Code.fromBucket, () => { // WHEN const code = synthetics.Code.fromBucket(bucket, 'code.js'); - const codeConfig = code.bind(stack, 'code.handler'); + const codeConfig = code.bind(stack, 'code.handler', RuntimeFamily.NODEJS); // THEN expect(codeConfig.s3Location?.bucketName).toEqual(bucket.bucketName); diff --git a/packages/@aws-cdk/aws-synthetics/test/integ.canary.expected.json b/packages/@aws-cdk/aws-synthetics/test/integ.canary.expected.json index 58412fee9bfbb..70d3908aa07f6 100644 --- a/packages/@aws-cdk/aws-synthetics/test/integ.canary.expected.json +++ b/packages/@aws-cdk/aws-synthetics/test/integ.canary.expected.json @@ -114,7 +114,7 @@ ] }, "Name": "canary-integ", - "RuntimeVersion": "syn-nodejs-2.0", + "RuntimeVersion": "syn-nodejs-puppeteer-3.2", "Schedule": { "DurationInSeconds": "0", "Expression": "rate(1 minute)" @@ -238,7 +238,7 @@ "Code": { "Handler": "canary.handler", "S3Bucket": { - "Ref": "AssetParameters5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529bS3Bucket58589EB6" + "Ref": "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbS3Bucket59F507C2" }, "S3Key": { "Fn::Join": [ @@ -251,7 +251,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529bS3VersionKey8FF13E90" + "Ref": "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbS3VersionKeyEFB5FFF8" } ] } @@ -264,7 +264,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529bS3VersionKey8FF13E90" + "Ref": "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbS3VersionKeyEFB5FFF8" } ] } @@ -281,7 +281,7 @@ ] }, "Name": "assetcanary-one", - "RuntimeVersion": "syn-nodejs-2.0", + "RuntimeVersion": "syn-nodejs-puppeteer-3.2", "Schedule": { "DurationInSeconds": "0", "Expression": "rate(5 minutes)" @@ -448,7 +448,174 @@ ] }, "Name": "assetcanary-two", - "RuntimeVersion": "syn-nodejs-2.0", + "RuntimeVersion": "syn-nodejs-puppeteer-3.2", + "Schedule": { + "DurationInSeconds": "0", + "Expression": "rate(5 minutes)" + }, + "StartCanaryAfterCreation": true + } + }, + "MyPythonCanaryArtifactsBucket7AE88133": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "aws:kms" + } + } + ] + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "MyPythonCanaryServiceRole41A363E1": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:ListAllMyBuckets", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "s3:PutObject", + "s3:GetBucketLocation" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyPythonCanaryArtifactsBucket7AE88133", + "Arn" + ] + }, + "/*" + ] + ] + } + }, + { + "Action": "cloudwatch:PutMetricData", + "Condition": { + "StringEquals": { + "cloudwatch:namespace": "CloudWatchSynthetics" + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "logs:CreateLogStream", + "logs:CreateLogGroup", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:::*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "canaryPolicy" + } + ] + } + }, + "MyPythonCanary9A3DE09E": { + "Type": "AWS::Synthetics::Canary", + "Properties": { + "ArtifactS3Location": { + "Fn::Join": [ + "", + [ + "s3://", + { + "Ref": "MyPythonCanaryArtifactsBucket7AE88133" + } + ] + ] + }, + "Code": { + "Handler": "canary.handler", + "S3Bucket": { + "Ref": "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbS3Bucket59F507C2" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbS3VersionKeyEFB5FFF8" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbS3VersionKeyEFB5FFF8" + } + ] + } + ] + } + ] + ] + } + }, + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "MyPythonCanaryServiceRole41A363E1", + "Arn" + ] + }, + "Name": "py-canary-integ", + "RuntimeVersion": "syn-python-selenium-1.0", "Schedule": { "DurationInSeconds": "0", "Expression": "rate(5 minutes)" @@ -458,17 +625,17 @@ } }, "Parameters": { - "AssetParameters5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529bS3Bucket58589EB6": { + "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbS3Bucket59F507C2": { "Type": "String", - "Description": "S3 bucket for asset \"5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529b\"" + "Description": "S3 bucket for asset \"9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bb\"" }, - "AssetParameters5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529bS3VersionKey8FF13E90": { + "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbS3VersionKeyEFB5FFF8": { "Type": "String", - "Description": "S3 key for asset version \"5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529b\"" + "Description": "S3 key for asset version \"9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bb\"" }, - "AssetParameters5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529bArtifactHash74DCED3D": { + "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbArtifactHash0E2FE9D4": { "Type": "String", - "Description": "Artifact hash for asset \"5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529b\"" + "Description": "Artifact hash for asset \"9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bb\"" }, "AssetParametersb1b777dcb79a2fa2790059927207d10bf5f4747d6dd1516e2780726d9d6fa820S3Bucket705C3761": { "Type": "String", diff --git a/packages/@aws-cdk/aws-synthetics/test/integ.canary.ts b/packages/@aws-cdk/aws-synthetics/test/integ.canary.ts index cbb3a505889ad..54822badf1c99 100644 --- a/packages/@aws-cdk/aws-synthetics/test/integ.canary.ts +++ b/packages/@aws-cdk/aws-synthetics/test/integ.canary.ts @@ -29,7 +29,7 @@ new synthetics.Canary(stack, 'MyCanary', { }), schedule: synthetics.Schedule.rate(cdk.Duration.minutes(1)), artifactsBucketLocation: { bucket, prefix }, - runtime: synthetics.Runtime.SYNTHETICS_NODEJS_2_0, + runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_3_2, }); new synthetics.Canary(stack, 'MyCanaryOne', { @@ -38,7 +38,7 @@ new synthetics.Canary(stack, 'MyCanaryOne', { handler: 'canary.handler', code: synthetics.Code.fromAsset(path.join(__dirname, 'canaries')), }), - runtime: synthetics.Runtime.SYNTHETICS_NODEJS_2_0, + runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_3_2, }); new synthetics.Canary(stack, 'MyCanaryTwo', { @@ -47,7 +47,16 @@ new synthetics.Canary(stack, 'MyCanaryTwo', { handler: 'canary.handler', code: synthetics.Code.fromAsset(path.join(__dirname, 'canary.zip')), }), - runtime: synthetics.Runtime.SYNTHETICS_NODEJS_2_0, + runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_3_2, +}); + +new synthetics.Canary(stack, 'MyPythonCanary', { + canaryName: 'py-canary-integ', + test: synthetics.Test.custom({ + handler: 'canary.handler', + code: synthetics.Code.fromAsset(path.join(__dirname, 'canaries')), + }), + runtime: synthetics.Runtime.SYNTHETICS_PYTHON_SELENIUM_1_0, }); app.synth(); From a85ad392459c815d5c8e645dd3e8240d059024e6 Mon Sep 17 00:00:00 2001 From: Massimo Prencipe Date: Fri, 27 Aug 2021 19:29:36 +0300 Subject: [PATCH 34/91] fix(elasticloadbalancingv2): target group health check does not validate interval versus timeout (#16107) fix: Add validation to target group health check creation. Fixes issue #3703. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/shared/base-target-group.ts | 5 +++ .../test/alb/listener.test.ts | 12 +++--- .../test/alb/target-group.test.ts | 41 +++++++++++++++++++ 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts index 175f63ddc4d3d..96acd45b34a4c 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts @@ -297,6 +297,11 @@ export abstract class TargetGroupBase extends CoreConstruct implements ITargetGr * Set/replace the target group's health check */ public configureHealthCheck(healthCheck: HealthCheck) { + if (healthCheck.interval && healthCheck.timeout) { + if (healthCheck.interval.toMilliseconds() <= healthCheck.timeout.toMilliseconds()) { + throw new Error(`Healthcheck interval ${healthCheck.interval.toHumanString()} must be greater than the timeout ${healthCheck.timeout.toHumanString()}`); + } + } this.healthCheck = healthCheck; } 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 2d945aeb2621f..885c872482e9c 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts @@ -437,17 +437,17 @@ describe('tests', () => { }); group.configureHealthCheck({ unhealthyThresholdCount: 3, - timeout: cdk.Duration.hours(1), - interval: cdk.Duration.seconds(30), + timeout: cdk.Duration.seconds(30), + interval: cdk.Duration.seconds(60), path: '/test', }); // THEN expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { UnhealthyThresholdCount: 3, - HealthCheckIntervalSeconds: 30, + HealthCheckIntervalSeconds: 60, HealthCheckPath: '/test', - HealthCheckTimeoutSeconds: 3600, + HealthCheckTimeoutSeconds: 30, }); }); @@ -466,8 +466,8 @@ describe('tests', () => { group.configureHealthCheck({ unhealthyThresholdCount: 3, - timeout: cdk.Duration.hours(1), - interval: cdk.Duration.seconds(30), + timeout: cdk.Duration.seconds(30), + interval: cdk.Duration.seconds(60), path: '/test', protocol: elbv2.Protocol.TCP, }); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts index 97c36c33ce237..f1a3db5eb9508 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts @@ -281,4 +281,45 @@ describe('tests', () => { }).toThrow(/Slow start duration value must be between 30 and 900 seconds./); }); }); + + test('Interval equal to timeout', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const vpc = new ec2.Vpc(stack, 'VPC', {}); + + // WHEN + const tg = new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', { + vpc, + }); + + // THEN + expect(() => { + tg.configureHealthCheck({ + interval: cdk.Duration.seconds(60), + timeout: cdk.Duration.seconds(60), + }); + }).toThrow(/Healthcheck interval 1 minute must be greater than the timeout 1 minute/); + }); + + test('Interval smaller than timeout', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const vpc = new ec2.Vpc(stack, 'VPC', {}); + + // WHEN + const tg = new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', { + vpc, + }); + + // THEN + expect(() => { + tg.configureHealthCheck({ + interval: cdk.Duration.seconds(60), + timeout: cdk.Duration.seconds(120), + }); + }).toThrow(/Healthcheck interval 1 minute must be greater than the timeout 2 minutes/); + }); + }); From f1bf935c47006096b33fb7bf0c847ffab9230870 Mon Sep 17 00:00:00 2001 From: "Michael S. Fischer" Date: Fri, 27 Aug 2021 10:09:42 -0700 Subject: [PATCH 35/91] feat(ecs): add support for Fargate PV1.4 ephemeral storage (#15440) Add support for ephemeral storage on Fargate PV 1.4.0 or later. Closes #14570 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-ecs/README.md | 11 +++++++ .../aws-ecs/lib/base/task-definition.ts | 21 +++++++++++++ .../lib/fargate/fargate-task-definition.ts | 20 +++++++++++++ .../fargate/fargate-task-definition.test.ts | 30 +++++++++++++++++++ 4 files changed, 82 insertions(+) diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 1d8a10f4deb8c..5454d93e61095 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -231,6 +231,17 @@ const fargateTaskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', { }); ``` +On Fargate Platform Version 1.4.0 or later, you may specify up to 200GiB of +[ephemeral storage](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/fargate-task-storage.html#fargate-task-storage-pv14): + +```ts +const fargateTaskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', { + memoryLimitMiB: 512, + cpu: 256, + ephemeralStorageGiB: 100 +}); +``` + To add containers to a task definition, call `addContainer()`: ```ts diff --git a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts index 91c019d9fa1f1..9521142e650c5 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts @@ -199,6 +199,15 @@ export interface TaskDefinitionProps extends CommonTaskDefinitionProps { * @default - No inference accelerators. */ readonly inferenceAccelerators?: InferenceAccelerator[]; + + /** + * The amount (in GiB) of ephemeral storage to be allocated to the task. + * + * Only supported in Fargate platform version 1.4.0 or later. + * + * @default - Undefined, in which case, the task will receive 20GiB ephemeral storage. + */ + readonly ephemeralStorageGiB?: number; } /** @@ -329,6 +338,13 @@ export class TaskDefinition extends TaskDefinitionBase { */ public readonly compatibility: Compatibility; + /** + * The amount (in GiB) of ephemeral storage to be allocated to the task. + * + * Only supported in Fargate platform version 1.4.0 or later. + */ + public readonly ephemeralStorageGiB?: number; + /** * The container definitions. */ @@ -399,6 +415,8 @@ export class TaskDefinition extends TaskDefinitionBase { props.inferenceAccelerators.forEach(ia => this.addInferenceAccelerator(ia)); } + this.ephemeralStorageGiB = props.ephemeralStorageGiB; + const taskDef = new CfnTaskDefinition(this, 'Resource', { containerDefinitions: Lazy.any({ produce: () => this.renderContainers() }, { omitEmptyArray: true }), volumes: Lazy.any({ produce: () => this.renderVolumes() }, { omitEmptyArray: true }), @@ -424,6 +442,9 @@ export class TaskDefinition extends TaskDefinitionBase { produce: () => !isFargateCompatible(this.compatibility) ? this.renderInferenceAccelerators() : undefined, }, { omitEmptyArray: true }), + ephemeralStorage: this.ephemeralStorageGiB ? { + sizeInGiB: this.ephemeralStorageGiB, + } : undefined, }); if (props.placementConstraints) { diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts index ccf3291859708..803ec6a449969 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts @@ -50,6 +50,15 @@ export interface FargateTaskDefinitionProps extends CommonTaskDefinitionProps { * @default 512 */ readonly memoryLimitMiB?: number; + + /** + * The amount (in GiB) of ephemeral storage to be allocated to the task. The maximum supported value is 200 GiB. + * + * NOTE: This parameter is only supported for tasks hosted on AWS Fargate using platform version 1.4.0 or later. + * + * @default 20 + */ + readonly ephemeralStorageGiB?: number; } /** @@ -104,6 +113,11 @@ export class FargateTaskDefinition extends TaskDefinition implements IFargateTas // we need to explicitly write the type here, as type deduction for enums won't lead to // the import being generated in the .d.ts file. + /** + * The amount (in GiB) of ephemeral storage to be allocated to the task. + */ + public readonly ephemeralStorageGiB?: number; + /** * Constructs a new instance of the FargateTaskDefinition class. */ @@ -115,5 +129,11 @@ export class FargateTaskDefinition extends TaskDefinition implements IFargateTas compatibility: Compatibility.FARGATE, networkMode: NetworkMode.AWS_VPC, }); + + if (props.ephemeralStorageGiB && (props.ephemeralStorageGiB < 21 || props.ephemeralStorageGiB > 200)) { + throw new Error('Ephemeral storage size must be between 21GiB and 200GiB'); + } + + this.ephemeralStorageGiB = props.ephemeralStorageGiB; } } diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-task-definition.test.ts b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-task-definition.test.ts index 9cd5d994c9555..294ae3001aa64 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-task-definition.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-task-definition.test.ts @@ -58,6 +58,7 @@ nodeunitShim({ taskRole: new iam.Role(stack, 'TaskRole', { assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), }), + ephemeralStorageGiB: 21, }); taskDefinition.addVolume({ @@ -76,6 +77,9 @@ nodeunitShim({ 'Arn', ], }, + EphemeralStorage: { + SizeInGiB: 21, + }, Family: 'myApp', Memory: '1024', NetworkMode: 'awsvpc', @@ -131,6 +135,32 @@ nodeunitShim({ test.done(); }, + + 'throws when ephemeral storage request is too high'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + test.throws(() => { + new ecs.FargateTaskDefinition(stack, 'FargateTaskDef', { + ephemeralStorageGiB: 201, + }); + }, /Ephemeral storage size must be between 21GiB and 200GiB/); + + // THEN + test.done(); + }, + + 'throws when ephemeral storage request is too low'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + test.throws(() => { + new ecs.FargateTaskDefinition(stack, 'FargateTaskDef', { + ephemeralStorageGiB: 20, + }); + }, /Ephemeral storage size must be between 21GiB and 200GiB/); + + // THEN + test.done(); + }, }, 'When importing from an existing Fargate TaskDefinition': { From d5ca419448e84f0cbb25dbd90d48fb4c407ede5c Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Sat, 28 Aug 2021 15:48:53 +0100 Subject: [PATCH 36/91] chore: migrate a few modules from nodeunit-shim to jest (#16258) Part of the the effort to homogenize our testing environment in the AWS CDK repo involves removing the nodeunit-shim and using standard jest. This change applies this to a few modules. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-autoscaling/package.json | 2 +- .../test/auto-scaling-group.test.ts | 476 +++++----- .../aws-autoscaling/test/cfn-init.test.ts | 1 + .../test/lifecyclehooks.test.ts | 10 +- .../aws-autoscaling/test/scaling.test.ts | 50 +- .../test/scheduled-action.test.ts | 22 +- .../aws-codepipeline-actions/package.json | 2 +- .../bitbucket/bitbucket-source-action.test.ts | 50 +- .../cloudformation-pipeline-actions.test.ts | 134 ++- .../cloudformation/pipeline-actions.test.ts | 104 +-- .../test/codebuild/codebuild-action.test.ts | 83 +- .../codecommit-source-action.test.ts | 146 +-- .../test/codedeploy/ecs-deploy-action.test.ts | 79 +- ...codestar-connections-source-action.test.ts | 50 +- .../test/ecr/ecr-source-action.test.ts | 19 +- .../test/ecs/ecs-deploy-action.test.ts | 95 +- .../test/github/github-source-action.test.ts | 50 +- .../test/manual-approval.test.ts | 47 +- .../test/pipeline.test.ts | 239 ++--- .../test/s3/s3-source-action.test.ts | 103 ++- ...servicecatalog-deploy-action-beta1.test.ts | 25 +- .../stepfunctions-invoke-actions.test.ts | 43 +- packages/@aws-cdk/aws-ecs/package.json | 2 +- .../test/app-mesh-proxy-configuration.test.ts | 45 +- .../aws-ecs/test/aws-log-driver.test.ts | 71 +- .../@aws-cdk/aws-ecs/test/cluster.test.ts | 716 ++++++++------- .../aws-ecs/test/ec2/cross-stack.test.ts | 39 +- .../aws-ecs/test/ec2/ec2-service.test.ts | 834 +++++++++--------- .../aws-ecs/test/environment-file.test.ts | 24 +- .../test/external/external-service.test.ts | 99 +-- .../external/external-task-definition.test.ts | 73 +- .../test/fargate/fargate-service.test.ts | 660 +++++++------- .../fargate/fargate-task-definition.test.ts | 127 ++- .../aws-ecs/test/firelens-log-driver.test.ts | 89 +- .../aws-ecs/test/fluentd-log-driver.test.ts | 51 +- .../aws-ecs/test/gelf-log-driver.test.ts | 31 +- .../tag-parameter-container-image.test.ts | 48 +- .../aws-ecs/test/journald-log-driver.test.ts | 41 +- .../aws-ecs/test/json-file-log-driver.test.ts | 41 +- .../aws-ecs/test/splunk-log-driver.test.ts | 73 +- .../aws-ecs/test/syslog-log-driver.test.ts | 41 +- .../aws-ecs/test/task-definition.test.ts | 71 +- packages/@aws-cdk/aws-route53/package.json | 1 - .../test/hosted-zone-provider.test.ts | 30 +- .../aws-route53/test/hosted-zone.test.ts | 43 +- .../aws-route53/test/record-set.test.ts | 234 ++--- .../@aws-cdk/aws-route53/test/route53.test.ts | 106 +-- .../@aws-cdk/aws-route53/test/util.test.ts | 49 +- .../vpc-endpoint-service-domain-name.test.ts | 1 - packages/@aws-cdk/aws-s3/package.json | 2 +- packages/@aws-cdk/aws-s3/test/aspect.test.ts | 21 +- .../aws-s3/test/bucket-policy.test.ts | 47 +- packages/@aws-cdk/aws-s3/test/cors.test.ts | 25 +- packages/@aws-cdk/aws-s3/test/metrics.test.ts | 45 +- .../@aws-cdk/aws-s3/test/notification.test.ts | 58 +- packages/@aws-cdk/aws-s3/test/rules.test.ts | 65 +- packages/@aws-cdk/aws-s3/test/util.test.ts | 69 +- 57 files changed, 2891 insertions(+), 2911 deletions(-) diff --git a/packages/@aws-cdk/aws-autoscaling/package.json b/packages/@aws-cdk/aws-autoscaling/package.json index c2ece82fdb8b0..67fb9d4e1dd7e 100644 --- a/packages/@aws-cdk/aws-autoscaling/package.json +++ b/packages/@aws-cdk/aws-autoscaling/package.json @@ -78,7 +78,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "nodeunit-shim": "0.0.0", + "jest": "^26.6.3", "pkglint": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" diff --git a/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts b/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts index 9fb5da7b46ce8..64795593e8ec4 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts @@ -1,17 +1,17 @@ -import { ABSENT, expect, haveResource, haveResourceLike, InspectionFailure, ResourcePart } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { ABSENT, InspectionFailure, ResourcePart } from '@aws-cdk/assert-internal'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as sns from '@aws-cdk/aws-sns'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as autoscaling from '../lib'; /* eslint-disable quote-props */ -nodeunitShim({ - 'default fleet'(test: Test) { +describe('auto scaling group', () => { + test('default fleet', () => { const stack = getTestStack(); const vpc = mockVpc(stack); @@ -21,7 +21,7 @@ nodeunitShim({ vpc, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Parameters': { 'SsmParameterValueawsserviceamiamazonlinuxlatestamznamihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter': { 'Type': 'AWS::SSM::Parameter::Value', @@ -136,10 +136,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 'can set minCapacity, maxCapacity, desiredCapacity to 0'(test: Test) { + }); + + test('can set minCapacity, maxCapacity, desiredCapacity to 0', () => { const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' } }); const vpc = mockVpc(stack); @@ -152,17 +152,17 @@ nodeunitShim({ desiredCapacity: 0, }); - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MinSize: '0', MaxSize: '0', DesiredCapacity: '0', }, - )); + ); - test.done(); - }, - 'validation is not performed when using Tokens'(test: Test) { + }); + + test('validation is not performed when using Tokens', () => { const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' } }); const vpc = mockVpc(stack); @@ -176,17 +176,17 @@ nodeunitShim({ }); // THEN: no exception - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MinSize: '5', MaxSize: '1', DesiredCapacity: '20', }, - )); + ); - test.done(); - }, - 'userdata can be overridden by image'(test: Test) { + }); + + test('userdata can be overridden by image', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -204,12 +204,12 @@ nodeunitShim({ }); // THEN - test.equals(asg.userData.render(), '#!/bin/bash\nit me!'); + expect(asg.userData.render()).toEqual('#!/bin/bash\nit me!'); + - test.done(); - }, + }); - 'userdata can be overridden at ASG directly'(test: Test) { + test('userdata can be overridden at ASG directly', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -231,12 +231,12 @@ nodeunitShim({ }); // THEN - test.equals(asg.userData.render(), '#!/bin/bash\nno me!'); + expect(asg.userData.render()).toEqual('#!/bin/bash\nno me!'); - test.done(); - }, - 'can specify only min capacity'(test: Test) { + }); + + test('can specify only min capacity', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -250,16 +250,16 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MinSize: '10', MaxSize: '10', }, - )); + ); + - test.done(); - }, + }); - 'can specify only max capacity'(test: Test) { + test('can specify only max capacity', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -273,16 +273,16 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MinSize: '1', MaxSize: '10', }, - )); + ); + - test.done(); - }, + }); - 'can specify only desiredCount'(test: Test) { + test('can specify only desiredCount', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -296,17 +296,17 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MinSize: '1', MaxSize: '10', DesiredCapacity: '10', }, - )); + ); + - test.done(); - }, + }); - 'addToRolePolicy can be used to add statements to the role policy'(test: Test) { + test('addToRolePolicy can be used to add statements to the role policy', () => { const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' } }); const vpc = mockVpc(stack); @@ -321,7 +321,7 @@ nodeunitShim({ resources: ['*'], })); - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -332,11 +332,11 @@ nodeunitShim({ }, ], }, - })); - test.done(); - }, + }); + + }); - 'can configure replacing update'(test: Test) { + test('can configure replacing update', () => { // GIVEN const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' } }); const vpc = mockVpc(stack); @@ -351,7 +351,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { UpdatePolicy: { AutoScalingReplacingUpdate: { WillReplace: true, @@ -362,12 +362,12 @@ nodeunitShim({ MinSuccessfulInstancesPercent: 50, }, }, - }, ResourcePart.CompleteDefinition)); + }, ResourcePart.CompleteDefinition); - test.done(); - }, - 'can configure rolling update'(test: Test) { + }); + + test('can configure rolling update', () => { // GIVEN const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' } }); const vpc = mockVpc(stack); @@ -385,7 +385,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { UpdatePolicy: { 'AutoScalingRollingUpdate': { 'MinSuccessfulInstancesPercent': 50, @@ -394,12 +394,12 @@ nodeunitShim({ 'SuspendProcesses': ['HealthCheck', 'ReplaceUnhealthy', 'AZRebalance', 'AlarmNotification', 'ScheduledActions'], }, }, - }, ResourcePart.CompleteDefinition)); + }, ResourcePart.CompleteDefinition); - test.done(); - }, - 'can configure resource signals'(test: Test) { + }); + + test('can configure resource signals', () => { // GIVEN const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' } }); const vpc = mockVpc(stack); @@ -414,19 +414,19 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { CreationPolicy: { ResourceSignal: { Count: 5, Timeout: 'PT11M6S', }, }, - }, ResourcePart.CompleteDefinition)); + }, ResourcePart.CompleteDefinition); - test.done(); - }, - 'can configure EC2 health check'(test: Test) { + }); + + test('can configure EC2 health check', () => { // GIVEN const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' } }); const vpc = mockVpc(stack); @@ -440,14 +440,14 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { HealthCheckType: 'EC2', - })); + }); - test.done(); - }, - 'can configure EBS health check'(test: Test) { + }); + + test('can configure EBS health check', () => { // GIVEN const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' } }); const vpc = mockVpc(stack); @@ -461,15 +461,15 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { HealthCheckType: 'ELB', HealthCheckGracePeriod: 900, - })); + }); - test.done(); - }, - 'can add Security Group to Fleet'(test: Test) { + }); + + test('can add Security Group to Fleet', () => { // GIVEN const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' } }); const vpc = mockVpc(stack); @@ -481,7 +481,7 @@ nodeunitShim({ vpc, }); asg.addSecurityGroup(mockSecurityGroup(stack)); - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { SecurityGroups: [ { 'Fn::GetAtt': [ @@ -491,11 +491,11 @@ nodeunitShim({ }, 'most-secure', ], - })); - test.done(); - }, + }); - 'can set tags'(test: Test) { + }); + + test('can set tags', () => { // GIVEN const stack = getTestStack(); // new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' }}); @@ -517,7 +517,7 @@ nodeunitShim({ cdk.Tags.of(asg).add('notsuper', 'caramel', { applyToLaunchedInstances: false }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { Tags: [ { Key: 'Name', @@ -535,11 +535,11 @@ nodeunitShim({ Value: 'acai', }, ], - })); - test.done(); - }, + }); + + }); - 'allows setting spot price'(test: Test) { + test('allows setting spot price', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -554,15 +554,15 @@ nodeunitShim({ }); // THEN - test.deepEqual(asg.spotPrice, '0.05'); - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(asg.spotPrice).toEqual('0.05'); + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { SpotPrice: '0.05', - })); + }); + - test.done(); - }, + }); - 'allows association of public IP address'(test: Test) { + test('allows association of public IP address', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -581,20 +581,20 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { AssociatePublicIpAddress: true, }, - )); - test.done(); - }, + ); + + }); - 'association of public IP address requires public subnet'(test: Test) { + test('association of public IP address requires public subnet', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); // WHEN - test.throws(() => { + expect(() => { new autoscaling.AutoScalingGroup(stack, 'MyStack', { instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), machineImage: new ec2.AmazonLinuxImage(), @@ -604,11 +604,11 @@ nodeunitShim({ desiredCapacity: 0, associatePublicIpAddress: true, }); - }); - test.done(); - }, + }).toThrow(); + + }); - 'allows disassociation of public IP address'(test: Test) { + test('allows disassociation of public IP address', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -625,14 +625,14 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { AssociatePublicIpAddress: false, }, - )); - test.done(); - }, + ); - 'does not specify public IP address association by default'(test: Test) { + }); + + test('does not specify public IP address association by default', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -648,7 +648,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', (resource: any, errors: InspectionFailure) => { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', (resource: any, errors: InspectionFailure) => { for (const key of Object.keys(resource)) { if (key === 'AssociatePublicIpAddress') { errors.failureReason = 'Has AssociatePublicIpAddress'; @@ -656,11 +656,11 @@ nodeunitShim({ } } return true; - })); - test.done(); - }, + }); - 'an existing security group can be specified instead of auto-created'(test: Test) { + }); + + test('an existing security group can be specified instead of auto-created', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -675,14 +675,14 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { SecurityGroups: ['most-secure'], }, - )); - test.done(); - }, + ); + + }); - 'an existing role can be specified instead of auto-created'(test: Test) { + test('an existing role can be specified instead of auto-created', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -697,14 +697,14 @@ nodeunitShim({ }); // THEN - test.equal(asg.role, importedRole); - expect(stack).to(haveResource('AWS::IAM::InstanceProfile', { + expect(asg.role).toEqual(importedRole); + expect(stack).toHaveResource('AWS::IAM::InstanceProfile', { 'Roles': ['HelloDude'], - })); - test.done(); - }, + }); + + }); - 'defaultChild is available on an ASG'(test: Test) { + test('defaultChild is available on an ASG', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -715,12 +715,12 @@ nodeunitShim({ }); // THEN - test.ok(asg.node.defaultChild instanceof autoscaling.CfnAutoScalingGroup); + expect(asg.node.defaultChild instanceof autoscaling.CfnAutoScalingGroup).toEqual(true); + - test.done(); - }, + }); - 'can set blockDeviceMappings'(test: Test) { + test('can set blockDeviceMappings', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -758,7 +758,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { BlockDeviceMappings: [ { DeviceName: 'ebs', @@ -795,12 +795,12 @@ nodeunitShim({ NoDevice: true, }, ], - })); + }); - test.done(); - }, - 'can configure maxInstanceLifetime'(test: Test) { + }); + + test('can configure maxInstanceLifetime', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -812,50 +812,50 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { 'MaxInstanceLifetime': 604800, - })); + }); - test.done(); - }, - 'throws if maxInstanceLifetime < 7 days'(test: Test) { + }); + + test('throws if maxInstanceLifetime < 7 days', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); // THEN - test.throws(() => { + expect(() => { new autoscaling.AutoScalingGroup(stack, 'MyStack', { instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), machineImage: new ec2.AmazonLinuxImage(), vpc, maxInstanceLifetime: cdk.Duration.days(6), }); - }, /maxInstanceLifetime must be between 7 and 365 days \(inclusive\)/); + }).toThrow(/maxInstanceLifetime must be between 7 and 365 days \(inclusive\)/); - test.done(); - }, - 'throws if maxInstanceLifetime > 365 days'(test: Test) { + }); + + test('throws if maxInstanceLifetime > 365 days', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); // THEN - test.throws(() => { + expect(() => { new autoscaling.AutoScalingGroup(stack, 'MyStack', { instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), machineImage: new ec2.AmazonLinuxImage(), vpc, maxInstanceLifetime: cdk.Duration.days(366), }); - }, /maxInstanceLifetime must be between 7 and 365 days \(inclusive\)/); + }).toThrow(/maxInstanceLifetime must be between 7 and 365 days \(inclusive\)/); - test.done(); - }, - 'can configure instance monitoring'(test: Test) { + }); + + test('can configure instance monitoring', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -869,13 +869,13 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { InstanceMonitoring: false, - })); - test.done(); - }, + }); + + }); - 'instance monitoring defaults to absent'(test: Test) { + test('instance monitoring defaults to absent', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -888,19 +888,19 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { InstanceMonitoring: ABSENT, - })); - test.done(); - }, + }); + + }); - 'throws if ephemeral volumeIndex < 0'(test: Test) { + test('throws if ephemeral volumeIndex < 0', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); // THEN - test.throws(() => { + expect(() => { new autoscaling.AutoScalingGroup(stack, 'MyStack', { instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), machineImage: new ec2.AmazonLinuxImage(), @@ -910,18 +910,18 @@ nodeunitShim({ volume: autoscaling.BlockDeviceVolume.ephemeral(-1), }], }); - }, /volumeIndex must be a number starting from 0/); + }).toThrow(/volumeIndex must be a number starting from 0/); - test.done(); - }, - 'throws if volumeType === IO1 without iops'(test: Test) { + }); + + test('throws if volumeType === IO1 without iops', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); // THEN - test.throws(() => { + expect(() => { new autoscaling.AutoScalingGroup(stack, 'MyStack', { instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), machineImage: new ec2.AmazonLinuxImage(), @@ -935,12 +935,12 @@ nodeunitShim({ }), }], }); - }, /ops property is required with volumeType: EbsDeviceVolumeType.IO1/); + }).toThrow(/ops property is required with volumeType: EbsDeviceVolumeType.IO1/); - test.done(); - }, - 'warning if iops without volumeType'(test: Test) { + }); + + test('warning if iops without volumeType', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -960,13 +960,13 @@ nodeunitShim({ }); // THEN - test.deepEqual(asg.node.metadata[0].type, cxschema.ArtifactMetadataEntryType.WARN); - test.deepEqual(asg.node.metadata[0].data, 'iops will be ignored without volumeType: EbsDeviceVolumeType.IO1'); + expect(asg.node.metadata[0].type).toEqual(cxschema.ArtifactMetadataEntryType.WARN); + expect(asg.node.metadata[0].data).toEqual('iops will be ignored without volumeType: EbsDeviceVolumeType.IO1'); - test.done(); - }, - 'warning if iops and volumeType !== IO1'(test: Test) { + }); + + test('warning if iops and volumeType !== IO1', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -987,13 +987,13 @@ nodeunitShim({ }); // THEN - test.deepEqual(asg.node.metadata[0].type, cxschema.ArtifactMetadataEntryType.WARN); - test.deepEqual(asg.node.metadata[0].data, 'iops will be ignored without volumeType: EbsDeviceVolumeType.IO1'); + expect(asg.node.metadata[0].type).toEqual(cxschema.ArtifactMetadataEntryType.WARN); + expect(asg.node.metadata[0].data).toEqual('iops will be ignored without volumeType: EbsDeviceVolumeType.IO1'); + - test.done(); - }, + }); - 'step scaling on metric'(test: Test) { + test('step scaling on metric', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -1018,18 +1018,18 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { ComparisonOperator: 'LessThanOrEqualToThreshold', EvaluationPeriods: 1, MetricName: 'Metric', Namespace: 'Test', Period: 300, - })); + }); + - test.done(); - }, + }); - 'step scaling on MathExpression'(test: Test) { + test('step scaling on MathExpression', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -1059,11 +1059,11 @@ nodeunitShim({ }); // THEN - expect(stack).notTo(haveResource('AWS::CloudWatch::Alarm', { + expect(stack).not.toHaveResource('AWS::CloudWatch::Alarm', { Period: 60, - })); + }); - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { 'ComparisonOperator': 'LessThanOrEqualToThreshold', 'EvaluationPeriods': 1, 'Metrics': [ @@ -1085,12 +1085,12 @@ nodeunitShim({ }, ], 'Threshold': 49, - })); + }); - test.done(); - }, - 'test GroupMetrics.all(), adds a single MetricsCollection with no Metrics specified'(test: Test) { + }); + + test('test GroupMetrics.all(), adds a single MetricsCollection with no Metrics specified', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -1103,18 +1103,18 @@ nodeunitShim({ }); // Then - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MetricsCollection: [ { Granularity: '1Minute', Metrics: ABSENT, }, ], - })); - test.done(); - }, + }); + + }); - 'test can specify a subset of group metrics'(test: Test) { + test('test can specify a subset of group metrics', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -1138,7 +1138,7 @@ nodeunitShim({ }); // Then - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MetricsCollection: [ { Granularity: '1Minute', @@ -1148,11 +1148,11 @@ nodeunitShim({ Metrics: ['GroupPendingInstances', 'GroupStandbyInstances', 'GroupTotalInstances', 'GroupTerminatingInstances'], }, ], - })); - test.done(); - }, + }); + + }); - 'test deduplication of group metrics '(test: Test) { + test('test deduplication of group metrics ', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -1168,18 +1168,18 @@ nodeunitShim({ }); // Then - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MetricsCollection: [ { Granularity: '1Minute', Metrics: ['GroupMinSize', 'GroupMaxSize'], }, ], - })); - test.done(); - }, + }); + + }); - 'allow configuring notifications'(test: Test) { + test('allow configuring notifications', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -1203,7 +1203,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { NotificationConfigurations: [ { TopicARN: { Ref: 'MyTopic86869434' }, @@ -1220,19 +1220,19 @@ nodeunitShim({ }, ], }, - )); + ); - test.done(); - }, - 'throw if notification and notificationsTopics are both configured'(test: Test) { + }); + + test('throw if notification and notificationsTopics are both configured', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); const topic = new sns.Topic(stack, 'MyTopic'); // THEN - test.throws(() => { + expect(() => { new autoscaling.AutoScalingGroup(stack, 'MyASG', { instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), machineImage: new ec2.AmazonLinuxImage(), @@ -1242,11 +1242,11 @@ nodeunitShim({ topic, }], }); - }, 'Cannot set \'notificationsTopic\' and \'notifications\', \'notificationsTopic\' is deprecated use \'notifications\' instead'); - test.done(); - }, + }).toThrow('Cannot set \'notificationsTopic\' and \'notifications\', \'notificationsTopic\' is deprecated use \'notifications\' instead'); + + }); - 'notificationTypes default includes all non test NotificationType'(test: Test) { + test('notificationTypes default includes all non test NotificationType', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -1265,7 +1265,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { NotificationConfigurations: [ { TopicARN: { Ref: 'MyTopic86869434' }, @@ -1278,12 +1278,12 @@ nodeunitShim({ }, ], }, - )); + ); - test.done(); - }, - 'setting notificationTopic configures all non test NotificationType'(test: Test) { + }); + + test('setting notificationTopic configures all non test NotificationType', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -1298,7 +1298,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { NotificationConfigurations: [ { TopicARN: { Ref: 'MyTopic86869434' }, @@ -1311,17 +1311,17 @@ nodeunitShim({ }, ], }, - )); + ); + + + }); - test.done(); - }, + test('NotificationTypes.ALL includes all non test NotificationType', () => { + expect(Object.values(autoscaling.ScalingEvent).length - 1).toEqual(autoscaling.ScalingEvents.ALL._types.length); - 'NotificationTypes.ALL includes all non test NotificationType'(test: Test) { - test.deepEqual(Object.values(autoscaling.ScalingEvent).length - 1, autoscaling.ScalingEvents.ALL._types.length); - test.done(); - }, + }); - 'Can protect new instances from scale-in via constructor property'(test: Test) { + test('Can protect new instances from scale-in via constructor property', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -1335,15 +1335,15 @@ nodeunitShim({ }); // THEN - test.strictEqual(asg.areNewInstancesProtectedFromScaleIn(), true); - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + expect(asg.areNewInstancesProtectedFromScaleIn()).toEqual(true); + expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { NewInstancesProtectedFromScaleIn: true, - })); + }); - test.done(); - }, - 'Can protect new instances from scale-in via setter'(test: Test) { + }); + + test('Can protect new instances from scale-in via setter', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -1357,13 +1357,13 @@ nodeunitShim({ asg.protectNewInstancesFromScaleIn(); // THEN - test.strictEqual(asg.areNewInstancesProtectedFromScaleIn(), true); - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + expect(asg.areNewInstancesProtectedFromScaleIn()).toEqual(true); + expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { NewInstancesProtectedFromScaleIn: true, - })); + }); - test.done(); - }, + + }); }); function mockVpc(stack: cdk.Stack) { @@ -1390,9 +1390,9 @@ test('Can set autoScalingGroupName', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { AutoScalingGroupName: 'MyAsg', - })); + }); }); test('can use Vpc imported from unparseable list tokens', () => { @@ -1427,11 +1427,11 @@ test('can use Vpc imported from unparseable list tokens', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { VPCZoneIdentifier: { 'Fn::Split': [',', { 'Fn::ImportValue': 'myPrivateSubnetIds' }], }, - })); + }); }); function mockSecurityGroup(stack: cdk.Stack) { diff --git a/packages/@aws-cdk/aws-autoscaling/test/cfn-init.test.ts b/packages/@aws-cdk/aws-autoscaling/test/cfn-init.test.ts index 58d9bc6ec152c..2fd252e5e459d 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/cfn-init.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/cfn-init.test.ts @@ -1,3 +1,4 @@ +import '@aws-cdk/assert-internal/jest'; import { anything, arrayWith, expect, haveResourceLike, ResourcePart } from '@aws-cdk/assert-internal'; import * as ec2 from '@aws-cdk/aws-ec2'; import { Duration, Stack } from '@aws-cdk/core'; diff --git a/packages/@aws-cdk/aws-autoscaling/test/lifecyclehooks.test.ts b/packages/@aws-cdk/aws-autoscaling/test/lifecyclehooks.test.ts index 24bebc1a605d0..7ebf6d18cbe6e 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/lifecyclehooks.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/lifecyclehooks.test.ts @@ -1,13 +1,13 @@ +import '@aws-cdk/assert-internal/jest'; import { expect, haveResource, ResourcePart } from '@aws-cdk/assert-internal'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as constructs from 'constructs'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as autoscaling from '../lib'; -nodeunitShim({ - 'we can add a lifecycle hook to an ASG'(test: Test) { +describe('lifecycle hooks', () => { + test('we can add a lifecycle hook to an ASG', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -67,8 +67,8 @@ nodeunitShim({ }, })); - test.done(); - }, + + }); }); class FakeNotificationTarget implements autoscaling.ILifecycleHookTarget { diff --git a/packages/@aws-cdk/aws-autoscaling/test/scaling.test.ts b/packages/@aws-cdk/aws-autoscaling/test/scaling.test.ts index 89b137dd5267f..8216968f406c1 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/scaling.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/scaling.test.ts @@ -1,19 +1,19 @@ +import '@aws-cdk/assert-internal/jest'; import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert-internal'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import * as cdk from '@aws-cdk/core'; import * as constructs from 'constructs'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as autoscaling from '../lib'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main // eslint-disable-next-line no-duplicate-imports, import/order import { Construct } from '@aws-cdk/core'; -nodeunitShim({ - 'target tracking policies': { - 'cpu utilization'(test: Test) { +describe('scaling', () => { + describe('target tracking policies', () => { + test('cpu utilization', () => { // GIVEN const stack = new cdk.Stack(); const fixture = new ASGFixture(stack, 'Fixture'); @@ -32,10 +32,10 @@ nodeunitShim({ }, })); - test.done(); - }, - 'network ingress'(test: Test) { + }); + + test('network ingress', () => { // GIVEN const stack = new cdk.Stack(); const fixture = new ASGFixture(stack, 'Fixture'); @@ -54,10 +54,10 @@ nodeunitShim({ }, })); - test.done(); - }, - 'network egress'(test: Test) { + }); + + test('network egress', () => { // GIVEN const stack = new cdk.Stack(); const fixture = new ASGFixture(stack, 'Fixture'); @@ -76,10 +76,10 @@ nodeunitShim({ }, })); - test.done(); - }, - 'request count per second'(test: Test) { + }); + + test('request count per second', () => { // GIVEN const stack = new cdk.Stack(); const fixture = new ASGFixture(stack, 'Fixture'); @@ -124,10 +124,10 @@ nodeunitShim({ }, })); - test.done(); - }, - 'request count per minute'(test: Test) { + }); + + test('request count per minute', () => { // GIVEN const stack = new cdk.Stack(); const fixture = new ASGFixture(stack, 'Fixture'); @@ -172,10 +172,10 @@ nodeunitShim({ }, })); - test.done(); - }, - 'custom metric'(test: Test) { + }); + + test('custom metric', () => { // GIVEN const stack = new cdk.Stack(); const fixture = new ASGFixture(stack, 'Fixture'); @@ -206,11 +206,11 @@ nodeunitShim({ }, })); - test.done(); - }, - }, - 'step scaling'(test: Test) { + }); + }); + + test('step scaling', () => { // GIVEN const stack = new cdk.Stack(); const fixture = new ASGFixture(stack, 'Fixture'); @@ -273,8 +273,8 @@ nodeunitShim({ AlarmDescription: 'Lower threshold scaling alarm', })); - test.done(); - }, + + }); }); test('step scaling from percentile metric', () => { diff --git a/packages/@aws-cdk/aws-autoscaling/test/scheduled-action.test.ts b/packages/@aws-cdk/aws-autoscaling/test/scheduled-action.test.ts index e5044af33543e..3afbd887b45ae 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/scheduled-action.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/scheduled-action.test.ts @@ -1,12 +1,12 @@ +import '@aws-cdk/assert-internal/jest'; import { expect, haveResource, MatchStyle } from '@aws-cdk/assert-internal'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import * as constructs from 'constructs'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as autoscaling from '../lib'; -nodeunitShim({ - 'can schedule an action'(test: Test) { +describe('scheduled action', () => { + test('can schedule an action', () => { // GIVEN const stack = new cdk.Stack(); const asg = makeAutoScalingGroup(stack); @@ -23,10 +23,10 @@ nodeunitShim({ MinSize: 10, })); - test.done(); - }, - 'correctly formats date objects'(test: Test) { + }); + + test('correctly formats date objects', () => { // GIVEN const stack = new cdk.Stack(); const asg = makeAutoScalingGroup(stack); @@ -43,10 +43,10 @@ nodeunitShim({ StartTime: '2033-09-10T12:00:00Z', })); - test.done(); - }, - 'autoscaling group has recommended updatepolicy for scheduled actions'(test: Test) { + }); + + test('autoscaling group has recommended updatepolicy for scheduled actions', () => { // GIVEN const stack = new cdk.Stack(); const asg = makeAutoScalingGroup(stack); @@ -104,8 +104,8 @@ nodeunitShim({ }, }, MatchStyle.SUPERSET); - test.done(); - }, + + }); }); function makeAutoScalingGroup(scope: constructs.Construct) { diff --git a/packages/@aws-cdk/aws-codepipeline-actions/package.json b/packages/@aws-cdk/aws-codepipeline-actions/package.json index fafc221e72351..e2409aa86db6a 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/package.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/package.json @@ -75,8 +75,8 @@ "@types/lodash": "^4.14.171", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", + "jest": "^26.6.3", "lodash": "^4.17.21", - "nodeunit-shim": "0.0.0", "pkglint": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/bitbucket-source-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/bitbucket-source-action.test.ts index 74df86e395548..a4767fa1a61bc 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/bitbucket-source-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/bitbucket-source-action.test.ts @@ -1,22 +1,22 @@ -import { arrayWith, expect, haveResourceLike, objectLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { arrayWith, objectLike } from '@aws-cdk/assert-internal'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import { Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -nodeunitShim({ - 'BitBucket source Action': { - 'produces the correct configuration when added to a pipeline'(test: Test) { +describe('BitBucket source Action', () => { + describe('BitBucket source Action', () => { + test('produces the correct configuration when added to a pipeline', () => { const stack = new Stack(); createBitBucketAndCodeBuildPipeline(stack, { codeBuildCloneOutput: false, }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -44,20 +44,20 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, - }, + }); + }); - 'setting codeBuildCloneOutput=true adds permission to use the connection to the following CodeBuild Project'(test: Test) { + test('setting codeBuildCloneOutput=true adds permission to use the connection to the following CodeBuild Project', () => { const stack = new Stack(); createBitBucketAndCodeBuildPipeline(stack, { codeBuildCloneOutput: true, }); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -78,16 +78,16 @@ nodeunitShim({ }, ], }, - })); + }); + - test.done(); - }, - 'grant s3 putObjectACL to the following CodeBuild Project'(test: Test) { + }); + test('grant s3 putObjectACL to the following CodeBuild Project', () => { const stack = new Stack(); createBitBucketAndCodeBuildPipeline(stack, { codeBuildCloneOutput: true, }); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': arrayWith( objectLike({ @@ -110,17 +110,17 @@ nodeunitShim({ }), ), }, - })); - test.done(); - }, - 'setting triggerOnPush=false reflects in the configuration'(test: Test) { + }); + + }); + test('setting triggerOnPush=false reflects in the configuration', () => { const stack = new Stack(); createBitBucketAndCodeBuildPipeline(stack, { triggerOnPush: false, }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -149,10 +149,10 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); }); function createBitBucketAndCodeBuildPipeline(stack: Stack, props: Partial): void { diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/cloudformation-pipeline-actions.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/cloudformation-pipeline-actions.test.ts index e478ad7a97d21..955e54107789a 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/cloudformation-pipeline-actions.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/cloudformation-pipeline-actions.test.ts @@ -1,16 +1,15 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codecommit from '@aws-cdk/aws-codecommit'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import { PolicyStatement, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -nodeunitShim({ - 'CreateChangeSetAction can be used to make a change set from a CodePipeline'(test: Test) { +describe('CloudFormation Pipeline Actions', () => { + test('CreateChangeSetAction can be used to make a change set from a CodePipeline', () => { const stack = new cdk.Stack(); const pipeline = new codepipeline.Pipeline(stack, 'MagicPipeline'); @@ -79,7 +78,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'ArtifactStore': { 'Location': { 'Ref': 'MagicPipelineArtifactsBucket212FE7BF', @@ -193,13 +192,12 @@ nodeunitShim({ ], 'Name': 'prod', }], - })); + }); - test.done(); - }, + }); - 'fullPermissions leads to admin role and full IAM capabilities with pipeline bucket+key read permissions'(test: Test) { + test('fullPermissions leads to admin role and full IAM capabilities with pipeline bucket+key read permissions', () => { // GIVEN const stack = new TestFixture(); @@ -214,7 +212,7 @@ nodeunitShim({ const roleId = 'PipelineDeployCreateUpdateRole515CB7D4'; // THEN: Action in Pipeline has named IAM capabilities - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -234,10 +232,10 @@ nodeunitShim({ ], }, ], - })); + }); // THEN: Role is created with full permissions - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -264,12 +262,12 @@ nodeunitShim({ ], }, Roles: [{ Ref: roleId }], - })); + }); + - test.done(); - }, + }); - 'outputFileName leads to creation of output artifact'(test: Test) { + test('outputFileName leads to creation of output artifact', () => { // GIVEN const stack = new TestFixture(); @@ -283,7 +281,7 @@ nodeunitShim({ })); // THEN: Action has output artifacts - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -296,12 +294,12 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); - 'replaceOnFailure switches action type'(test: Test) { + test('replaceOnFailure switches action type', () => { // GIVEN const stack = new TestFixture(); @@ -315,7 +313,7 @@ nodeunitShim({ })); // THEN: Action has output artifacts - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -330,12 +328,12 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); - 'parameterOverrides are serialized as a string'(test: Test) { + test('parameterOverrides are serialized as a string', () => { // GIVEN const stack = new TestFixture(); @@ -351,7 +349,7 @@ nodeunitShim({ })); // THEN - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -372,12 +370,12 @@ nodeunitShim({ ], }, ], - })); + }); - test.done(); - }, - 'Action service role is passed to template'(test: Test) { + }); + + test('Action service role is passed to template', () => { const stack = new TestFixture(); const importedRole = Role.fromRoleArn(stack, 'ImportedRole', 'arn:aws:iam::000000000000:role/action-role'); @@ -399,7 +397,7 @@ nodeunitShim({ stackName: 'magicStack', })); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', /* don't care about the rest */ @@ -423,12 +421,12 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); - 'Single capability is passed to template'(test: Test) { + test('Single capability is passed to template', () => { // GIVEN const stack = new TestFixture(); @@ -446,7 +444,7 @@ nodeunitShim({ const roleId = 'PipelineDeployCreateUpdateRole515CB7D4'; // THEN: Action in Pipeline has named IAM capabilities - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -466,12 +464,12 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); - 'Multiple capabilities are passed to template'(test: Test) { + test('Multiple capabilities are passed to template', () => { // GIVEN const stack = new TestFixture(); @@ -490,7 +488,7 @@ nodeunitShim({ const roleId = 'PipelineDeployCreateUpdateRole515CB7D4'; // THEN: Action in Pipeline has named IAM and AUTOEXPAND capabilities - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -510,12 +508,12 @@ nodeunitShim({ ], }, ], - })); + }); - test.done(); - }, - 'Empty capabilities is not passed to template'(test: Test) { + }); + + test('Empty capabilities is not passed to template', () => { // GIVEN const stack = new TestFixture(); @@ -533,7 +531,7 @@ nodeunitShim({ const roleId = 'PipelineDeployCreateUpdateRole515CB7D4'; // THEN: Action in Pipeline has no capabilities - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -552,12 +550,12 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); - 'can use CfnCapabilities from the core module'(test: Test) { + test('can use CfnCapabilities from the core module', () => { // GIVEN const stack = new TestFixture(); @@ -574,7 +572,7 @@ nodeunitShim({ })); // THEN: Action in Pipeline has named IAM and AUTOEXPAND capabilities - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -594,13 +592,13 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); - 'cross-account CFN Pipeline': { - 'correctly creates the deployment Role in the other account'(test: Test) { + describe('cross-account CFN Pipeline', () => { + test('correctly creates the deployment Role in the other account', () => { const app = new cdk.App(); const pipelineStack = new cdk.Stack(app, 'PipelineStack', { @@ -638,7 +636,7 @@ nodeunitShim({ ], }); - expect(pipelineStack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -662,10 +660,10 @@ nodeunitShim({ ], }, ], - })); + }); // the pipeline's BucketPolicy should trust both CFN roles - expect(pipelineStack).to(haveResourceLike('AWS::S3::BucketPolicy', { + expect(pipelineStack).toHaveResourceLike('AWS::S3::BucketPolicy', { 'PolicyDocument': { 'Statement': [ { @@ -698,19 +696,19 @@ nodeunitShim({ }, ], }, - })); + }); const otherStack = app.node.findChild('cross-account-support-stack-123456789012') as cdk.Stack; - expect(otherStack).to(haveResourceLike('AWS::IAM::Role', { + expect(otherStack).toHaveResourceLike('AWS::IAM::Role', { 'RoleName': 'pipelinestack-support-123loycfnactionrole56af64af3590f311bc50', - })); - expect(otherStack).to(haveResourceLike('AWS::IAM::Role', { + }); + expect(otherStack).toHaveResourceLike('AWS::IAM::Role', { 'RoleName': 'pipelinestack-support-123fndeploymentrole4668d9b5a30ce3dc4508', - })); + }); - test.done(); - }, - }, + + }); + }); }); /** diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/pipeline-actions.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/pipeline-actions.test.ts index 2e94273f6924c..3d9594a9ddfbd 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/pipeline-actions.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/pipeline-actions.test.ts @@ -1,3 +1,4 @@ +import '@aws-cdk/assert-internal/jest'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as notifications from '@aws-cdk/aws-codestarnotifications'; import * as events from '@aws-cdk/aws-events'; @@ -6,12 +7,11 @@ import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; import * as constructs from 'constructs'; import * as _ from 'lodash'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; -nodeunitShim({ - CreateReplaceChangeSet: { - 'works'(test: Test) { +describe('Pipeline Actions', () => { + describe('CreateReplaceChangeSet', () => { + test('works', (done) => { const app = new cdk.App(); const stack = new cdk.Stack(app, 'Stack'); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); @@ -30,29 +30,29 @@ nodeunitShim({ app.synth(); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'iam:PassRole', action.deploymentRole.roleArn); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'iam:PassRole', action.deploymentRole.roleArn); const stackArn = _stackArn('MyStack', stack); const changeSetCondition = { StringEqualsIfExists: { 'cloudformation:ChangeSetName': 'MyChangeSet' } }; - _assertPermissionGranted(test, stack, pipelineRole.statements, 'cloudformation:DescribeStacks', stackArn, changeSetCondition); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'cloudformation:DescribeChangeSet', stackArn, changeSetCondition); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'cloudformation:CreateChangeSet', stackArn, changeSetCondition); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'cloudformation:DeleteChangeSet', stackArn, changeSetCondition); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'cloudformation:DescribeStacks', stackArn, changeSetCondition); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'cloudformation:DescribeChangeSet', stackArn, changeSetCondition); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'cloudformation:CreateChangeSet', stackArn, changeSetCondition); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'cloudformation:DeleteChangeSet', stackArn, changeSetCondition); // TODO: revert "as any" once we move all actions into a single package. - test.deepEqual(stage.fullActions[0].actionProperties.inputs, [artifact], - 'The input was correctly registered'); + expect(stage.fullActions[0].actionProperties.inputs).toEqual([artifact]); - _assertActionMatches(test, stack, stage.fullActions, 'CloudFormation', 'Deploy', { + _assertActionMatches(done, stack, stage.fullActions, 'CloudFormation', 'Deploy', { ActionMode: 'CHANGE_SET_CREATE_REPLACE', StackName: 'MyStack', ChangeSetName: 'MyChangeSet', }); - test.done(); - }, + done(); - 'uses a single permission statement if the same ChangeSet name is used'(test: Test) { + }); + + test('uses a single permission statement if the same ChangeSet name is used', () => { const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); const artifact = new codepipeline.Artifact('TestArtifact'); @@ -76,8 +76,9 @@ nodeunitShim({ ], }); - test.deepEqual( + expect( stack.resolve(pipelineRole.statements.map(s => s.toStatementJson())), + ).toEqual( [ { Action: 'iam:PassRole', @@ -106,12 +107,12 @@ nodeunitShim({ ], ); - test.done(); - }, - }, - ExecuteChangeSet: { - 'works'(test: Test) { + }); + }); + + describe('ExecuteChangeSet', () => { + test('works', (done) => { const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); const stage = new StageDouble({ @@ -126,19 +127,19 @@ nodeunitShim({ }); const stackArn = _stackArn('MyStack', stack); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'cloudformation:ExecuteChangeSet', stackArn, + _assertPermissionGranted(done, stack, pipelineRole.statements, 'cloudformation:ExecuteChangeSet', stackArn, { StringEqualsIfExists: { 'cloudformation:ChangeSetName': 'MyChangeSet' } }); - _assertActionMatches(test, stack, stage.fullActions, 'CloudFormation', 'Deploy', { + _assertActionMatches(done, stack, stage.fullActions, 'CloudFormation', 'Deploy', { ActionMode: 'CHANGE_SET_EXECUTE', StackName: 'MyStack', ChangeSetName: 'MyChangeSet', }); - test.done(); - }, + done(); + }); - 'uses a single permission statement if the same ChangeSet name is used'(test: Test) { + test('uses a single permission statement if the same ChangeSet name is used', () => { const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); new StageDouble({ @@ -157,8 +158,9 @@ nodeunitShim({ ], }); - test.deepEqual( + expect( stack.resolve(pipelineRole.statements.map(s => s.toStatementJson())), + ).toEqual( [ { Action: [ @@ -178,11 +180,11 @@ nodeunitShim({ ], ); - test.done(); - }, - }, - 'the CreateUpdateStack Action sets the DescribeStack*, Create/Update/DeleteStack & PassRole permissions'(test: Test) { + }); + }); + + test('the CreateUpdateStack Action sets the DescribeStack*, Create/Update/DeleteStack & PassRole permissions', (done) => { const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); const action = new cpactions.CloudFormationCreateUpdateStackAction({ @@ -198,17 +200,17 @@ nodeunitShim({ }); const stackArn = _stackArn('MyStack', stack); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'cloudformation:DescribeStack*', stackArn); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'cloudformation:CreateStack', stackArn); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'cloudformation:UpdateStack', stackArn); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'cloudformation:DeleteStack', stackArn); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'cloudformation:DescribeStack*', stackArn); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'cloudformation:CreateStack', stackArn); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'cloudformation:UpdateStack', stackArn); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'cloudformation:DeleteStack', stackArn); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'iam:PassRole', action.deploymentRole.roleArn); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'iam:PassRole', action.deploymentRole.roleArn); - test.done(); - }, + done(); + }); - 'the DeleteStack Action sets the DescribeStack*, DeleteStack & PassRole permissions'(test: Test) { + test('the DeleteStack Action sets the DescribeStack*, DeleteStack & PassRole permissions', (done) => { const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); const action = new cpactions.CloudFormationDeleteStackAction({ @@ -222,13 +224,13 @@ nodeunitShim({ }); const stackArn = _stackArn('MyStack', stack); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'cloudformation:DescribeStack*', stackArn); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'cloudformation:DeleteStack', stackArn); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'cloudformation:DescribeStack*', stackArn); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'cloudformation:DeleteStack', stackArn); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'iam:PassRole', action.deploymentRole.roleArn); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'iam:PassRole', action.deploymentRole.roleArn); - test.done(); - }, + done(); + }); }); interface PolicyStatementJson { @@ -239,7 +241,7 @@ interface PolicyStatementJson { } function _assertActionMatches( - test: Test, + done: jest.DoneCallback, stack: cdk.Stack, actions: FullAction[], provider: string, @@ -256,8 +258,9 @@ function _assertActionMatches( configuration: stack.resolve(a.actionConfig.configuration), }), ), null, 2); - test.ok(_hasAction(stack, actions, provider, category, configuration), - `Expected to find an action with provider ${provider}, category ${category}${configurationStr}, but found ${actionsStr}`); + if (!_hasAction(stack, actions, provider, category, configuration)) { + done.fail(`Expected to find an action with provider ${provider}, category ${category}${configurationStr}, but found ${actionsStr}`); + } } function _hasAction( @@ -280,7 +283,7 @@ function _hasAction( } function _assertPermissionGranted( - test: Test, + done: jest.DoneCallback, stack: cdk.Stack, statements: iam.PolicyStatement[], action: string, @@ -291,8 +294,9 @@ function _assertPermissionGranted( : ''; const resolvedStatements = stack.resolve(statements.map(s => s.toStatementJson())); const statementsStr = JSON.stringify(resolvedStatements, null, 2); - test.ok(_grantsPermission(stack, resolvedStatements, action, resource, conditions), - `Expected to find a statement granting ${action} on ${JSON.stringify(stack.resolve(resource))}${conditionStr}, found:\n${statementsStr}`); + if (!_grantsPermission(stack, resolvedStatements, action, resource, conditions)) { + done.fail(`Expected to find a statement granting ${action} on ${JSON.stringify(stack.resolve(resource))}${conditionStr}, found:\n${statementsStr}`); + } } function _grantsPermission(stack: cdk.Stack, statements: PolicyStatementJson[], action: string, resource: string, conditions?: any) { diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/codebuild-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/codebuild-action.test.ts index 09b583c011513..c8ddc211a813d 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/codebuild-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/codebuild-action.test.ts @@ -1,19 +1,18 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codecommit from '@aws-cdk/aws-codecommit'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as s3 from '@aws-cdk/aws-s3'; import * as sns from '@aws-cdk/aws-sns'; import { App, SecretValue, Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -nodeunitShim({ - 'CodeBuild action': { - 'that is cross-account and has outputs': { - 'causes an error'(test: Test) { +describe('CodeBuild Action', () => { + describe('CodeBuild action', () => { + describe('that is cross-account and has outputs', () => { + test('causes an error', () => { const app = new App(); const projectStack = new Stack(app, 'ProjectStack', { @@ -61,15 +60,15 @@ nodeunitShim({ outputs: [new codepipeline.Artifact()], }); - test.throws(() => { + expect(() => { buildStage.addAction(buildAction2); - }, /https:\/\/github\.com\/aws\/aws-cdk\/issues\/4169/); + }).toThrow(/https:\/\/github\.com\/aws\/aws-cdk\/issues\/4169/); - test.done(); - }, - }, - 'can be backed by an imported project'(test: Test) { + }); + }); + + test('can be backed by an imported project', () => { const stack = new Stack(); const codeBuildProject = codebuild.PipelineProject.fromProjectName(stack, 'CodeBuild', @@ -102,7 +101,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -119,12 +118,12 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); - 'exposes variables for other actions to consume'(test: Test) { + test('exposes variables for other actions to consume', () => { const stack = new Stack(); const sourceOutput = new codepipeline.Artifact(); @@ -177,7 +176,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -198,12 +197,12 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); - 'sets the BatchEnabled configuration'(test: Test) { + test('sets the BatchEnabled configuration', () => { const stack = new Stack(); const codeBuildProject = new codebuild.PipelineProject(stack, 'CodeBuild'); @@ -236,7 +235,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -253,12 +252,12 @@ nodeunitShim({ ], }, ], - })); + }); - test.done(); - }, - 'sets the CombineArtifacts configuration'(test: Test) { + }); + + test('sets the CombineArtifacts configuration', () => { const stack = new Stack(); const codeBuildProject = new codebuild.PipelineProject(stack, 'CodeBuild'); @@ -292,7 +291,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -310,13 +309,13 @@ nodeunitShim({ ], }, ], - })); + }); - test.done(); - }, - 'environment variables': { - 'should fail by default when added to a Pipeline while using a secret value in a plaintext variable'(test: Test) { + }); + + describe('environment variables', () => { + test('should fail by default when added to a Pipeline while using a secret value in a plaintext variable', () => { const stack = new Stack(); const sourceOutput = new codepipeline.Artifact(); @@ -350,14 +349,14 @@ nodeunitShim({ }, }); - test.throws(() => { + expect(() => { buildStage.addAction(buildAction); - }, /Plaintext environment variable 'X' contains a secret value!/); + }).toThrow(/Plaintext environment variable 'X' contains a secret value!/); - test.done(); - }, - "should allow opting out of the 'secret value in a plaintext variable' validation"(test: Test) { + }); + + test("should allow opting out of the 'secret value in a plaintext variable' validation", () => { const stack = new Stack(); const sourceOutput = new codepipeline.Artifact(); @@ -390,8 +389,8 @@ nodeunitShim({ ], }); - test.done(); - }, - }, - }, + + }); + }); + }); }); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/codecommit-source-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/codecommit-source-action.test.ts index da226dd37a74d..648c113ce2155 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/codecommit-source-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/codecommit-source-action.test.ts @@ -1,4 +1,5 @@ -import { ABSENT, arrayWith, countResources, expect, haveResourceLike, not, objectLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { ABSENT, arrayWith, objectLike } from '@aws-cdk/assert-internal'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codecommit from '@aws-cdk/aws-codecommit'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; @@ -6,19 +7,18 @@ import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as s3 from '@aws-cdk/aws-s3'; import { Stack, Lazy, App } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -nodeunitShim({ - 'CodeCommit Source Action': { - 'by default does not poll for source changes and uses Events'(test: Test) { +describe('CodeCommit Source Action', () => { + describe('CodeCommit Source Action', () => { + test('by default does not poll for source changes and uses Events', () => { const stack = new Stack(); minimalPipeline(stack, undefined); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -31,14 +31,14 @@ nodeunitShim({ }, {}, ], - })); + }); + + expect(stack).toCountResources('AWS::Events::Rule', 1); - expect(stack).to(countResources('AWS::Events::Rule', 1)); - test.done(); - }, + }); - 'cross-account CodeCommit Repository Source does not use target role in source stack'(test: Test) { + test('cross-account CodeCommit Repository Source does not use target role in source stack', () => { // Test for https://github.com/aws/aws-cdk/issues/15639 const app = new App(); const sourceStack = new Stack(app, 'SourceStack', { env: { account: '1234', region: 'north-pole' } }); @@ -67,7 +67,7 @@ nodeunitShim({ }); // THEN - creates a Rule in the source stack targeting the pipeline stack's event bus using a generated role - expect(sourceStack).to(haveResourceLike('AWS::Events::Rule', { + expect(sourceStack).toHaveResourceLike('AWS::Events::Rule', { EventPattern: { source: ['aws.codecommit'], resources: [ @@ -84,10 +84,10 @@ nodeunitShim({ ]], }, }], - })); + }); // THEN - creates a Rule in the pipeline stack using the role to start the pipeline - expect(targetStack).to(haveResourceLike('AWS::Events::Rule', { + expect(targetStack).toHaveResourceLike('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'aws.codecommit', @@ -118,17 +118,17 @@ nodeunitShim({ 'RoleArn': { 'Fn::GetAtt': ['MyPipelineEventsRoleFAB99F32', 'Arn'] }, }, ], - })); + }); + - test.done(); - }, + }); - 'does not poll for source changes and uses Events for CodeCommitTrigger.EVENTS'(test: Test) { + test('does not poll for source changes and uses Events for CodeCommitTrigger.EVENTS', () => { const stack = new Stack(); minimalPipeline(stack, cpactions.CodeCommitTrigger.EVENTS); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -141,19 +141,19 @@ nodeunitShim({ }, {}, ], - })); + }); - expect(stack).to(countResources('AWS::Events::Rule', 1)); + expect(stack).toCountResources('AWS::Events::Rule', 1); - test.done(); - }, - 'polls for source changes and does not use Events for CodeCommitTrigger.POLL'(test: Test) { + }); + + test('polls for source changes and does not use Events for CodeCommitTrigger.POLL', () => { const stack = new Stack(); minimalPipeline(stack, cpactions.CodeCommitTrigger.POLL); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -166,19 +166,19 @@ nodeunitShim({ }, {}, ], - })); + }); - expect(stack).to(not(haveResourceLike('AWS::Events::Rule'))); + expect(stack).not.toHaveResourceLike('AWS::Events::Rule'); - test.done(); - }, - 'does not poll for source changes and does not use Events for CodeCommitTrigger.NONE'(test: Test) { + }); + + test('does not poll for source changes and does not use Events for CodeCommitTrigger.NONE', () => { const stack = new Stack(); minimalPipeline(stack, cpactions.CodeCommitTrigger.NONE); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -191,32 +191,32 @@ nodeunitShim({ }, {}, ], - })); + }); + + expect(stack).not.toHaveResourceLike('AWS::Events::Rule'); - expect(stack).to(not(haveResourceLike('AWS::Events::Rule'))); - test.done(); - }, + }); - 'cannot be created with an empty branch'(test: Test) { + test('cannot be created with an empty branch', () => { const stack = new Stack(); const repo = new codecommit.Repository(stack, 'MyRepo', { repositoryName: 'my-repo', }); - test.throws(() => { + expect(() => { new cpactions.CodeCommitSourceAction({ actionName: 'Source2', repository: repo, output: new codepipeline.Artifact(), branch: '', }); - }, /'branch' parameter cannot be an empty string/); + }).toThrow(/'branch' parameter cannot be an empty string/); + - test.done(); - }, + }); - 'allows using the same repository multiple times with different branches when trigger=EVENTS'(test: Test) { + test('allows using the same repository multiple times with different branches when trigger=EVENTS', () => { const stack = new Stack(); const repo = new codecommit.Repository(stack, 'MyRepo', { @@ -255,10 +255,10 @@ nodeunitShim({ ], }); - test.done(); - }, - 'exposes variables for other actions to consume'(test: Test) { + }); + + test('exposes variables for other actions to consume', () => { const stack = new Stack(); const sourceOutput = new codepipeline.Artifact(); @@ -291,7 +291,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -308,12 +308,12 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); - 'allows using a Token for the branch name'(test: Test) { + test('allows using a Token for the branch name', () => { const stack = new Stack(); const sourceOutput = new codepipeline.Artifact(); @@ -345,18 +345,18 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { EventPattern: { detail: { referenceName: ['my-branch'], }, }, - })); + }); + - test.done(); - }, + }); - 'allows to enable full clone'(test: Test) { + test('allows to enable full clone', () => { const stack = new Stack(); const sourceOutput = new codepipeline.Artifact(); @@ -389,7 +389,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -408,9 +408,9 @@ nodeunitShim({ ], }, ], - })); + }); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': arrayWith( objectLike({ @@ -432,9 +432,9 @@ nodeunitShim({ }), ), }, - })); + }); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': arrayWith( objectLike({ @@ -456,12 +456,12 @@ nodeunitShim({ }), ), }, - })); + }); + - test.done(); - }, + }); - 'uses the role when passed'(test: Test) { + test('uses the role when passed', () => { const stack = new Stack(); const pipeline = new codepipeline.Pipeline(stack, 'P', { @@ -504,7 +504,7 @@ nodeunitShim({ actions: [buildAction], }); - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { Arn: stack.resolve(pipeline.pipelineArn), @@ -512,12 +512,12 @@ nodeunitShim({ RoleArn: stack.resolve(triggerEventTestRole.roleArn), }, ], - })); + }); + - test.done(); - }, + }); - 'grants explicit s3:PutObjectAcl permissions when the Actions is cross-account'(test: Test) { + test('grants explicit s3:PutObjectAcl permissions when the Actions is cross-account', () => { const app = new App(); const repoStack = new Stack(app, 'RepoStack', { @@ -552,7 +552,7 @@ nodeunitShim({ ], }); - expect(repoStack).to(haveResourceLike('AWS::IAM::Policy', { + expect(repoStack).toHaveResourceLike('AWS::IAM::Policy', { PolicyDocument: { Statement: arrayWith({ 'Action': 's3:PutObjectAcl', @@ -566,11 +566,11 @@ nodeunitShim({ }, }), }, - })); + }); - test.done(); - }, - }, + + }); + }); }); function minimalPipeline(stack: Stack, trigger: cpactions.CodeCommitTrigger | undefined): codepipeline.Pipeline { diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/codedeploy/ecs-deploy-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/codedeploy/ecs-deploy-action.test.ts index 30a1d4827b091..18a0c714f4d5b 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/codedeploy/ecs-deploy-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/codedeploy/ecs-deploy-action.test.ts @@ -1,13 +1,12 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as codedeploy from '@aws-cdk/aws-codedeploy'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; -nodeunitShim({ - 'CodeDeploy ECS Deploy Action': { - 'throws an exception if more than 4 container image inputs are provided'(test: Test) { +describe('CodeDeploy ECS Deploy Action', () => { + describe('CodeDeploy ECS Deploy Action', () => { + test('throws an exception if more than 4 container image inputs are provided', () => { const stack = new cdk.Stack(); const deploymentGroup = addEcsDeploymentGroup(stack); const artifact = new codepipeline.Artifact('Artifact'); @@ -19,7 +18,7 @@ nodeunitShim({ }); } - test.throws(() => { + expect(() => { new cpactions.CodeDeployEcsDeployAction({ actionName: 'DeployToECS', deploymentGroup, @@ -27,18 +26,18 @@ nodeunitShim({ appSpecTemplateInput: artifact, containerImageInputs, }); - }, /Action cannot have more than 4 container image inputs, got: 5/); + }).toThrow(/Action cannot have more than 4 container image inputs, got: 5/); - test.done(); - }, - 'throws an exception if both appspec artifact input and file are specified'(test: Test) { + }); + + test('throws an exception if both appspec artifact input and file are specified', () => { const stack = new cdk.Stack(); const deploymentGroup = addEcsDeploymentGroup(stack); const artifact = new codepipeline.Artifact('Artifact'); const artifactPath = new codepipeline.ArtifactPath(artifact, 'hello'); - test.throws(() => { + expect(() => { new cpactions.CodeDeployEcsDeployAction({ actionName: 'DeployToECS', deploymentGroup, @@ -46,34 +45,34 @@ nodeunitShim({ appSpecTemplateInput: artifact, appSpecTemplateFile: artifactPath, }); - }, /Exactly one of 'appSpecTemplateInput' or 'appSpecTemplateFile' can be provided in the ECS CodeDeploy Action/); + }).toThrow(/Exactly one of 'appSpecTemplateInput' or 'appSpecTemplateFile' can be provided in the ECS CodeDeploy Action/); - test.done(); - }, - 'throws an exception if neither appspec artifact input nor file are specified'(test: Test) { + }); + + test('throws an exception if neither appspec artifact input nor file are specified', () => { const stack = new cdk.Stack(); const deploymentGroup = addEcsDeploymentGroup(stack); const artifact = new codepipeline.Artifact('Artifact'); - test.throws(() => { + expect(() => { new cpactions.CodeDeployEcsDeployAction({ actionName: 'DeployToECS', deploymentGroup, taskDefinitionTemplateInput: artifact, }); - }, /Specifying one of 'appSpecTemplateInput' or 'appSpecTemplateFile' is required for the ECS CodeDeploy Action/); + }).toThrow(/Specifying one of 'appSpecTemplateInput' or 'appSpecTemplateFile' is required for the ECS CodeDeploy Action/); - test.done(); - }, - 'throws an exception if both task definition artifact input and file are specified'(test: Test) { + }); + + test('throws an exception if both task definition artifact input and file are specified', () => { const stack = new cdk.Stack(); const deploymentGroup = addEcsDeploymentGroup(stack); const artifact = new codepipeline.Artifact('Artifact'); const artifactPath = new codepipeline.ArtifactPath(artifact, 'hello'); - test.throws(() => { + expect(() => { new cpactions.CodeDeployEcsDeployAction({ actionName: 'DeployToECS', deploymentGroup, @@ -81,28 +80,28 @@ nodeunitShim({ taskDefinitionTemplateFile: artifactPath, appSpecTemplateInput: artifact, }); - }, /Exactly one of 'taskDefinitionTemplateInput' or 'taskDefinitionTemplateFile' can be provided in the ECS CodeDeploy Action/); + }).toThrow(/Exactly one of 'taskDefinitionTemplateInput' or 'taskDefinitionTemplateFile' can be provided in the ECS CodeDeploy Action/); + - test.done(); - }, + }); - 'throws an exception if neither task definition artifact input nor file are specified'(test: Test) { + test('throws an exception if neither task definition artifact input nor file are specified', () => { const stack = new cdk.Stack(); const deploymentGroup = addEcsDeploymentGroup(stack); const artifact = new codepipeline.Artifact('Artifact'); - test.throws(() => { + expect(() => { new cpactions.CodeDeployEcsDeployAction({ actionName: 'DeployToECS', deploymentGroup, appSpecTemplateInput: artifact, }); - }, /Specifying one of 'taskDefinitionTemplateInput' or 'taskDefinitionTemplateFile' is required for the ECS CodeDeploy Action/); + }).toThrow(/Specifying one of 'taskDefinitionTemplateInput' or 'taskDefinitionTemplateFile' is required for the ECS CodeDeploy Action/); - test.done(); - }, - 'defaults task definition and appspec template paths'(test: Test) { + }); + + test('defaults task definition and appspec template paths', () => { const stack = new cdk.Stack(); const deploymentGroup = addEcsDeploymentGroup(stack); addCodeDeployECSCodePipeline(stack, { @@ -112,7 +111,7 @@ nodeunitShim({ appSpecTemplateInput: new codepipeline.Artifact('AppSpecArtifact'), }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: [ {}, { @@ -138,12 +137,12 @@ nodeunitShim({ ], }, ], - })); + }); - test.done(); - }, - 'defaults task definition placeholder string'(test: Test) { + }); + + test('defaults task definition placeholder string', () => { const stack = new cdk.Stack(); const deploymentGroup = addEcsDeploymentGroup(stack); const artifact1 = new codepipeline.Artifact(); @@ -163,7 +162,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: [ {}, { @@ -193,11 +192,11 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, - }, + }); + }); }); function addEcsDeploymentGroup(stack: cdk.Stack): codedeploy.IEcsDeploymentGroup { diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/codestar-connections/codestar-connections-source-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/codestar-connections/codestar-connections-source-action.test.ts index 367da1e11ed3f..75fe764109b25 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/codestar-connections/codestar-connections-source-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/codestar-connections/codestar-connections-source-action.test.ts @@ -1,22 +1,22 @@ -import { arrayWith, expect, haveResourceLike, objectLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { arrayWith, objectLike } from '@aws-cdk/assert-internal'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import { Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -nodeunitShim({ - 'CodeStar Connections source Action': { - 'produces the correct configuration when added to a pipeline'(test: Test) { +describe('CodeStar Connections source Action', () => { + describe('CodeStar Connections source Action', () => { + test('produces the correct configuration when added to a pipeline', () => { const stack = new Stack(); createBitBucketAndCodeBuildPipeline(stack, { codeBuildCloneOutput: false, }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -44,20 +44,20 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, - }, + }); + }); - 'setting codeBuildCloneOutput=true adds permission to use the connection to the following CodeBuild Project'(test: Test) { + test('setting codeBuildCloneOutput=true adds permission to use the connection to the following CodeBuild Project', () => { const stack = new Stack(); createBitBucketAndCodeBuildPipeline(stack, { codeBuildCloneOutput: true, }); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -78,19 +78,19 @@ nodeunitShim({ }, ], }, - })); + }); + - test.done(); - }, + }); - 'grant s3 putObjectACL to the following CodeBuild Project'(test: Test) { + test('grant s3 putObjectACL to the following CodeBuild Project', () => { const stack = new Stack(); createBitBucketAndCodeBuildPipeline(stack, { codeBuildCloneOutput: true, }); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': arrayWith( objectLike({ @@ -105,19 +105,19 @@ nodeunitShim({ }), ), }, - })); + }); + - test.done(); - }, + }); - 'setting triggerOnPush=false reflects in the configuration'(test: Test) { + test('setting triggerOnPush=false reflects in the configuration', () => { const stack = new Stack(); createBitBucketAndCodeBuildPipeline(stack, { triggerOnPush: false, }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -146,10 +146,10 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); }); function createBitBucketAndCodeBuildPipeline(stack: Stack, props: Partial): void { diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/ecr/ecr-source-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/ecr/ecr-source-action.test.ts index aecf6cb915f04..b51a566d4ef14 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/ecr/ecr-source-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/ecr/ecr-source-action.test.ts @@ -1,16 +1,15 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as ecr from '@aws-cdk/aws-ecr'; import { Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -nodeunitShim({ - 'ECR source Action': { - 'exposes variables for other actions to consume'(test: Test) { +describe('ecr source action', () => { + describe('ECR source Action', () => { + test('exposes variables for other actions to consume', () => { const stack = new Stack(); const sourceOutput = new codepipeline.Artifact(); @@ -41,7 +40,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -58,9 +57,9 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, - }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts index 3cdc12554d501..63927d5832ec8 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts @@ -1,124 +1,123 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; -nodeunitShim({ - 'ECS deploy Action': { - 'throws an exception if neither inputArtifact nor imageFile were provided'(test: Test) { +describe('ecs deploy action', () => { + describe('ECS deploy Action', () => { + test('throws an exception if neither inputArtifact nor imageFile were provided', () => { const service = anyEcsService(); - test.throws(() => { + expect(() => { new cpactions.EcsDeployAction({ actionName: 'ECS', service, }); - }, /one of 'input' or 'imageFile' is required/); + }).toThrow(/one of 'input' or 'imageFile' is required/); - test.done(); - }, - 'can be created just by specifying the inputArtifact'(test: Test) { + }); + + test('can be created just by specifying the inputArtifact', () => { const service = anyEcsService(); const artifact = new codepipeline.Artifact('Artifact'); - test.doesNotThrow(() => { + expect(() => { new cpactions.EcsDeployAction({ actionName: 'ECS', service, input: artifact, }); - }); + }).not.toThrow(); - test.done(); - }, - 'can be created just by specifying the imageFile'(test: Test) { + }); + + test('can be created just by specifying the imageFile', () => { const service = anyEcsService(); const artifact = new codepipeline.Artifact('Artifact'); - test.doesNotThrow(() => { + expect(() => { new cpactions.EcsDeployAction({ actionName: 'ECS', service, imageFile: artifact.atPath('imageFile.json'), }); - }); + }).not.toThrow(); - test.done(); - }, - 'throws an exception if both inputArtifact and imageFile were provided'(test: Test) { + }); + + test('throws an exception if both inputArtifact and imageFile were provided', () => { const service = anyEcsService(); const artifact = new codepipeline.Artifact('Artifact'); - test.throws(() => { + expect(() => { new cpactions.EcsDeployAction({ actionName: 'ECS', service, input: artifact, imageFile: artifact.atPath('file.json'), }); - }, /one of 'input' or 'imageFile' can be provided/); + }).toThrow(/one of 'input' or 'imageFile' can be provided/); + - test.done(); - }, + }); - 'can be created with deploymentTimeout between 1-60 minutes'(test: Test) { + test('can be created with deploymentTimeout between 1-60 minutes', () => { const service = anyEcsService(); const artifact = new codepipeline.Artifact('Artifact'); - test.doesNotThrow(() => { + expect(() => { new cpactions.EcsDeployAction({ actionName: 'ECS', service, input: artifact, deploymentTimeout: cdk.Duration.minutes(30), }); - }); + }).not.toThrow(); - test.done(); - }, - 'throws an exception if deploymentTimeout is out of bounds'(test: Test) { + }); + + test('throws an exception if deploymentTimeout is out of bounds', () => { const service = anyEcsService(); const artifact = new codepipeline.Artifact('Artifact'); - test.throws(() => { + expect(() => { new cpactions.EcsDeployAction({ actionName: 'ECS', service, input: artifact, deploymentTimeout: cdk.Duration.minutes(61), }); - }, /timeout must be between 1 and 60 minutes/); + }).toThrow(/timeout must be between 1 and 60 minutes/); - test.throws(() => { + expect(() => { new cpactions.EcsDeployAction({ actionName: 'ECS', service, input: artifact, deploymentTimeout: cdk.Duration.minutes(0), }); - }, /timeout must be between 1 and 60 minutes/); + }).toThrow(/timeout must be between 1 and 60 minutes/); - test.throws(() => { + expect(() => { new cpactions.EcsDeployAction({ actionName: 'ECS', service, input: artifact, deploymentTimeout: cdk.Duration.seconds(30), }); - }, /cannot be converted into a whole number/); + }).toThrow(/cannot be converted into a whole number/); + - test.done(); - }, + }); - "sets the target service as the action's backing resource"(test: Test) { + test("sets the target service as the action's backing resource", () => { const service = anyEcsService(); const artifact = new codepipeline.Artifact('Artifact'); @@ -128,12 +127,12 @@ nodeunitShim({ input: artifact, }); - test.equal(action.actionProperties.resource, service); + expect(action.actionProperties.resource).toEqual(service); + - test.done(); - }, + }); - 'can be created by existing service'(test: Test) { + test('can be created by existing service', () => { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Vpc'); const service = ecs.FargateService.fromFargateServiceAttributes(stack, 'FargateService', { @@ -173,7 +172,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: [ {}, { @@ -193,11 +192,11 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, - }, + }); + }); }); function anyEcsService(): ecs.FargateService { diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/github/github-source-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/github/github-source-action.test.ts index 635f7ac67780f..6a1342888042e 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/github/github-source-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/github/github-source-action.test.ts @@ -1,15 +1,15 @@ -import { expect, haveResourceLike, SynthUtils } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { SynthUtils } from '@aws-cdk/assert-internal'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import { SecretValue, Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -nodeunitShim({ - 'GitHub source Action': { - 'exposes variables for other actions to consume'(test: Test) { +describe('Github source action', () => { + describe('GitHub source Action', () => { + test('exposes variables for other actions to consume', () => { const stack = new Stack(); const sourceOutput = new codepipeline.Artifact(); @@ -42,7 +42,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -59,12 +59,12 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); - 'always renders the customer-supplied namespace, even if none of the variables are used'(test: Test) { + test('always renders the customer-supplied namespace, even if none of the variables are used', () => { const stack = new Stack(); const sourceOutput = new codepipeline.Artifact(); @@ -96,7 +96,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -110,12 +110,12 @@ nodeunitShim({ { }, ], - })); + }); - test.done(); - }, - 'fails if a variable from an action without a namespace set that is not part of a pipeline is referenced'(test: Test) { + }); + + test('fails if a variable from an action without a namespace set that is not part of a pipeline is referenced', () => { const stack = new Stack(); const unusedSourceAction = new cpactions.GitHubSourceAction({ @@ -154,14 +154,14 @@ nodeunitShim({ ], }); - test.throws(() => { + expect(() => { SynthUtils.synthesize(stack); - }, /Cannot reference variables of action 'Source2', as that action was never added to a pipeline/); + }).toThrow(/Cannot reference variables of action 'Source2', as that action was never added to a pipeline/); + - test.done(); - }, + }); - 'fails if a variable from an action with a namespace set that is not part of a pipeline is referenced'(test: Test) { + test('fails if a variable from an action with a namespace set that is not part of a pipeline is referenced', () => { const stack = new Stack(); const unusedSourceAction = new cpactions.GitHubSourceAction({ @@ -201,11 +201,11 @@ nodeunitShim({ ], }); - test.throws(() => { + expect(() => { SynthUtils.synthesize(stack); - }, /Cannot reference variables of action 'Source2', as that action was never added to a pipeline/); + }).toThrow(/Cannot reference variables of action 'Source2', as that action was never added to a pipeline/); + - test.done(); - }, - }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/manual-approval.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/manual-approval.test.ts index 93b4dc1134160..bf894a5db1c88 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/manual-approval.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/manual-approval.test.ts @@ -1,16 +1,15 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as iam from '@aws-cdk/aws-iam'; import * as sns from '@aws-cdk/aws-sns'; import { SecretValue, Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../lib'; /* eslint-disable quote-props */ -nodeunitShim({ - 'manual approval Action': { - 'allows passing an SNS Topic when constructing it'(test: Test) { +describe('manual approval', () => { + describe('manual approval Action', () => { + test('allows passing an SNS Topic when constructing it', () => { const stack = new Stack(); const topic = new sns.Topic(stack, 'Topic'); const manualApprovalAction = new cpactions.ManualApprovalAction({ @@ -21,12 +20,12 @@ nodeunitShim({ const stage = pipeline.addStage({ stageName: 'stage' }); stage.addAction(manualApprovalAction); - test.equal(manualApprovalAction.notificationTopic, topic); + expect(manualApprovalAction.notificationTopic).toEqual(topic); - test.done(); - }, - 'allows granting manual approval permissions to role'(test: Test) { + }); + + test('allows granting manual approval permissions to role', () => { const stack = new Stack(); const role = new iam.Role(stack, 'Human', { assumedBy: new iam.AnyPrincipal() }); const pipeline = new codepipeline.Pipeline(stack, 'pipeline'); @@ -49,7 +48,7 @@ nodeunitShim({ manualApprovalAction.grantManualApproval(role); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -109,26 +108,26 @@ nodeunitShim({ 'Ref': 'HumanD337C84C', }, ], - })); + }); + - test.done(); - }, + }); - 'rejects granting manual approval permissions before binding action to stage'(test: Test) { + test('rejects granting manual approval permissions before binding action to stage', () => { const stack = new Stack(); const role = new iam.Role(stack, 'Human', { assumedBy: new iam.AnyPrincipal() }); const manualApprovalAction = new cpactions.ManualApprovalAction({ actionName: 'Approve', }); - test.throws(() => { + expect(() => { manualApprovalAction.grantManualApproval(role); - }, 'Cannot grant permissions before binding action to a stage'); + }).toThrow('Cannot grant permissions before binding action to a stage'); - test.done(); - }, - 'renders CustomData and ExternalEntityLink even if notificationTopic was not passed'(test: Test) { + }); + + test('renders CustomData and ExternalEntityLink even if notificationTopic was not passed', () => { const stack = new Stack(); new codepipeline.Pipeline(stack, 'pipeline', { stages: [ @@ -155,7 +154,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -173,9 +172,9 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, - }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline.test.ts index 331bae0db4b9e..1c680e0c7e95d 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline.test.ts @@ -1,4 +1,5 @@ -import { countResources, expect, haveResource, haveResourceLike, not, SynthUtils } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { SynthUtils } from '@aws-cdk/assert-internal'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codecommit from '@aws-cdk/aws-codecommit'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; @@ -8,13 +9,12 @@ import * as lambda from '@aws-cdk/aws-lambda'; import * as s3 from '@aws-cdk/aws-s3'; import * as sns from '@aws-cdk/aws-sns'; import { App, Aws, CfnParameter, ConstructNode, SecretValue, Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../lib'; /* eslint-disable quote-props */ -nodeunitShim({ - 'basic pipeline'(test: Test) { +describe('pipeline', () => { + test('basic pipeline', () => { const stack = new Stack(); const repository = new codecommit.Repository(stack, 'MyRepo', { @@ -45,28 +45,50 @@ nodeunitShim({ ], }); - test.notDeepEqual(SynthUtils.toCloudFormation(stack), {}); - test.deepEqual([], ConstructNode.validate(pipeline.node)); - test.done(); - }, + expect(SynthUtils.toCloudFormation(stack)).not.toEqual({}); + expect([]).toEqual(ConstructNode.validate(pipeline.node)); - 'Tokens can be used as physical names of the Pipeline'(test: Test) { + }); + + test('Tokens can be used as physical names of the Pipeline', () => { const stack = new Stack(undefined, 'StackName'); - new codepipeline.Pipeline(stack, 'Pipeline', { + const p = new codepipeline.Pipeline(stack, 'Pipeline', { pipelineName: Aws.STACK_NAME, }); + p.addStage({ + stageName: 'Source', + actions: [ + new cpactions.GitHubSourceAction({ + actionName: 'GH', + runOrder: 8, + output: new codepipeline.Artifact('A'), + branch: 'branch', + oauthToken: SecretValue.plainText('secret'), + owner: 'foo', + repo: 'bar', + trigger: cpactions.GitHubTrigger.POLL, + }), + ], + }); - expect(stack, true).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + p.addStage({ + stageName: 'Two', + actions: [ + new cpactions.ManualApprovalAction({ actionName: 'Boo' }), + ], + }); + + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Name': { 'Ref': 'AWS::StackName', }, - })); + }); + - test.done(); - }, + }); - 'pipeline with GitHub source with poll trigger'(test: Test) { + test('pipeline with GitHub source with poll trigger', () => { const stack = new Stack(); const secret = new CfnParameter(stack, 'GitHubToken', { type: 'String', default: 'my-token' }); @@ -96,9 +118,9 @@ nodeunitShim({ ], }); - expect(stack).to(not(haveResourceLike('AWS::CodePipeline::Webhook'))); + expect(stack).not.toHaveResourceLike('AWS::CodePipeline::Webhook'); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -120,12 +142,12 @@ nodeunitShim({ 'Name': 'Two', }, ], - })); + }); + - test.done(); - }, + }); - 'pipeline with GitHub source without triggers'(test: Test) { + test('pipeline with GitHub source without triggers', () => { const stack = new Stack(); const secret = new CfnParameter(stack, 'GitHubToken', { type: 'String', default: 'my-token' }); @@ -155,9 +177,9 @@ nodeunitShim({ ], }); - expect(stack).to(not(haveResourceLike('AWS::CodePipeline::Webhook'))); + expect(stack).not.toHaveResourceLike('AWS::CodePipeline::Webhook'); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -179,12 +201,12 @@ nodeunitShim({ 'Name': 'Two', }, ], - })); + }); + - test.done(); - }, + }); - 'github action uses ThirdParty owner'(test: Test) { + test('github action uses ThirdParty owner', () => { const stack = new Stack(); const secret = new CfnParameter(stack, 'GitHubToken', { type: 'String', default: 'my-token' }); @@ -213,9 +235,9 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Webhook')); + expect(stack).toHaveResourceLike('AWS::CodePipeline::Webhook'); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'ArtifactStore': { 'Location': { 'Ref': 'PArtifactsBucket5E711C12', @@ -274,13 +296,13 @@ nodeunitShim({ 'Name': 'Two', }, ], - })); + }); + + expect([]).toEqual(ConstructNode.validate(p.node)); - test.deepEqual([], ConstructNode.validate(p.node)); - test.done(); - }, + }); - 'onStateChange'(test: Test) { + test('onStateChange', () => { const stack = new Stack(); const topic = new sns.Topic(stack, 'Topic'); @@ -316,7 +338,7 @@ nodeunitShim({ }, }); - expect(stack).to(haveResource('AWS::Events::Rule', { + expect(stack).toHaveResource('AWS::Events::Rule', { 'Description': 'desc', 'EventPattern': { 'detail': { @@ -365,22 +387,22 @@ nodeunitShim({ 'Id': 'Target0', }, ], - })); + }); - test.deepEqual([], ConstructNode.validate(pipeline.node)); - test.done(); - }, + expect([]).toEqual(ConstructNode.validate(pipeline.node)); - 'PipelineProject': { - 'with a custom Project Name': { - 'sets the source and artifacts to CodePipeline'(test: Test) { + }); + + describe('PipelineProject', () => { + describe('with a custom Project Name', () => { + test('sets the source and artifacts to CodePipeline', () => { const stack = new Stack(); new codebuild.PipelineProject(stack, 'MyProject', { projectName: 'MyProject', }); - expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', { + expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { 'Name': 'MyProject', 'Source': { 'Type': 'CODEPIPELINE', @@ -400,14 +422,14 @@ nodeunitShim({ 'Image': 'aws/codebuild/standard:1.0', 'ComputeType': 'BUILD_GENERAL1_SMALL', }, - })); + }); - test.done(); - }, - }, - }, - 'Lambda PipelineInvokeAction can be used to invoke Lambda functions from a CodePipeline'(test: Test) { + }); + }); + }); + + test('Lambda PipelineInvokeAction can be used to invoke Lambda functions from a CodePipeline', () => { const stack = new Stack(); const lambdaFun = new lambda.Function(stack, 'Function', { @@ -459,7 +481,7 @@ nodeunitShim({ actions: [lambdaAction], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'ArtifactStore': { 'Location': { 'Ref': 'PipelineArtifactsBucket22248F97', @@ -506,11 +528,11 @@ nodeunitShim({ 'Name': 'Stage', }, ], - })); + }); - test.equal((lambdaAction.actionProperties.outputs || []).length, 3); + expect((lambdaAction.actionProperties.outputs || []).length).toEqual(3); - expect(stack, /* skip validation */ true).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -530,13 +552,13 @@ nodeunitShim({ 'Ref': 'FunctionServiceRole675BB04A', }, ], - })); + }); - test.done(); - }, - 'cross-region Pipeline': { - 'generates the required Action & ArtifactStores properties in the template'(test: Test) { + }); + + describe('cross-region Pipeline', () => { + test('generates the required Action & ArtifactStores properties in the template', () => { const pipelineRegion = 'us-west-2'; const pipelineAccount = '123'; @@ -594,7 +616,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'ArtifactStores': [ { 'Region': 'us-west-1', @@ -644,36 +666,35 @@ nodeunitShim({ ], }, ], - })); + }); - test.notEqual(pipeline.crossRegionSupport[pipelineRegion], undefined); - test.notEqual(pipeline.crossRegionSupport['us-west-1'], undefined); + expect(pipeline.crossRegionSupport[pipelineRegion]).toBeDefined(); + expect(pipeline.crossRegionSupport['us-west-1']).toBeDefined(); const usEast1Support = pipeline.crossRegionSupport['us-east-1']; - test.notEqual(usEast1Support, undefined); - test.equal(usEast1Support.stack.region, 'us-east-1'); - test.equal(usEast1Support.stack.account, pipelineAccount); - test.ok(usEast1Support.stack.node.id.indexOf('us-east-1') !== -1, - `expected '${usEast1Support.stack.node.id}' to contain 'us-east-1'`); + expect(usEast1Support).toBeDefined(); + expect(usEast1Support.stack.region).toEqual('us-east-1'); + expect(usEast1Support.stack.account).toEqual(pipelineAccount); + expect(usEast1Support.stack.node.id.indexOf('us-east-1')).not.toEqual(-1); - test.done(); - }, - 'allows specifying only one of artifactBucket and crossRegionReplicationBuckets'(test: Test) { + }); + + test('allows specifying only one of artifactBucket and crossRegionReplicationBuckets', () => { const stack = new Stack(); - test.throws(() => { + expect(() => { new codepipeline.Pipeline(stack, 'Pipeline', { artifactBucket: new s3.Bucket(stack, 'Bucket'), crossRegionReplicationBuckets: { // even an empty map should trigger this validation... }, }); - }, /Only one of artifactBucket and crossRegionReplicationBuckets can be specified!/); - test.done(); - }, + }).toThrow(/Only one of artifactBucket and crossRegionReplicationBuckets can be specified!/); - 'does not create a new artifact Bucket if one was provided in the cross-region Buckets for the Pipeline region'(test: Test) { + }); + + test('does not create a new artifact Bucket if one was provided in the cross-region Buckets for the Pipeline region', () => { const pipelineRegion = 'us-west-2'; const stack = new Stack(undefined, undefined, { @@ -712,9 +733,9 @@ nodeunitShim({ ], }); - expect(stack).to(countResources('AWS::S3::Bucket', 1)); + expect(stack).toCountResources('AWS::S3::Bucket', 1); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'ArtifactStores': [ { 'Region': pipelineRegion, @@ -726,12 +747,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'allows providing a resource-backed action from a different region directly'(test: Test) { + }); + + test('allows providing a resource-backed action from a different region directly', () => { const account = '123456789012'; const app = new App(); @@ -765,7 +786,7 @@ nodeunitShim({ ], }); - expect(pipelineStack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'ArtifactStores': [ { 'Region': replicationRegion, @@ -810,18 +831,18 @@ nodeunitShim({ ], }, ], - })); + }); - expect(replicationStack).to(haveResourceLike('AWS::S3::Bucket', { + expect(replicationStack).toHaveResourceLike('AWS::S3::Bucket', { 'BucketName': 'replicationstackeplicationbucket2464cd5c33b386483b66', - })); + }); + - test.done(); - }, - }, + }); + }); - 'cross-account Pipeline': { - 'with a CodeBuild Project in a different account works correctly'(test: Test) { + describe('cross-account Pipeline', () => { + test('with a CodeBuild Project in a different account works correctly', () => { const app = new App(); const buildAccount = '901234567890'; @@ -874,7 +895,7 @@ nodeunitShim({ ], }); - expect(pipelineStack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -903,9 +924,9 @@ nodeunitShim({ ], }, ], - })); + }); - expect(buildStack).to(haveResourceLike('AWS::IAM::Policy', { + expect(buildStack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -958,12 +979,12 @@ nodeunitShim({ }, ], }, - })); + }); - test.done(); - }, - 'adds a dependency on the Stack containing a new action Role'(test: Test) { + }); + + test('adds a dependency on the Stack containing a new action Role', () => { const region = 'us-west-2'; const pipelineAccount = '123456789012'; const buildAccount = '901234567890'; @@ -1017,7 +1038,7 @@ nodeunitShim({ ], }); - expect(pipelineStack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -1041,14 +1062,14 @@ nodeunitShim({ ], }, ], - })); + }); - test.equal(pipelineStack.dependencies.length, 1); + expect(pipelineStack.dependencies.length).toEqual(1); - test.done(); - }, - 'does not add a dependency on the Stack containing an imported action Role'(test: Test) { + }); + + test('does not add a dependency on the Stack containing an imported action Role', () => { const region = 'us-west-2'; const pipelineAccount = '123456789012'; const buildAccount = '901234567890'; @@ -1101,7 +1122,7 @@ nodeunitShim({ ], }); - expect(pipelineStack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -1119,11 +1140,11 @@ nodeunitShim({ ], }, ], - })); + }); + + expect(pipelineStack.dependencies.length).toEqual(0); - test.equal(pipelineStack.dependencies.length, 0); - test.done(); - }, - }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/s3/s3-source-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/s3/s3-source-action.test.ts index 0fb227a9246e8..65553aee81be5 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/s3/s3-source-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/s3/s3-source-action.test.ts @@ -1,21 +1,20 @@ -import { countResources, expect, haveResourceLike, not } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as s3 from '@aws-cdk/aws-s3'; import { Lazy, Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -nodeunitShim({ - 'S3 Source Action': { - 'by default polls for source changes and does not use Events'(test: Test) { +describe('S3 source Action', () => { + describe('S3 Source Action', () => { + test('by default polls for source changes and does not use Events', () => { const stack = new Stack(); minimalPipeline(stack, undefined); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -27,19 +26,19 @@ nodeunitShim({ }, {}, ], - })); + }); + + expect(stack).not.toHaveResourceLike('AWS::Events::Rule'); - expect(stack).to(not(haveResourceLike('AWS::Events::Rule'))); - test.done(); - }, + }); - 'does not poll for source changes and uses Events for S3Trigger.EVENTS'(test: Test) { + test('does not poll for source changes and uses Events for S3Trigger.EVENTS', () => { const stack = new Stack(); minimalPipeline(stack, { trigger: cpactions.S3Trigger.EVENTS }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -52,19 +51,19 @@ nodeunitShim({ }, {}, ], - })); + }); + + expect(stack).toCountResources('AWS::Events::Rule', 1); - expect(stack).to(countResources('AWS::Events::Rule', 1)); - test.done(); - }, + }); - 'polls for source changes and does not use Events for S3Trigger.POLL'(test: Test) { + test('polls for source changes and does not use Events for S3Trigger.POLL', () => { const stack = new Stack(); minimalPipeline(stack, { trigger: cpactions.S3Trigger.POLL }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -77,19 +76,19 @@ nodeunitShim({ }, {}, ], - })); + }); + + expect(stack).not.toHaveResourceLike('AWS::Events::Rule'); - expect(stack).to(not(haveResourceLike('AWS::Events::Rule'))); - test.done(); - }, + }); - 'does not poll for source changes and does not use Events for S3Trigger.NONE'(test: Test) { + test('does not poll for source changes and does not use Events for S3Trigger.NONE', () => { const stack = new Stack(); minimalPipeline(stack, { trigger: cpactions.S3Trigger.NONE }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -102,29 +101,29 @@ nodeunitShim({ }, {}, ], - })); + }); - expect(stack).to(not(haveResourceLike('AWS::Events::Rule'))); + expect(stack).not.toHaveResourceLike('AWS::Events::Rule'); - test.done(); - }, - 'does not allow passing an empty string for the bucketKey property'(test: Test) { + }); + + test('does not allow passing an empty string for the bucketKey property', () => { const stack = new Stack(); - test.throws(() => { + expect(() => { new cpactions.S3SourceAction({ actionName: 'Source', bucket: new s3.Bucket(stack, 'MyBucket'), bucketKey: '', output: new codepipeline.Artifact(), }); - }, /Property bucketKey cannot be an empty string/); + }).toThrow(/Property bucketKey cannot be an empty string/); + - test.done(); - }, + }); - 'allows using the same bucket with events trigger mutliple times with different bucket paths'(test: Test) { + test('allows using the same bucket with events trigger mutliple times with different bucket paths', () => { const stack = new Stack(); const bucket = new s3.Bucket(stack, 'MyBucket'); @@ -141,10 +140,10 @@ nodeunitShim({ output: new codepipeline.Artifact(), })); - test.done(); - }, - 'throws an error if the same bucket and path with trigger = Events are added to the same pipeline twice'(test: Test) { + }); + + test('throws an error if the same bucket and path with trigger = Events are added to the same pipeline twice', () => { const stack = new Stack(); const bucket = new s3.Bucket(stack, 'MyBucket'); @@ -169,14 +168,14 @@ nodeunitShim({ output: new codepipeline.Artifact(), }); - test.throws(() => { + expect(() => { sourceStage.addAction(duplicateBucketAndPath); - }, /S3 source action with path 'my\/other\/path' is already present in the pipeline for this source bucket/); + }).toThrow(/S3 source action with path 'my\/other\/path' is already present in the pipeline for this source bucket/); - test.done(); - }, - 'allows using a Token bucketKey with trigger = Events, multiple times'(test: Test) { + }); + + test('allows using a Token bucketKey with trigger = Events, multiple times', () => { const stack = new Stack(); const bucket = new s3.Bucket(stack, 'MyBucket'); @@ -193,7 +192,7 @@ nodeunitShim({ output: new codepipeline.Artifact(), })); - expect(stack, /* skipValidation = */ true).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -210,12 +209,12 @@ nodeunitShim({ ], }, ], - })); + }); - test.done(); - }, - 'exposes variables for other actions to consume'(test: Test) { + }); + + test('exposes variables for other actions to consume', () => { const stack = new Stack(); const sourceOutput = new codepipeline.Artifact(); @@ -247,7 +246,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -264,11 +263,11 @@ nodeunitShim({ ], }, ], - })); + }); - test.done(); - }, - }, + + }); + }); }); interface MinimalPipelineOptions { diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/servicecatalog/servicecatalog-deploy-action-beta1.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/servicecatalog/servicecatalog-deploy-action-beta1.test.ts index ef96441ca585e..cf490f21fc206 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/servicecatalog/servicecatalog-deploy-action-beta1.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/servicecatalog/servicecatalog-deploy-action-beta1.test.ts @@ -1,14 +1,13 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as codecommit from '@aws-cdk/aws-codecommit'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import { Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -nodeunitShim({ - 'addAction succesfully leads to creation of codepipeline service catalog action with properly formatted TemplateFilePath'(test: Test) { +describe('ServiceCatalog Deploy Action', () => { + test('addAction succesfully leads to creation of codepipeline service catalog action with properly formatted TemplateFilePath', () => { // GIVEN const stack = new TestFixture(); // WHEN @@ -20,7 +19,7 @@ nodeunitShim({ productId: 'prod-xxxxxxxxx', })); // THEN - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -50,11 +49,11 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, - 'deployment without a description works successfully'(test: Test) { + }); + test('deployment without a description works successfully', () => { // GIVEN const stack = new TestFixture(); // WHEN @@ -65,7 +64,7 @@ nodeunitShim({ productId: 'prod-xxxxxxxxx', })); // THEN - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -94,10 +93,10 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); }); /** diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/stepfunctions/stepfunctions-invoke-actions.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/stepfunctions/stepfunctions-invoke-actions.test.ts index 50f8788e8823c..0ad5d8b2213a1 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/stepfunctions/stepfunctions-invoke-actions.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/stepfunctions/stepfunctions-invoke-actions.test.ts @@ -1,21 +1,20 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as s3 from '@aws-cdk/aws-s3'; import * as stepfunction from '@aws-cdk/aws-stepfunctions'; import { Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; -nodeunitShim({ - 'StepFunctions Invoke Action': { - 'Verify stepfunction configuration properties are set to specific values'(test: Test) { +describe('StepFunctions Invoke Action', () => { + describe('StepFunctions Invoke Action', () => { + test('Verify stepfunction configuration properties are set to specific values', () => { const stack = new Stack(); // when minimalPipeline(stack); // then - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: [ // Must have a source stage { @@ -58,17 +57,17 @@ nodeunitShim({ ], }, ], - })); + }); - test.done(); - }, - 'Allows the pipeline to invoke this stepfunction'(test: Test) { + }); + + test('Allows the pipeline to invoke this stepfunction', () => { const stack = new Stack(); minimalPipeline(stack); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -80,19 +79,19 @@ nodeunitShim({ }, ], }, - })); + }); + + expect(stack).toHaveResourceLike('AWS::IAM::Role'); - expect(stack).to(haveResourceLike('AWS::IAM::Role')); - test.done(); - }, + }); - 'Allows the pipeline to describe this stepfunction execution'(test: Test) { + test('Allows the pipeline to describe this stepfunction execution', () => { const stack = new Stack(); minimalPipeline(stack); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { PolicyDocument: { Statement: [ {}, @@ -136,14 +135,14 @@ nodeunitShim({ }, ], }, - })); + }); - expect(stack).to(haveResourceLike('AWS::IAM::Role')); + expect(stack).toHaveResourceLike('AWS::IAM::Role'); - test.done(); - }, - }, + }); + + }); }); function minimalPipeline(stack: Stack): codepipeline.IStage { diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index 1348aef9b9d62..f050efe94d045 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -81,7 +81,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "nodeunit-shim": "0.0.0", + "jest": "^26.6.3", "pkglint": "0.0.0", "proxyquire": "^2.1.3", "@aws-cdk/assert-internal": "0.0.0" diff --git a/packages/@aws-cdk/aws-ecs/test/app-mesh-proxy-configuration.test.ts b/packages/@aws-cdk/aws-ecs/test/app-mesh-proxy-configuration.test.ts index 7865809011a32..7c8bd5f880f77 100644 --- a/packages/@aws-cdk/aws-ecs/test/app-mesh-proxy-configuration.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/app-mesh-proxy-configuration.test.ts @@ -1,10 +1,9 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../lib'; -nodeunitShim({ - 'correctly sets all appMeshProxyConfiguration'(test: Test) { +describe('app mesh proxy configuration', () => { + test('correctly sets all appMeshProxyConfiguration', () => { // GIVEN const stack = new cdk.Stack(); @@ -34,7 +33,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ProxyConfiguration: { ContainerName: 'envoy', ProxyConfigurationProperties: [ @@ -69,11 +68,11 @@ nodeunitShim({ ], Type: 'APPMESH', }, - })); - test.done(); - }, + }); + + }); - 'correctly sets appMeshProxyConfiguration with default properties set'(test: Test) { + test('correctly sets appMeshProxyConfiguration with default properties set', () => { // GIVEN const stack = new cdk.Stack(); @@ -100,7 +99,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ProxyConfiguration: { ContainerName: 'envoy', ProxyConfigurationProperties: [ @@ -123,11 +122,11 @@ nodeunitShim({ ], Type: 'APPMESH', }, - })); - test.done(); - }, + }); + + }); - 'correctly sets appMeshProxyConfiguration with empty egressIgnoredPorts and egressIgnoredIPs'(test: Test) { + test('correctly sets appMeshProxyConfiguration with empty egressIgnoredPorts and egressIgnoredIPs', () => { // GIVEN const stack = new cdk.Stack(); @@ -156,7 +155,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ProxyConfiguration: { ContainerName: 'envoy', ProxyConfigurationProperties: [ @@ -179,16 +178,16 @@ nodeunitShim({ ], Type: 'APPMESH', }, - })); - test.done(); - }, + }); + + }); - 'throws when neither of IgnoredUID and IgnoredGID is set'(test: Test) { + test('throws when neither of IgnoredUID and IgnoredGID is set', () => { // GIVEN const stack = new cdk.Stack(); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { networkMode: ecs.NetworkMode.AWS_VPC, proxyConfiguration: ecs.ProxyConfigurations.appMeshProxyConfiguration({ @@ -200,8 +199,8 @@ nodeunitShim({ }, }), }); - }, /At least one of ignoredUID or ignoredGID should be specified./); + }).toThrow(/At least one of ignoredUID or ignoredGID should be specified./); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/aws-log-driver.test.ts b/packages/@aws-cdk/aws-ecs/test/aws-log-driver.test.ts index 8a802ac72014b..5e570a7d39eeb 100644 --- a/packages/@aws-cdk/aws-ecs/test/aws-log-driver.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/aws-log-driver.test.ts @@ -1,22 +1,21 @@ -import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as logs from '@aws-cdk/aws-logs'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../lib'; let stack: cdk.Stack; let td: ecs.TaskDefinition; const image = ecs.ContainerImage.fromRegistry('test-image'); -nodeunitShim({ - 'setUp'(cb: () => void) { +describe('aws log driver', () => { + beforeEach(() => { stack = new cdk.Stack(); td = new ecs.FargateTaskDefinition(stack, 'TaskDefinition'); - cb(); - }, - 'create an aws log driver'(test: Test) { + }); + + test('create an aws log driver', () => { // WHEN td.addContainer('Container', { image, @@ -30,11 +29,11 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Logs::LogGroup', { + expect(stack).toHaveResource('AWS::Logs::LogGroup', { RetentionInDays: logs.RetentionDays.ONE_MONTH, - })); + }); - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -50,12 +49,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'create an aws log driver using awsLogs'(test: Test) { + }); + + test('create an aws log driver using awsLogs', () => { // WHEN td.addContainer('Container', { image, @@ -68,11 +67,11 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Logs::LogGroup', { + expect(stack).toHaveResource('AWS::Logs::LogGroup', { RetentionInDays: logs.RetentionDays.ONE_MONTH, - })); + }); - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -87,12 +86,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'with a defined log group'(test: Test) { + }); + + test('with a defined log group', () => { // GIVEN const logGroup = new logs.LogGroup(stack, 'LogGroup'); @@ -106,11 +105,11 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Logs::LogGroup', { + expect(stack).toHaveResource('AWS::Logs::LogGroup', { RetentionInDays: logs.RetentionDays.TWO_YEARS, - })); + }); - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -123,12 +122,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'without a defined log group: creates one anyway'(test: Test) { + }); + + test('without a defined log group: creates one anyway', () => { // GIVEN td.addContainer('Container', { image, @@ -138,22 +137,22 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Logs::LogGroup', {})); + expect(stack).toHaveResource('AWS::Logs::LogGroup', {}); + - test.done(); - }, + }); - 'throws when specifying log retention and log group'(test: Test) { + test('throws when specifying log retention and log group', () => { // GIVEN const logGroup = new logs.LogGroup(stack, 'LogGroup'); // THEN - test.throws(() => new ecs.AwsLogDriver({ + expect(() => new ecs.AwsLogDriver({ logGroup, logRetention: logs.RetentionDays.FIVE_DAYS, streamPrefix: 'hello', - }), /`logGroup`.*`logRetentionDays`/); + })).toThrow(/`logGroup`.*`logRetentionDays`/); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/cluster.test.ts b/packages/@aws-cdk/aws-ecs/test/cluster.test.ts index 40c74a08257f2..cfe65183d1c02 100644 --- a/packages/@aws-cdk/aws-ecs/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/cluster.test.ts @@ -1,11 +1,5 @@ -import { - countResources, - expect, - haveResource, - haveResourceLike, - ResourcePart, - ABSENT, -} from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { ABSENT } from '@aws-cdk/assert-internal'; import * as autoscaling from '@aws-cdk/aws-autoscaling'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as kms from '@aws-cdk/aws-kms'; @@ -13,12 +7,11 @@ import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../lib'; -nodeunitShim({ - 'When creating an ECS Cluster': { - 'with no properties set, it correctly sets default properties'(test: Test) { +describe('cluster', () => { + describe('When creating an ECS Cluster', () => { + test('with no properties set, it correctly sets default properties', () => { // GIVEN const stack = new cdk.Stack(); const cluster = new ecs.Cluster(stack, 'EcsCluster'); @@ -27,9 +20,9 @@ nodeunitShim({ instanceType: new ec2.InstanceType('t2.micro'), }); - expect(stack).to(haveResource('AWS::ECS::Cluster')); + expect(stack).toHaveResource('AWS::ECS::Cluster'); - expect(stack).to(haveResource('AWS::EC2::VPC', { + expect(stack).toHaveResource('AWS::EC2::VPC', { CidrBlock: '10.0.0.0/16', EnableDnsHostnames: true, EnableDnsSupport: true, @@ -40,9 +33,9 @@ nodeunitShim({ Value: 'Default/EcsCluster/Vpc', }, ], - })); + }); - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, @@ -73,9 +66,9 @@ nodeunitShim({ ], }, }, - })); + }); - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MaxSize: '1', MinSize: '1', LaunchConfigurationName: { @@ -96,9 +89,9 @@ nodeunitShim({ Ref: 'EcsClusterVpcPrivateSubnet2SubnetC2B7B1BA', }, ], - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup', SecurityGroupEgress: [ { @@ -116,9 +109,9 @@ nodeunitShim({ VpcId: { Ref: 'EcsClusterVpc779914AB', }, - })); + }); - expect(stack).to(haveResource('AWS::IAM::Role', { + expect(stack).toHaveResource('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -131,9 +124,9 @@ nodeunitShim({ ], Version: '2012-10-17', }, - })); + }); - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -181,12 +174,12 @@ nodeunitShim({ ], Version: '2012-10-17', }, - })); + }); - test.done(); - }, - 'with only vpc set, it correctly sets default properties'(test: Test) { + }); + + test('with only vpc set, it correctly sets default properties', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -198,9 +191,9 @@ nodeunitShim({ instanceType: new ec2.InstanceType('t2.micro'), }); - expect(stack).to(haveResource('AWS::ECS::Cluster')); + expect(stack).toHaveResource('AWS::ECS::Cluster'); - expect(stack).to(haveResource('AWS::EC2::VPC', { + expect(stack).toHaveResource('AWS::EC2::VPC', { CidrBlock: '10.0.0.0/16', EnableDnsHostnames: true, EnableDnsSupport: true, @@ -211,9 +204,9 @@ nodeunitShim({ Value: 'Default/MyVpc', }, ], - })); + }); - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, @@ -244,9 +237,9 @@ nodeunitShim({ ], }, }, - })); + }); - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MaxSize: '1', MinSize: '1', LaunchConfigurationName: { @@ -267,9 +260,9 @@ nodeunitShim({ Ref: 'MyVpcPrivateSubnet2Subnet0040C983', }, ], - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup', SecurityGroupEgress: [ { @@ -287,9 +280,9 @@ nodeunitShim({ VpcId: { Ref: 'MyVpcF9F0CA6F', }, - })); + }); - expect(stack).to(haveResource('AWS::IAM::Role', { + expect(stack).toHaveResource('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -302,9 +295,9 @@ nodeunitShim({ ], Version: '2012-10-17', }, - })); + }); - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -352,12 +345,12 @@ nodeunitShim({ ], Version: '2012-10-17', }, - })); + }); - test.done(); - }, - 'multiple clusters with default capacity'(test: Test) { + }); + + test('multiple clusters with default capacity', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -370,10 +363,10 @@ nodeunitShim({ }); } - test.done(); - }, - 'lifecycle hook is automatically added'(test: Test) { + }); + + test('lifecycle hook is automatically added', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -387,16 +380,16 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LifecycleHook', { + expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { AutoScalingGroupName: { Ref: 'EcsClusterDefaultAutoScalingGroupASGC1A785DB' }, LifecycleTransition: 'autoscaling:EC2_INSTANCE_TERMINATING', DefaultResult: 'CONTINUE', HeartbeatTimeout: 300, NotificationTargetARN: { Ref: 'EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicACD2D4A4' }, RoleARN: { 'Fn::GetAtt': ['EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleA38EC83B', 'Arn'] }, - })); + }); - expect(stack).to(haveResource('AWS::Lambda::Function', { + expect(stack).toHaveResource('AWS::Lambda::Function', { Timeout: 310, Environment: { Variables: { @@ -406,9 +399,9 @@ nodeunitShim({ }, }, Handler: 'index.lambda_handler', - })); + }); - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -507,12 +500,12 @@ nodeunitShim({ Ref: 'EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole94543EDA', }, ], - })); + }); + - test.done(); - }, + }); - 'lifecycle hook with encrypted SNS is added correctly'(test: Test) { + test('lifecycle hook with encrypted SNS is added correctly', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -528,19 +521,19 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::SNS::Topic', { + expect(stack).toHaveResourceLike('AWS::SNS::Topic', { KmsMasterKeyId: { 'Fn::GetAtt': [ 'Key961B73FD', 'Arn', ], }, - })); + }); - test.done(); - }, - 'with capacity and cloudmap namespace properties set'(test: Test) { + }); + + test('with capacity and cloudmap namespace properties set', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -555,16 +548,16 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ServiceDiscovery::PrivateDnsNamespace', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::PrivateDnsNamespace', { Name: 'foo.com', Vpc: { Ref: 'MyVpcF9F0CA6F', }, - })); + }); - expect(stack).to(haveResource('AWS::ECS::Cluster')); + expect(stack).toHaveResource('AWS::ECS::Cluster'); - expect(stack).to(haveResource('AWS::EC2::VPC', { + expect(stack).toHaveResource('AWS::EC2::VPC', { CidrBlock: '10.0.0.0/16', EnableDnsHostnames: true, EnableDnsSupport: true, @@ -575,9 +568,9 @@ nodeunitShim({ Value: 'Default/MyVpc', }, ], - })); + }); - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, @@ -608,9 +601,9 @@ nodeunitShim({ ], }, }, - })); + }); - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MaxSize: '1', MinSize: '1', LaunchConfigurationName: { @@ -631,9 +624,9 @@ nodeunitShim({ Ref: 'MyVpcPrivateSubnet2Subnet0040C983', }, ], - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup', SecurityGroupEgress: [ { @@ -651,9 +644,9 @@ nodeunitShim({ VpcId: { Ref: 'MyVpcF9F0CA6F', }, - })); + }); - expect(stack).to(haveResource('AWS::IAM::Role', { + expect(stack).toHaveResource('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -666,9 +659,9 @@ nodeunitShim({ ], Version: '2012-10-17', }, - })); + }); - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -716,13 +709,13 @@ nodeunitShim({ ], Version: '2012-10-17', }, - })); + }); + - test.done(); - }, - }, + }); + }); - 'allows specifying instance type'(test: Test) { + test('allows specifying instance type', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -733,14 +726,14 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 'm3.large', - })); + }); - test.done(); - }, - 'allows specifying cluster size'(test: Test) { + }); + + test('allows specifying cluster size', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -752,14 +745,14 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MaxSize: '3', - })); + }); - test.done(); - }, - 'configures userdata with powershell if windows machine image is specified'(test: Test) { + }); + + test('configures userdata with powershell if windows machine image is specified', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -773,7 +766,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsserviceecsoptimizedamiwindowsserver2019englishfullrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, @@ -807,15 +800,15 @@ nodeunitShim({ ], }, }, - })); + }); - test.done(); - }, + + }); /* * TODO:v2.0.0 BEGINNING OF OBSOLETE BLOCK */ - 'allows specifying special HW AMI Type'(test: Test) { + test('allows specifying special HW AMI Type', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -832,23 +825,23 @@ nodeunitShim({ // THEN const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsserviceecsoptimizedamiamazonlinux2gpurecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, - })); + }); - test.deepEqual(template.Parameters, { + expect(template.Parameters).toEqual({ SsmParameterValueawsserviceecsoptimizedamiamazonlinux2gpurecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter: { Type: 'AWS::SSM::Parameter::Value', Default: '/aws/service/ecs/optimized-ami/amazon-linux-2/gpu/recommended/image_id', }, }); - test.done(); - }, - 'errors if amazon linux given with special HW type'(test: Test) { + }); + + test('errors if amazon linux given with special HW type', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -856,7 +849,7 @@ nodeunitShim({ const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); // THEN - test.throws(() => { + expect(() => { cluster.addCapacity('GpuAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro'), machineImage: new ecs.EcsOptimizedAmi({ @@ -864,12 +857,12 @@ nodeunitShim({ hardwareType: ecs.AmiHardwareType.GPU, }), }); - }, /Amazon Linux does not support special hardware type/); + }).toThrow(/Amazon Linux does not support special hardware type/); + - test.done(); - }, + }); - 'allows specifying windows image'(test: Test) { + test('allows specifying windows image', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -886,17 +879,17 @@ nodeunitShim({ // THEN const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - test.deepEqual(template.Parameters, { + expect(template.Parameters).toEqual({ SsmParameterValueawsserviceecsoptimizedamiwindowsserver2019englishfullrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter: { Type: 'AWS::SSM::Parameter::Value', Default: '/aws/service/ecs/optimized-ami/windows_server/2019/english/full/recommended/image_id', }, }); - test.done(); - }, - 'errors if windows given with special HW type'(test: Test) { + }); + + test('errors if windows given with special HW type', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -904,7 +897,7 @@ nodeunitShim({ const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); // THEN - test.throws(() => { + expect(() => { cluster.addCapacity('WindowsGpuAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro'), machineImage: new ecs.EcsOptimizedAmi({ @@ -912,12 +905,12 @@ nodeunitShim({ hardwareType: ecs.AmiHardwareType.GPU, }), }); - }, /Windows Server does not support special hardware type/); + }).toThrow(/Windows Server does not support special hardware type/); + - test.done(); - }, + }); - 'errors if windowsVersion and linux generation are set'(test: Test) { + test('errors if windowsVersion and linux generation are set', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -925,7 +918,7 @@ nodeunitShim({ const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); // THEN - test.throws(() => { + expect(() => { cluster.addCapacity('WindowsScalingGroup', { instanceType: new ec2.InstanceType('t2.micro'), machineImage: new ecs.EcsOptimizedAmi({ @@ -933,93 +926,93 @@ nodeunitShim({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX, }), }); - }, /"windowsVersion" and Linux image "generation" cannot be both set/); + }).toThrow(/"windowsVersion" and Linux image "generation" cannot be both set/); - test.done(); - }, - 'allows returning the correct image for windows for EcsOptimizedAmi'(test: Test) { + }); + + test('allows returning the correct image for windows for EcsOptimizedAmi', () => { // GIVEN const stack = new cdk.Stack(); const ami = new ecs.EcsOptimizedAmi({ windowsVersion: ecs.WindowsOptimizedVersion.SERVER_2019, }); - test.equal(ami.getImage(stack).osType, ec2.OperatingSystemType.WINDOWS); + expect(ami.getImage(stack).osType).toEqual(ec2.OperatingSystemType.WINDOWS); + - test.done(); - }, + }); - 'allows returning the correct image for linux for EcsOptimizedAmi'(test: Test) { + test('allows returning the correct image for linux for EcsOptimizedAmi', () => { // GIVEN const stack = new cdk.Stack(); const ami = new ecs.EcsOptimizedAmi({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX, }); - test.equal(ami.getImage(stack).osType, ec2.OperatingSystemType.LINUX); + expect(ami.getImage(stack).osType).toEqual(ec2.OperatingSystemType.LINUX); - test.done(); - }, - 'allows returning the correct image for linux 2 for EcsOptimizedAmi'(test: Test) { + }); + + test('allows returning the correct image for linux 2 for EcsOptimizedAmi', () => { // GIVEN const stack = new cdk.Stack(); const ami = new ecs.EcsOptimizedAmi({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, }); - test.equal(ami.getImage(stack).osType, ec2.OperatingSystemType.LINUX); + expect(ami.getImage(stack).osType).toEqual(ec2.OperatingSystemType.LINUX); + - test.done(); - }, + }); - 'allows returning the correct image for linux for EcsOptimizedImage'(test: Test) { + test('allows returning the correct image for linux for EcsOptimizedImage', () => { // GIVEN const stack = new cdk.Stack(); - test.equal(ecs.EcsOptimizedImage.amazonLinux().getImage(stack).osType, + expect(ecs.EcsOptimizedImage.amazonLinux().getImage(stack).osType).toEqual( ec2.OperatingSystemType.LINUX); - test.done(); - }, - 'allows returning the correct image for linux 2 for EcsOptimizedImage'(test: Test) { + }); + + test('allows returning the correct image for linux 2 for EcsOptimizedImage', () => { // GIVEN const stack = new cdk.Stack(); - test.equal(ecs.EcsOptimizedImage.amazonLinux2().getImage(stack).osType, + expect(ecs.EcsOptimizedImage.amazonLinux2().getImage(stack).osType).toEqual( ec2.OperatingSystemType.LINUX); - test.done(); - }, - 'allows returning the correct image for linux 2 for EcsOptimizedImage with ARM hardware'(test: Test) { + }); + + test('allows returning the correct image for linux 2 for EcsOptimizedImage with ARM hardware', () => { // GIVEN const stack = new cdk.Stack(); - test.equal(ecs.EcsOptimizedImage.amazonLinux2(ecs.AmiHardwareType.ARM).getImage(stack).osType, + expect(ecs.EcsOptimizedImage.amazonLinux2(ecs.AmiHardwareType.ARM).getImage(stack).osType).toEqual( ec2.OperatingSystemType.LINUX); - test.done(); - }, + }); - 'allows returning the correct image for windows for EcsOptimizedImage'(test: Test) { + + test('allows returning the correct image for windows for EcsOptimizedImage', () => { // GIVEN const stack = new cdk.Stack(); - test.equal(ecs.EcsOptimizedImage.windows(ecs.WindowsOptimizedVersion.SERVER_2019).getImage(stack).osType, + expect(ecs.EcsOptimizedImage.windows(ecs.WindowsOptimizedVersion.SERVER_2019).getImage(stack).osType).toEqual( ec2.OperatingSystemType.WINDOWS); - test.done(); - }, + + }); /* * TODO:v2.0.0 END OF OBSOLETE BLOCK */ - 'allows specifying special HW AMI Type v2'(test: Test) { + test('allows specifying special HW AMI Type v2', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1034,23 +1027,23 @@ nodeunitShim({ // THEN const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsserviceecsoptimizedamiamazonlinux2gpurecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, - })); + }); - test.deepEqual(template.Parameters, { + expect(template.Parameters).toEqual({ SsmParameterValueawsserviceecsoptimizedamiamazonlinux2gpurecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter: { Type: 'AWS::SSM::Parameter::Value', Default: '/aws/service/ecs/optimized-ami/amazon-linux-2/gpu/recommended/image_id', }, }); - test.done(); - }, - 'allows specifying Amazon Linux v1 AMI'(test: Test) { + }); + + test('allows specifying Amazon Linux v1 AMI', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1065,23 +1058,23 @@ nodeunitShim({ // THEN const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsserviceecsoptimizedamiamazonlinuxrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, - })); + }); - test.deepEqual(template.Parameters, { + expect(template.Parameters).toEqual({ SsmParameterValueawsserviceecsoptimizedamiamazonlinuxrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter: { Type: 'AWS::SSM::Parameter::Value', Default: '/aws/service/ecs/optimized-ami/amazon-linux/recommended/image_id', }, }); - test.done(); - }, - 'allows specifying windows image v2'(test: Test) { + }); + + test('allows specifying windows image v2', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1096,17 +1089,17 @@ nodeunitShim({ // THEN const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - test.deepEqual(template.Parameters, { + expect(template.Parameters).toEqual({ SsmParameterValueawsserviceecsoptimizedamiwindowsserver2019englishfullrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter: { Type: 'AWS::SSM::Parameter::Value', Default: '/aws/service/ecs/optimized-ami/windows_server/2019/english/full/recommended/image_id', }, }); - test.done(); - }, - 'allows specifying spot fleet'(test: Test) { + }); + + test('allows specifying spot fleet', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1118,14 +1111,14 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { SpotPrice: '0.31', - })); + }); - test.done(); - }, - 'allows specifying drain time'(test: Test) { + }); + + test('allows specifying drain time', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1137,14 +1130,14 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LifecycleHook', { + expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { HeartbeatTimeout: 60, - })); + }); + - test.done(); - }, + }); - 'allows specifying automated spot draining'(test: Test) { + test('allows specifying automated spot draining', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1157,7 +1150,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { UserData: { 'Fn::Base64': { 'Fn::Join': [ @@ -1172,12 +1165,12 @@ nodeunitShim({ ], }, }, - })); + }); + - test.done(); - }, + }); - 'allows containers access to instance metadata service'(test: Test) { + test('allows containers access to instance metadata service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1189,7 +1182,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { UserData: { 'Fn::Base64': { 'Fn::Join': [ @@ -1204,12 +1197,12 @@ nodeunitShim({ ], }, }, - })); + }); + - test.done(); - }, + }); - 'allows adding default service discovery namespace'(test: Test) { + test('allows adding default service discovery namespace', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1225,17 +1218,17 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ServiceDiscovery::PrivateDnsNamespace', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::PrivateDnsNamespace', { Name: 'foo.com', Vpc: { Ref: 'MyVpcF9F0CA6F', }, - })); + }); + - test.done(); - }, + }); - 'allows adding public service discovery namespace'(test: Test) { + test('allows adding public service discovery namespace', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1252,16 +1245,16 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ServiceDiscovery::PublicDnsNamespace', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::PublicDnsNamespace', { Name: 'foo.com', - })); + }); + + expect(cluster.defaultCloudMapNamespace!.type).toEqual(cloudmap.NamespaceType.DNS_PUBLIC); - test.equal(cluster.defaultCloudMapNamespace!.type, cloudmap.NamespaceType.DNS_PUBLIC); - test.done(); - }, + }); - 'throws if default service discovery namespace added more than once'(test: Test) { + test('throws if default service discovery namespace added more than once', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1277,16 +1270,16 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { cluster.addDefaultCloudMapNamespace({ name: 'foo.com', }); - }, /Can only add default namespace once./); + }).toThrow(/Can only add default namespace once./); - test.done(); - }, - 'export/import of a cluster with a namespace'(test: Test) { + }); + + test('export/import of a cluster with a namespace', () => { // GIVEN const stack1 = new cdk.Stack(); const vpc1 = new ec2.Vpc(stack1, 'Vpc'); @@ -1310,16 +1303,16 @@ nodeunitShim({ }); // THEN - test.equal(cluster2.defaultCloudMapNamespace!.type, cloudmap.NamespaceType.DNS_PRIVATE); - test.deepEqual(stack2.resolve(cluster2.defaultCloudMapNamespace!.namespaceId), 'import-namespace-id'); + expect(cluster2.defaultCloudMapNamespace!.type).toEqual(cloudmap.NamespaceType.DNS_PRIVATE); + expect(stack2.resolve(cluster2.defaultCloudMapNamespace!.namespaceId)).toEqual('import-namespace-id'); // Can retrieve subnets from VPC - will throw 'There are no 'Private' subnets in this VPC. Use a different VPC subnet selection.' if broken. cluster2.vpc.selectSubnets(); - test.done(); - }, - 'imported cluster with imported security groups honors allowAllOutbound'(test: Test) { + }); + + test('imported cluster with imported security groups honors allowAllOutbound', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Vpc'); @@ -1337,16 +1330,16 @@ nodeunitShim({ cluster.connections.allowToAnyIpv4(ec2.Port.tcp(443)); // THEN - expect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { GroupId: 'sg-1', - })); + }); + + expect(stack).toCountResources('AWS::EC2::SecurityGroupEgress', 1); - expect(stack).to(countResources('AWS::EC2::SecurityGroupEgress', 1)); - test.done(); - }, + }); - 'Metric'(test: Test) { + test('Metric', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1354,7 +1347,7 @@ nodeunitShim({ const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); // THEN - test.deepEqual(stack.resolve(cluster.metricCpuReservation()), { + expect(stack.resolve(cluster.metricCpuReservation())).toEqual({ dimensions: { ClusterName: { Ref: 'EcsCluster97242B84' }, }, @@ -1364,7 +1357,7 @@ nodeunitShim({ statistic: 'Average', }); - test.deepEqual(stack.resolve(cluster.metricMemoryReservation()), { + expect(stack.resolve(cluster.metricMemoryReservation())).toEqual({ dimensions: { ClusterName: { Ref: 'EcsCluster97242B84' }, }, @@ -1374,7 +1367,7 @@ nodeunitShim({ statistic: 'Average', }); - test.deepEqual(stack.resolve(cluster.metric('myMetric')), { + expect(stack.resolve(cluster.metric('myMetric'))).toEqual({ dimensions: { ClusterName: { Ref: 'EcsCluster97242B84' }, }, @@ -1384,10 +1377,10 @@ nodeunitShim({ statistic: 'Average', }); - test.done(); - }, - 'ASG with a public VPC without NAT Gateways'(test: Test) { + }); + + test('ASG with a public VPC without NAT Gateways', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyPublicVpc', { @@ -1409,9 +1402,9 @@ nodeunitShim({ }, }); - expect(stack).to(haveResource('AWS::ECS::Cluster')); + expect(stack).toHaveResource('AWS::ECS::Cluster'); - expect(stack).to(haveResource('AWS::EC2::VPC', { + expect(stack).toHaveResource('AWS::EC2::VPC', { CidrBlock: '10.0.0.0/16', EnableDnsHostnames: true, EnableDnsSupport: true, @@ -1422,9 +1415,9 @@ nodeunitShim({ Value: 'Default/MyPublicVpc', }, ], - })); + }); - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, @@ -1456,9 +1449,9 @@ nodeunitShim({ ], }, }, - })); + }); - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MaxSize: '1', MinSize: '1', LaunchConfigurationName: { @@ -1479,9 +1472,9 @@ nodeunitShim({ Ref: 'MyPublicVpcingressSubnet2SubnetD2F2E034', }, ], - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup', SecurityGroupEgress: [ { @@ -1499,13 +1492,13 @@ nodeunitShim({ VpcId: { Ref: 'MyPublicVpcA2BF6CDA', }, - })); + }); // THEN - test.done(); - }, - 'enable container insights'(test: Test) { + }); + + test('enable container insights', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1513,19 +1506,19 @@ nodeunitShim({ new ecs.Cluster(stack, 'EcsCluster', { containerInsights: true }); // THEN - expect(stack).to(haveResource('AWS::ECS::Cluster', { + expect(stack).toHaveResource('AWS::ECS::Cluster', { ClusterSettings: [ { Name: 'containerInsights', Value: 'enabled', }, ], - }, ResourcePart.Properties)); + }); + - test.done(); - }, + }); - 'disable container insights'(test: Test) { + test('disable container insights', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1533,19 +1526,19 @@ nodeunitShim({ new ecs.Cluster(stack, 'EcsCluster', { containerInsights: false }); // THEN - expect(stack).to(haveResource('AWS::ECS::Cluster', { + expect(stack).toHaveResource('AWS::ECS::Cluster', { ClusterSettings: [ { Name: 'containerInsights', Value: 'disabled', }, ], - }, ResourcePart.Properties)); + }); + - test.done(); - }, + }); - 'default container insights is undefined'(test: Test) { + test('default container insights is undefined', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1557,17 +1550,15 @@ nodeunitShim({ const stackAssembly = assembly.getStackByName(stack.stackName); const template = stackAssembly.template; - test.equal( + expect( template.Resources.EcsCluster97242B84.Properties === undefined || template.Resources.EcsCluster97242B84.Properties.ClusterSettings === undefined, - true, - 'ClusterSettings should not be defined', - ); + ).toEqual(true); - test.done(); - }, - 'BottleRocketImage() returns correct AMI'(test: Test) { + }); + + test('BottleRocketImage() returns correct AMI', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1578,18 +1569,18 @@ nodeunitShim({ // THEN const assembly = app.synth(); const parameters = assembly.getStackByName(stack.stackName).template.Parameters; - test.ok(Object.entries(parameters).some( + expect(Object.entries(parameters).some( ([k, v]) => k.startsWith('SsmParameterValueawsservicebottlerocketawsecs') && (v as any).Default.includes('/bottlerocket/'), - ), 'Bottlerocket AMI should be in ssm parameters'); - test.ok(Object.entries(parameters).some( + )).toEqual(true); + expect(Object.entries(parameters).some( ([k, v]) => k.startsWith('SsmParameterValueawsservicebottlerocketawsecs') && (v as any).Default.includes('/aws-ecs-1/'), - ), 'ecs variant should be in ssm parameters'); - test.done(); - }, + )).toEqual(true); + + }); - 'cluster capacity with bottlerocket AMI, by setting machineImageType'(test: Test) { + test('cluster capacity with bottlerocket AMI, by setting machineImageType', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1601,9 +1592,9 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Cluster')); - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup')); - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::ECS::Cluster'); + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup'); + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsservicebottlerocketawsecs1x8664latestimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, @@ -1621,8 +1612,8 @@ nodeunitShim({ ], }, }, - })); - expect(stack).to(haveResourceLike('AWS::IAM::Role', { + }); + expect(stack).toHaveResourceLike('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -1677,12 +1668,11 @@ nodeunitShim({ Value: 'test/EcsCluster/bottlerocket-asg', }, ], - }), - ); - test.done(); - }, + }); - 'correct bottlerocket AMI for ARM64 architecture'(test: Test) { + }); + + test('correct bottlerocket AMI for ARM64 architecture', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1694,24 +1684,24 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsservicebottlerocketawsecs1arm64latestimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, - })); + }); const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - test.deepEqual(template.Parameters, { + expect(template.Parameters).toEqual({ SsmParameterValueawsservicebottlerocketawsecs1arm64latestimageidC96584B6F00A464EAD1953AFF4B05118Parameter: { Type: 'AWS::SSM::Parameter::Value', Default: '/aws/service/bottlerocket/aws-ecs-1/arm64/latest/image_id', }, }); - test.done(); - }, - 'throws when machineImage and machineImageType both specified'(test: Test) { + }); + + test('throws when machineImage and machineImageType both specified', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1723,7 +1713,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResourceLike('AWS::AutoScaling::LaunchConfiguration', { UserData: { 'Fn::Base64': { 'Fn::Join': [ @@ -1738,11 +1728,11 @@ nodeunitShim({ ], }, }, - })); - test.done(); - }, + }); - 'allows specifying capacityProviders (deprecated)'(test: Test) { + }); + + test('allows specifying capacityProviders (deprecated)', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1751,18 +1741,18 @@ nodeunitShim({ new ecs.Cluster(stack, 'EcsCluster', { capacityProviders: ['FARGATE_SPOT'] }); // THEN - expect(stack).to(haveResource('AWS::ECS::Cluster', { + expect(stack).toHaveResource('AWS::ECS::Cluster', { CapacityProviders: ABSENT, - })); + }); - expect(stack).to(haveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + expect(stack).toHaveResource('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE_SPOT'], - })); + }); + - test.done(); - }, + }); - 'allows specifying Fargate capacityProviders'(test: Test) { + test('allows specifying Fargate capacityProviders', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1773,18 +1763,18 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Cluster', { + expect(stack).toHaveResource('AWS::ECS::Cluster', { CapacityProviders: ABSENT, - })); + }); - expect(stack).to(haveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + expect(stack).toHaveResource('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE', 'FARGATE_SPOT'], - })); + }); - test.done(); - }, - 'allows specifying capacityProviders (alternate method)'(test: Test) { + }); + + test('allows specifying capacityProviders (alternate method)', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1794,18 +1784,18 @@ nodeunitShim({ cluster.enableFargateCapacityProviders(); // THEN - expect(stack).to(haveResource('AWS::ECS::Cluster', { + expect(stack).toHaveResource('AWS::ECS::Cluster', { CapacityProviders: ABSENT, - })); + }); - expect(stack).to(haveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + expect(stack).toHaveResource('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE', 'FARGATE_SPOT'], - })); + }); + - test.done(); - }, + }); - 'allows adding capacityProviders post-construction (deprecated)'(test: Test) { + test('allows adding capacityProviders post-construction (deprecated)', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1816,18 +1806,18 @@ nodeunitShim({ cluster.addCapacityProvider('FARGATE'); // does not add twice // THEN - expect(stack).to(haveResource('AWS::ECS::Cluster', { + expect(stack).toHaveResource('AWS::ECS::Cluster', { CapacityProviders: ABSENT, - })); + }); - expect(stack).to(haveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + expect(stack).toHaveResource('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE'], - })); + }); - test.done(); - }, - 'allows adding capacityProviders post-construction'(test: Test) { + }); + + test('allows adding capacityProviders post-construction', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1838,32 +1828,32 @@ nodeunitShim({ cluster.addCapacityProvider('FARGATE'); // does not add twice // THEN - expect(stack).to(haveResource('AWS::ECS::Cluster', { + expect(stack).toHaveResource('AWS::ECS::Cluster', { CapacityProviders: ABSENT, - })); + }); - expect(stack).to(haveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + expect(stack).toHaveResource('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE'], - })); + }); + - test.done(); - }, + }); - 'throws for unsupported capacity providers'(test: Test) { + test('throws for unsupported capacity providers', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); const cluster = new ecs.Cluster(stack, 'EcsCluster'); // THEN - test.throws(() => { + expect(() => { cluster.addCapacityProvider('HONK'); - }, /CapacityProvider not supported/); + }).toThrow(/CapacityProvider not supported/); - test.done(); - }, - 'creates ASG capacity providers with expected defaults'(test: Test) { + }); + + test('creates ASG capacity providers with expected defaults', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1880,7 +1870,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::CapacityProvider', { + expect(stack).toHaveResource('AWS::ECS::CapacityProvider', { AutoScalingGroupProvider: { AutoScalingGroupArn: { Ref: 'asgASG4D014670', @@ -1891,11 +1881,11 @@ nodeunitShim({ }, ManagedTerminationProtection: 'ENABLED', }, - })); - test.done(); - }, + }); - 'can disable managed scaling for ASG capacity provider'(test: Test) { + }); + + test('can disable managed scaling for ASG capacity provider', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1913,7 +1903,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::CapacityProvider', { + expect(stack).toHaveResource('AWS::ECS::CapacityProvider', { AutoScalingGroupProvider: { AutoScalingGroupArn: { Ref: 'asgASG4D014670', @@ -1921,11 +1911,11 @@ nodeunitShim({ ManagedScaling: ABSENT, ManagedTerminationProtection: 'ENABLED', }, - })); - test.done(); - }, + }); - 'capacity provider enables ASG new instance scale-in protection by default'(test: Test) { + }); + + test('capacity provider enables ASG new instance scale-in protection by default', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1942,13 +1932,13 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { NewInstancesProtectedFromScaleIn: true, - })); - test.done(); - }, + }); - 'capacity provider disables ASG new instance scale-in protection'(test: Test) { + }); + + test('capacity provider disables ASG new instance scale-in protection', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1966,13 +1956,13 @@ nodeunitShim({ }); // THEN - expect(stack).notTo(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).not.toHaveResource('AWS::AutoScaling::AutoScalingGroup', { NewInstancesProtectedFromScaleIn: true, - })); - test.done(); - }, + }); - 'can add ASG capacity via Capacity Provider'(test: Test) { + }); + + test('can add ASG capacity via Capacity Provider', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1998,7 +1988,7 @@ nodeunitShim({ cluster.addAsgCapacityProvider(capacityProvider); // THEN - expect(stack).to(haveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + expect(stack).toHaveResource('AWS::ECS::ClusterCapacityProviderAssociations', { Cluster: { Ref: 'EcsCluster97242B84', }, @@ -2010,11 +2000,11 @@ nodeunitShim({ }, ], DefaultCapacityProviderStrategy: [], - })); - test.done(); - }, + }); - 'correctly sets log configuration for execute command'(test: Test) { + }); + + test('correctly sets log configuration for execute command', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -2045,7 +2035,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Cluster', { + expect(stack).toHaveResource('AWS::ECS::Cluster', { Configuration: { ExecuteCommandConfiguration: { KmsKeyId: { @@ -2068,29 +2058,29 @@ nodeunitShim({ Logging: 'OVERRIDE', }, }, - })); + }); - test.done(); - }, - 'throws when no log configuration is provided when logging is set to OVERRIDE'(test: Test) { + }); + + test('throws when no log configuration is provided when logging is set to OVERRIDE', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); // THEN - test.throws(() => { + expect(() => { new ecs.Cluster(stack, 'EcsCluster', { executeCommandConfiguration: { logging: ecs.ExecuteCommandLogging.OVERRIDE, }, }); - }, /Execute command log configuration must only be specified when logging is OVERRIDE./); + }).toThrow(/Execute command log configuration must only be specified when logging is OVERRIDE./); + - test.done(); - }, + }); - 'throws when log configuration provided but logging is set to DEFAULT'(test: Test) { + test('throws when log configuration provided but logging is set to DEFAULT', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -2098,7 +2088,7 @@ nodeunitShim({ const logGroup = new logs.LogGroup(stack, 'LogGroup'); // THEN - test.throws(() => { + expect(() => { new ecs.Cluster(stack, 'EcsCluster', { executeCommandConfiguration: { logConfiguration: { @@ -2107,18 +2097,18 @@ nodeunitShim({ logging: ecs.ExecuteCommandLogging.DEFAULT, }, }); - }, /Execute command log configuration must only be specified when logging is OVERRIDE./); + }).toThrow(/Execute command log configuration must only be specified when logging is OVERRIDE./); - test.done(); - }, - 'throws when CloudWatchEncryptionEnabled without providing CloudWatch Logs log group name'(test: Test) { + }); + + test('throws when CloudWatchEncryptionEnabled without providing CloudWatch Logs log group name', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); // THEN - test.throws(() => { + expect(() => { new ecs.Cluster(stack, 'EcsCluster', { executeCommandConfiguration: { logConfiguration: { @@ -2127,18 +2117,18 @@ nodeunitShim({ logging: ecs.ExecuteCommandLogging.OVERRIDE, }, }); - }, /You must specify a CloudWatch log group in the execute command log configuration to enable CloudWatch encryption./); + }).toThrow(/You must specify a CloudWatch log group in the execute command log configuration to enable CloudWatch encryption./); + - test.done(); - }, + }); - 'throws when S3EncryptionEnabled without providing S3 Bucket name'(test: Test) { + test('throws when S3EncryptionEnabled without providing S3 Bucket name', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); // THEN - test.throws(() => { + expect(() => { new ecs.Cluster(stack, 'EcsCluster', { executeCommandConfiguration: { logConfiguration: { @@ -2147,8 +2137,8 @@ nodeunitShim({ logging: ecs.ExecuteCommandLogging.OVERRIDE, }, }); - }, /You must specify an S3 bucket name in the execute command log configuration to enable S3 encryption./); + }).toThrow(/You must specify an S3 bucket name in the execute command log configuration to enable S3 encryption./); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/cross-stack.test.ts b/packages/@aws-cdk/aws-ecs/test/ec2/cross-stack.test.ts index 7e6fce7a4f301..7decd0047975e 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/cross-stack.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/cross-stack.test.ts @@ -1,8 +1,7 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import { App, Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../../lib'; // Test various cross-stack Cluster/Service/ALB scenario's @@ -13,8 +12,8 @@ let stack2: Stack; let cluster: ecs.Cluster; let service: ecs.Ec2Service; -nodeunitShim({ - 'setUp'(cb: () => void) { +describe('cross stack', () => { + beforeEach(() => { app = new App(); stack1 = new Stack(app, 'Stack1'); @@ -37,10 +36,10 @@ nodeunitShim({ taskDefinition, }); - cb(); - }, - 'ALB next to Service'(test: Test) { + }); + + test('ALB next to Service', () => { // WHEN const lb = new elbv2.ApplicationLoadBalancer(stack2, 'ALB', { vpc: cluster.vpc }); const listener = lb.addListener('listener', { port: 80 }); @@ -50,14 +49,14 @@ nodeunitShim({ }); // THEN: it shouldn't throw due to cyclic dependencies - expect(stack2).to(haveResource('AWS::ECS::Service')); + expect(stack2).toHaveResource('AWS::ECS::Service'); expectIngress(stack2); - test.done(); - }, - 'ALB next to Cluster'(test: Test) { + }); + + test('ALB next to Cluster', () => { // WHEN const lb = new elbv2.ApplicationLoadBalancer(stack1, 'ALB', { vpc: cluster.vpc }); const listener = lb.addListener('listener', { port: 80 }); @@ -67,13 +66,13 @@ nodeunitShim({ }); // THEN: it shouldn't throw due to cyclic dependencies - expect(stack2).to(haveResource('AWS::ECS::Service')); + expect(stack2).toHaveResource('AWS::ECS::Service'); expectIngress(stack2); - test.done(); - }, - 'ALB in its own stack'(test: Test) { + }); + + test('ALB in its own stack', () => { // WHEN const stack3 = new Stack(app, 'Stack3'); const lb = new elbv2.ApplicationLoadBalancer(stack3, 'ALB', { vpc: cluster.vpc }); @@ -84,17 +83,17 @@ nodeunitShim({ }); // THEN: it shouldn't throw due to cyclic dependencies - expect(stack2).to(haveResource('AWS::ECS::Service')); + expect(stack2).toHaveResource('AWS::ECS::Service'); expectIngress(stack2); - test.done(); - }, + + }); }); function expectIngress(stack: Stack) { - expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { FromPort: 32768, ToPort: 65535, GroupId: { 'Fn::ImportValue': 'Stack1:ExportsOutputFnGetAttClusterDefaultAutoScalingGroupInstanceSecurityGroup1D15236AGroupIdEAB9C5E1' }, - })); + }); } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts index 7e942c866b213..48da04142ff3c 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts @@ -1,4 +1,5 @@ -import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert-internal'; +import { SynthUtils } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as autoscaling from '@aws-cdk/aws-autoscaling'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elb from '@aws-cdk/aws-elasticloadbalancing'; @@ -8,14 +9,13 @@ import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../../lib'; import { DeploymentControllerType, LaunchType, PropagatedTagSource } from '../../lib/base/base-service'; import { PlacementConstraint, PlacementStrategy } from '../../lib/placement'; -nodeunitShim({ - 'When creating an EC2 Service': { - 'with only required properties set, it correctly sets default properties'(test: Test) { +describe('ec2 service', () => { + describe('When creating an EC2 Service', () => { + test('with only required properties set, it correctly sets default properties', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -34,7 +34,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { TaskDefinition: { Ref: 'Ec2TaskDef0226F28C', }, @@ -48,14 +48,14 @@ nodeunitShim({ LaunchType: LaunchType.EC2, SchedulingStrategy: 'REPLICA', EnableECSManagedTags: false, - })); + }); - test.notEqual(service.node.defaultChild, undefined); + expect(service.node.defaultChild).toBeDefined(); - test.done(); - }, - 'allows setting enable execute command'(test: Test) { + }); + + test('allows setting enable execute command', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -75,7 +75,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { TaskDefinition: { Ref: 'Ec2TaskDef0226F28C', }, @@ -90,9 +90,9 @@ nodeunitShim({ SchedulingStrategy: 'REPLICA', EnableECSManagedTags: false, EnableExecuteCommand: true, - })); + }); - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -128,12 +128,12 @@ nodeunitShim({ Ref: 'Ec2TaskDefTaskRole400FA349', }, ], - })); + }); - test.done(); - }, - 'no logging enabled when logging field is set to NONE'(test: Test) { + }); + + test('no logging enabled when logging field is set to NONE', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -166,7 +166,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -188,12 +188,12 @@ nodeunitShim({ Ref: 'Ec2TaskDefTaskRole400FA349', }, ], - })); + }); + - test.done(); - }, + }); - 'enables execute command logging when logging field is set to OVERRIDE'(test: Test) { + test('enables execute command logging when logging field is set to OVERRIDE', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -229,7 +229,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -305,12 +305,12 @@ nodeunitShim({ Ref: 'Ec2TaskDefTaskRole400FA349', }, ], - })); + }); - test.done(); - }, - 'enables only execute command session encryption'(test: Test) { + }); + + test('enables only execute command session encryption', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -349,7 +349,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -438,9 +438,9 @@ nodeunitShim({ Ref: 'Ec2TaskDefTaskRole400FA349', }, ], - })); + }); - expect(stack).to(haveResource('AWS::KMS::Key', { + expect(stack).toHaveResource('AWS::KMS::Key', { KeyPolicy: { Statement: [ { @@ -504,12 +504,12 @@ nodeunitShim({ ], Version: '2012-10-17', }, - })); + }); + - test.done(); - }, + }); - 'enables encryption for execute command logging'(test: Test) { + test('enables encryption for execute command logging', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -555,7 +555,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -659,9 +659,9 @@ nodeunitShim({ Ref: 'Ec2TaskDefTaskRole400FA349', }, ], - })); + }); - expect(stack).to(haveResource('AWS::KMS::Key', { + expect(stack).toHaveResource('AWS::KMS::Key', { KeyPolicy: { Statement: [ { @@ -770,12 +770,12 @@ nodeunitShim({ ], Version: '2012-10-17', }, - })); + }); + - test.done(); - }, + }); - 'with custom cloudmap namespace'(test: Test) { + test('with custom cloudmap namespace', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -805,7 +805,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ServiceDiscovery::Service', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -831,19 +831,19 @@ nodeunitShim({ 'Id', ], }, - })); + }); - expect(stack).to(haveResource('AWS::ServiceDiscovery::PrivateDnsNamespace', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::PrivateDnsNamespace', { Name: 'scorekeep.com', Vpc: { Ref: 'MyVpcF9F0CA6F', }, - })); + }); + - test.done(); - }, + }); - 'with all properties set'(test: Test) { + test('with all properties set', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -896,7 +896,7 @@ nodeunitShim({ service.addPlacementStrategies(PlacementStrategy.spreadAcross(ecs.BuiltInAttributes.AVAILABILITY_ZONE)); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { TaskDefinition: { Ref: 'Ec2TaskDef0226F28C', }, @@ -957,12 +957,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'with autoscaling group capacity provider'(test: Test) { + }); + + test('with autoscaling group capacity provider', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Vpc'); @@ -1002,7 +1002,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { CapacityProviderStrategy: [ { CapacityProvider: { @@ -1010,11 +1010,11 @@ nodeunitShim({ }, }, ], - })); - test.done(); - }, + }); - 'with multiple security groups, it correctly updates the cfn template'(test: Test) { + }); + + test('with multiple security groups, it correctly updates the cfn template', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1053,7 +1053,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { TaskDefinition: { Ref: 'Ec2TaskDef0226F28C', }, @@ -1091,9 +1091,9 @@ nodeunitShim({ }, SchedulingStrategy: 'REPLICA', ServiceName: 'bonjour', - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { GroupDescription: 'Example', GroupName: 'Bingo', SecurityGroupEgress: [ @@ -1106,9 +1106,9 @@ nodeunitShim({ VpcId: { Ref: 'MyVpcF9F0CA6F', }, - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { GroupDescription: 'Example', GroupName: 'Rolly', SecurityGroupEgress: [ @@ -1123,12 +1123,12 @@ nodeunitShim({ VpcId: { Ref: 'MyVpcF9F0CA6F', }, - })); + }); - test.done(); - }, - 'throws when both securityGroup and securityGroups are supplied'(test: Test) { + }); + + test('throws when both securityGroup and securityGroups are supplied', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1155,7 +1155,7 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Ec2Service', { cluster, taskDefinition, @@ -1168,12 +1168,12 @@ nodeunitShim({ serviceName: 'bonjour', vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, }); - }, /Only one of SecurityGroup or SecurityGroups can be populated./); + }).toThrow(/Only one of SecurityGroup or SecurityGroups can be populated./); - test.done(); - }, - 'throws when task definition is not EC2 compatible'(test: Test) { + }); + + test('throws when task definition is not EC2 compatible', () => { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); @@ -1188,17 +1188,17 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Ec2Service', { cluster, taskDefinition, }); - }, /Supplied TaskDefinition is not configured for compatibility with EC2/); + }).toThrow(/Supplied TaskDefinition is not configured for compatibility with EC2/); - test.done(); - }, - 'ignore task definition and launch type if deployment controller is set to be EXTERNAL'(test: Test) { + }); + + test('ignore task definition and launch type if deployment controller is set to be EXTERNAL', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1220,8 +1220,8 @@ nodeunitShim({ }); // THEN - test.deepEqual(service.node.metadata[0].data, 'taskDefinition and launchType are blanked out when using external deployment controller.'); - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(service.node.metadata[0].data).toEqual('taskDefinition and launchType are blanked out when using external deployment controller.'); + expect(stack).toHaveResource('AWS::ECS::Service', { Cluster: { Ref: 'EcsCluster97242B84', }, @@ -1231,12 +1231,12 @@ nodeunitShim({ }, SchedulingStrategy: 'REPLICA', EnableECSManagedTags: false, - })); + }); - test.done(); - }, - 'errors if daemon and desiredCount both specified'(test: Test) { + }); + + test('errors if daemon and desiredCount both specified', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1249,19 +1249,19 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Ec2Service', { cluster, taskDefinition, daemon: true, desiredCount: 2, }); - }, /Don't supply desiredCount/); + }).toThrow(/Don't supply desiredCount/); - test.done(); - }, - 'errors if daemon and maximumPercent not 100'(test: Test) { + }); + + test('errors if daemon and maximumPercent not 100', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1274,19 +1274,19 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Ec2Service', { cluster, taskDefinition, daemon: true, maxHealthyPercent: 300, }); - }, /Maximum percent must be 100 for daemon mode./); + }).toThrow(/Maximum percent must be 100 for daemon mode./); - test.done(); - }, - 'errors if minimum not less than maximum'(test: Test) { + }); + + test('errors if minimum not less than maximum', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1299,7 +1299,7 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Ec2Service', { cluster, taskDefinition, @@ -1307,12 +1307,12 @@ nodeunitShim({ minHealthyPercent: 100, maxHealthyPercent: 100, }); - }, /Minimum healthy percent must be less than maximum healthy percent./); + }).toThrow(/Minimum healthy percent must be less than maximum healthy percent./); - test.done(); - }, - 'errors if no container definitions'(test: Test) { + }); + + test('errors if no container definitions', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1326,14 +1326,14 @@ nodeunitShim({ }); // THEN - test.throws(() => { - expect(stack); - }, /one essential container/); + expect(() => { + SynthUtils.synthesize(stack); + }).toThrow(/one essential container/); - test.done(); - }, - 'allows adding the default container after creating the service'(test: Test) { + }); + + test('allows adding the default container after creating the service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1353,18 +1353,18 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Name: 'main', }, ], - })); + }); - test.done(); - }, - 'sets daemon scheduling strategy'(test: Test) { + }); + + test('sets daemon scheduling strategy', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1384,19 +1384,19 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { SchedulingStrategy: 'DAEMON', DeploymentConfiguration: { MaximumPercent: 100, MinimumHealthyPercent: 0, }, - })); + }); - test.done(); - }, - 'with a TaskDefinition with Bridge network mode': { - 'it errors if vpcSubnets is specified'(test: Test) { + }); + + describe('with a TaskDefinition with Bridge network mode', () => { + test('it errors if vpcSubnets is specified', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1412,7 +1412,7 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Ec2Service', { cluster, taskDefinition, @@ -1423,10 +1423,10 @@ nodeunitShim({ }); // THEN - test.done(); - }, - 'it errors if assignPublicIp is true'(test: Test) { + }); + + test('it errors if assignPublicIp is true', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1442,19 +1442,19 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Ec2Service', { cluster, taskDefinition, assignPublicIp: true, }); - }, /vpcSubnets, securityGroup\(s\) and assignPublicIp can only be used in AwsVpc networking mode/); + }).toThrow(/vpcSubnets, securityGroup\(s\) and assignPublicIp can only be used in AwsVpc networking mode/); // THEN - test.done(); - }, - 'it errors if vpc subnets is provided'(test: Test) { + }); + + test('it errors if vpc subnets is provided', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1474,7 +1474,7 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Ec2Service', { cluster, taskDefinition, @@ -1482,13 +1482,13 @@ nodeunitShim({ subnets: [subnet], }, }); - }, /vpcSubnets, securityGroup\(s\) and assignPublicIp can only be used in AwsVpc networking mode/); + }).toThrow(/vpcSubnets, securityGroup\(s\) and assignPublicIp can only be used in AwsVpc networking mode/); // THEN - test.done(); - }, - 'it errors if security group is provided'(test: Test) { + }); + + test('it errors if security group is provided', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1504,19 +1504,19 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Ec2Service', { cluster, taskDefinition, securityGroup, }); - }, /vpcSubnets, securityGroup\(s\) and assignPublicIp can only be used in AwsVpc networking mode/); + }).toThrow(/vpcSubnets, securityGroup\(s\) and assignPublicIp can only be used in AwsVpc networking mode/); // THEN - test.done(); - }, - 'it errors if multiple security groups is provided'(test: Test) { + }); + + test('it errors if multiple security groups is provided', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1535,21 +1535,21 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Ec2Service', { cluster, taskDefinition, securityGroups, }); - }, /vpcSubnets, securityGroup\(s\) and assignPublicIp can only be used in AwsVpc networking mode/); + }).toThrow(/vpcSubnets, securityGroup\(s\) and assignPublicIp can only be used in AwsVpc networking mode/); // THEN - test.done(); - }, - }, - 'with a TaskDefinition with AwsVpc network mode': { - 'it creates a security group for the service'(test: Test) { + }); + }); + + describe('with a TaskDefinition with AwsVpc network mode', () => { + test('it creates a security group for the service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1570,7 +1570,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { NetworkConfiguration: { AwsvpcConfiguration: { AssignPublicIp: 'DISABLED', @@ -1592,12 +1592,12 @@ nodeunitShim({ ], }, }, - })); + }); - test.done(); - }, - 'it allows vpcSubnets'(test: Test) { + }); + + test('it allows vpcSubnets', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1621,11 +1621,11 @@ nodeunitShim({ }); // THEN - test.done(); - }, - }, - 'with distinctInstance placement constraint'(test: Test) { + }); + }); + + test('with distinctInstance placement constraint', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1645,16 +1645,16 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { PlacementConstraints: [{ Type: 'distinctInstance', }], - })); + }); + - test.done(); - }, + }); - 'with memberOf placement constraints'(test: Test) { + test('with memberOf placement constraints', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1675,17 +1675,17 @@ nodeunitShim({ service.addPlacementConstraints(PlacementConstraint.memberOf('attribute:ecs.instance-type =~ t2.*')); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { PlacementConstraints: [{ Expression: 'attribute:ecs.instance-type =~ t2.*', Type: 'memberOf', }], - })); + }); + - test.done(); - }, + }); - 'with spreadAcross container instances strategy'(test: Test) { + test('with spreadAcross container instances strategy', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1707,17 +1707,17 @@ nodeunitShim({ service.addPlacementStrategies(PlacementStrategy.spreadAcrossInstances()); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { PlacementStrategies: [{ Field: 'instanceId', Type: 'spread', }], - })); + }); + - test.done(); - }, + }); - 'with spreadAcross placement strategy'(test: Test) { + test('with spreadAcross placement strategy', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1738,36 +1738,36 @@ nodeunitShim({ service.addPlacementStrategies(PlacementStrategy.spreadAcross(ecs.BuiltInAttributes.AVAILABILITY_ZONE)); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { PlacementStrategies: [{ Field: 'attribute:ecs.availability-zone', Type: 'spread', }], - })); + }); + - test.done(); - }, + }); - 'can turn PlacementStrategy into json format'(test: Test) { + test('can turn PlacementStrategy into json format', () => { // THEN - test.deepEqual(PlacementStrategy.spreadAcross(ecs.BuiltInAttributes.AVAILABILITY_ZONE).toJson(), [{ + expect(PlacementStrategy.spreadAcross(ecs.BuiltInAttributes.AVAILABILITY_ZONE).toJson()).toEqual([{ type: 'spread', field: 'attribute:ecs.availability-zone', }]); - test.done(); - }, - 'can turn PlacementConstraints into json format'(test: Test) { + }); + + test('can turn PlacementConstraints into json format', () => { // THEN - test.deepEqual(PlacementConstraint.distinctInstances().toJson(), [{ + expect(PlacementConstraint.distinctInstances().toJson()).toEqual([{ type: 'distinctInstance', }]); - test.done(); - }, - 'errors when spreadAcross with no input'(test: Test) { + }); + + test('errors when spreadAcross with no input', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1786,14 +1786,14 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { service.addPlacementStrategies(PlacementStrategy.spreadAcross()); - }, 'spreadAcross: give at least one field to spread by'); + }).toThrow('spreadAcross: give at least one field to spread by'); + - test.done(); - }, + }); - 'errors with spreadAcross placement strategy if daemon specified'(test: Test) { + test('errors with spreadAcross placement strategy if daemon specified', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1813,14 +1813,14 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { service.addPlacementStrategies(PlacementStrategy.spreadAcross(ecs.BuiltInAttributes.AVAILABILITY_ZONE)); }); - test.done(); - }, - 'with no placement constraints'(test: Test) { + }); + + test('with no placement constraints', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1839,14 +1839,14 @@ nodeunitShim({ }); // THEN - expect(stack).notTo(haveResource('AWS::ECS::Service', { + expect(stack).not.toHaveResource('AWS::ECS::Service', { PlacementConstraints: undefined, - })); + }); + - test.done(); - }, + }); - 'with both propagateTags and propagateTaskTagsFrom defined'(test: Test) { + test('with both propagateTags and propagateTaskTagsFrom defined', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1859,18 +1859,18 @@ nodeunitShim({ memoryLimitMiB: 512, }); - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Ec2Service', { cluster, taskDefinition, propagateTags: PropagatedTagSource.SERVICE, propagateTaskTagsFrom: PropagatedTagSource.SERVICE, }); - }, /You can only specify either propagateTags or propagateTaskTagsFrom. Alternatively, you can leave both blank/); - test.done(); - }, + }).toThrow(/You can only specify either propagateTags or propagateTaskTagsFrom. Alternatively, you can leave both blank/); + + }); - 'with no placement strategy if daemon specified'(test: Test) { + test('with no placement strategy if daemon specified', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1890,14 +1890,14 @@ nodeunitShim({ }); // THEN - expect(stack).notTo(haveResource('AWS::ECS::Service', { + expect(stack).not.toHaveResource('AWS::ECS::Service', { PlacementStrategies: undefined, - })); + }); + - test.done(); - }, + }); - 'with random placement strategy'(test: Test) { + test('with random placement strategy', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc'); @@ -1918,16 +1918,16 @@ nodeunitShim({ service.addPlacementStrategies(PlacementStrategy.randomly()); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { PlacementStrategies: [{ Type: 'random', }], - })); + }); - test.done(); - }, - 'errors with random placement strategy if daemon specified'(test: Test) { + }); + + test('errors with random placement strategy if daemon specified', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc'); @@ -1947,14 +1947,14 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { service.addPlacementStrategies(PlacementStrategy.randomly()); - }); + }).toThrow(); - test.done(); - }, - 'with packedbyCpu placement strategy'(test: Test) { + }); + + test('with packedbyCpu placement strategy', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1975,17 +1975,17 @@ nodeunitShim({ service.addPlacementStrategies(PlacementStrategy.packedByCpu()); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { PlacementStrategies: [{ Field: 'cpu', Type: 'binpack', }], - })); + }); + - test.done(); - }, + }); - 'with packedbyMemory placement strategy'(test: Test) { + test('with packedbyMemory placement strategy', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2006,17 +2006,17 @@ nodeunitShim({ service.addPlacementStrategies(PlacementStrategy.packedByMemory()); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { PlacementStrategies: [{ Field: 'memory', Type: 'binpack', }], - })); + }); - test.done(); - }, - 'with packedBy placement strategy'(test: Test) { + }); + + test('with packedBy placement strategy', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2037,17 +2037,17 @@ nodeunitShim({ service.addPlacementStrategies(PlacementStrategy.packedBy(ecs.BinPackResource.MEMORY)); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { PlacementStrategies: [{ Field: 'memory', Type: 'binpack', }], - })); + }); + - test.done(); - }, + }); - 'errors with packedBy placement strategy if daemon specified'(test: Test) { + test('errors with packedBy placement strategy if daemon specified', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2067,16 +2067,16 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { service.addPlacementStrategies(PlacementStrategy.packedBy(ecs.BinPackResource.MEMORY)); - }); + }).toThrow(); - test.done(); - }, - }, - 'attachToClassicLB': { - 'allows network mode of task definition to be host'(test: Test) { + }); + }); + + describe('attachToClassicLB', () => { + test('allows network mode of task definition to be host', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -2097,10 +2097,10 @@ nodeunitShim({ const lb = new elb.LoadBalancer(stack, 'LB', { vpc }); service.attachToClassicLB(lb); - test.done(); - }, - 'allows network mode of task definition to be bridge'(test: Test) { + }); + + test('allows network mode of task definition to be bridge', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -2121,10 +2121,10 @@ nodeunitShim({ const lb = new elb.LoadBalancer(stack, 'LB', { vpc }); service.attachToClassicLB(lb); - test.done(); - }, - 'throws when network mode of task definition is AwsVpc'(test: Test) { + }); + + test('throws when network mode of task definition is AwsVpc', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -2143,14 +2143,14 @@ nodeunitShim({ // THEN const lb = new elb.LoadBalancer(stack, 'LB', { vpc }); - test.throws(() => { + expect(() => { service.attachToClassicLB(lb); - }, /Cannot use a Classic Load Balancer if NetworkMode is AwsVpc. Use Host or Bridge instead./); + }).toThrow(/Cannot use a Classic Load Balancer if NetworkMode is AwsVpc. Use Host or Bridge instead./); - test.done(); - }, - 'throws when network mode of task definition is none'(test: Test) { + }); + + test('throws when network mode of task definition is none', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -2169,16 +2169,16 @@ nodeunitShim({ // THEN const lb = new elb.LoadBalancer(stack, 'LB', { vpc }); - test.throws(() => { + expect(() => { service.attachToClassicLB(lb); - }, /Cannot use a Classic Load Balancer if NetworkMode is None. Use Host or Bridge instead./); + }).toThrow(/Cannot use a Classic Load Balancer if NetworkMode is None. Use Host or Bridge instead./); - test.done(); - }, - }, - 'attachToApplicationTargetGroup': { - 'allows network mode of task definition to be other than none'(test: Test) { + }); + }); + + describe('attachToApplicationTargetGroup', () => { + test('allows network mode of task definition to be other than none', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2203,10 +2203,10 @@ nodeunitShim({ // THEN service.attachToApplicationTargetGroup(targetGroup); - test.done(); - }, - 'throws when network mode of task definition is none'(test: Test) { + }); + + test('throws when network mode of task definition is none', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2229,15 +2229,15 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { service.attachToApplicationTargetGroup(targetGroup); - }, /Cannot use a load balancer if NetworkMode is None. Use Bridge, Host or AwsVpc instead./); + }).toThrow(/Cannot use a load balancer if NetworkMode is None. Use Bridge, Host or AwsVpc instead./); - test.done(); - }, - 'correctly setting ingress and egress port': { - 'with bridge/NAT network mode and 0 host port'(test: Test) { + }); + + describe('correctly setting ingress and egress port', () => { + test('with bridge/NAT network mode and 0 host port', () => { [ecs.NetworkMode.BRIDGE, ecs.NetworkMode.NAT].forEach((networkMode: ecs.NetworkMode) => { // GIVEN const stack = new cdk.Stack(); @@ -2269,23 +2269,23 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { Description: 'Load balancer to target', FromPort: 32768, ToPort: 65535, - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { Description: 'Load balancer to target', FromPort: 32768, ToPort: 65535, - })); + }); }); - test.done(); - }, - 'with bridge/NAT network mode and host port other than 0'(test: Test) { + }); + + test('with bridge/NAT network mode and host port other than 0', () => { [ecs.NetworkMode.BRIDGE, ecs.NetworkMode.NAT].forEach((networkMode: ecs.NetworkMode) => { // GIVEN const stack = new cdk.Stack(); @@ -2317,23 +2317,23 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { Description: 'Load balancer to target', FromPort: 80, ToPort: 80, - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { Description: 'Load balancer to target', FromPort: 80, ToPort: 80, - })); + }); }); - test.done(); - }, - 'with host network mode'(test: Test) { + }); + + test('with host network mode', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2364,22 +2364,22 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { Description: 'Load balancer to target', FromPort: 8001, ToPort: 8001, - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { Description: 'Load balancer to target', FromPort: 8001, ToPort: 8001, - })); + }); - test.done(); - }, - 'with aws_vpc network mode'(test: Test) { + }); + + test('with aws_vpc network mode', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2410,25 +2410,25 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { Description: 'Load balancer to target', FromPort: 8001, ToPort: 8001, - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { Description: 'Load balancer to target', FromPort: 8001, ToPort: 8001, - })); + }); - test.done(); - }, - }, - }, - 'attachToNetworkTargetGroup': { - 'allows network mode of task definition to be other than none'(test: Test) { + }); + }); + }); + + describe('attachToNetworkTargetGroup', () => { + test('allows network mode of task definition to be other than none', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2453,10 +2453,10 @@ nodeunitShim({ // THEN service.attachToNetworkTargetGroup(targetGroup); - test.done(); - }, - 'throws when network mode of task definition is none'(test: Test) { + }); + + test('throws when network mode of task definition is none', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2479,16 +2479,16 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { service.attachToNetworkTargetGroup(targetGroup); - }, /Cannot use a load balancer if NetworkMode is None. Use Bridge, Host or AwsVpc instead./); + }).toThrow(/Cannot use a load balancer if NetworkMode is None. Use Bridge, Host or AwsVpc instead./); - test.done(); - }, - }, - 'classic ELB': { - 'can attach to classic ELB'(test: Test) { + }); + }); + + describe('classic ELB', () => { + test('can attach to classic ELB', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -2510,7 +2510,7 @@ nodeunitShim({ lb.addTarget(service); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'web', @@ -2518,18 +2518,18 @@ nodeunitShim({ LoadBalancerName: { Ref: 'LB8A12904C' }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { // if any load balancer is configured and healthCheckGracePeriodSeconds is not // set, then it should default to 60 seconds. HealthCheckGracePeriodSeconds: 60, - })); + }); - test.done(); - }, - 'can attach any container and port as a target'(test: Test) { + }); + + test('can attach any container and port as a target', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -2555,7 +2555,7 @@ nodeunitShim({ })); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'web', @@ -2563,14 +2563,14 @@ nodeunitShim({ LoadBalancerName: { Ref: 'LB8A12904C' }, }, ], - })); + }); - test.done(); - }, - }, - 'When enabling service discovery': { - 'throws if namespace has not been added to cluster'(test: Test) { + }); + }); + + describe('When enabling service discovery', () => { + test('throws if namespace has not been added to cluster', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2586,7 +2586,7 @@ nodeunitShim({ container.addPortMappings({ containerPort: 8000 }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Service', { cluster, taskDefinition, @@ -2594,12 +2594,12 @@ nodeunitShim({ name: 'myApp', }, }); - }, /Cannot enable service discovery if a Cloudmap Namespace has not been created in the cluster./); + }).toThrow(/Cannot enable service discovery if a Cloudmap Namespace has not been created in the cluster./); + - test.done(); - }, + }); - 'throws if network mode is none'(test: Test) { + test('throws if network mode is none', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2617,7 +2617,7 @@ nodeunitShim({ cluster.addDefaultCloudMapNamespace({ name: 'foo.com' }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Service', { cluster, taskDefinition, @@ -2625,12 +2625,12 @@ nodeunitShim({ name: 'myApp', }, }); - }, /Cannot use a service discovery if NetworkMode is None. Use Bridge, Host or AwsVpc instead./); + }).toThrow(/Cannot use a service discovery if NetworkMode is None. Use Bridge, Host or AwsVpc instead./); - test.done(); - }, - 'creates AWS Cloud Map service for Private DNS namespace with bridge network mode'(test: Test) { + }); + + test('creates AWS Cloud Map service for Private DNS namespace with bridge network mode', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2660,7 +2660,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { ServiceRegistries: [ { ContainerName: 'MainContainer', @@ -2673,9 +2673,9 @@ nodeunitShim({ }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ServiceDiscovery::Service', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -2701,12 +2701,12 @@ nodeunitShim({ 'Id', ], }, - })); + }); + - test.done(); - }, + }); - 'creates AWS Cloud Map service for Private DNS namespace with host network mode'(test: Test) { + test('creates AWS Cloud Map service for Private DNS namespace with host network mode', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2737,7 +2737,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { ServiceRegistries: [ { ContainerName: 'MainContainer', @@ -2750,9 +2750,9 @@ nodeunitShim({ }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ServiceDiscovery::Service', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -2778,12 +2778,12 @@ nodeunitShim({ 'Id', ], }, - })); + }); + - test.done(); - }, + }); - 'throws if wrong DNS record type specified with bridge network mode'(test: Test) { + test('throws if wrong DNS record type specified with bridge network mode', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2803,7 +2803,7 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Service', { cluster, taskDefinition, @@ -2812,12 +2812,12 @@ nodeunitShim({ dnsRecordType: cloudmap.DnsRecordType.A, }, }); - }, /SRV records must be used when network mode is Bridge or Host./); + }).toThrow(/SRV records must be used when network mode is Bridge or Host./); + - test.done(); - }, + }); - 'creates AWS Cloud Map service for Private DNS namespace with AwsVpc network mode'(test: Test) { + test('creates AWS Cloud Map service for Private DNS namespace with AwsVpc network mode', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2848,7 +2848,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { ServiceRegistries: [ { RegistryArn: { @@ -2859,9 +2859,9 @@ nodeunitShim({ }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ServiceDiscovery::Service', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -2887,12 +2887,12 @@ nodeunitShim({ 'Id', ], }, - })); + }); + - test.done(); - }, + }); - 'creates AWS Cloud Map service for Private DNS namespace with AwsVpc network mode with SRV records'(test: Test) { + test('creates AWS Cloud Map service for Private DNS namespace with AwsVpc network mode with SRV records', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2924,7 +2924,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { ServiceRegistries: [ { ContainerName: 'MainContainer', @@ -2937,9 +2937,9 @@ nodeunitShim({ }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ServiceDiscovery::Service', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -2965,12 +2965,12 @@ nodeunitShim({ 'Id', ], }, - })); + }); + - test.done(); - }, + }); - 'user can select any container and port'(test: Test) { + test('user can select any container and port', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -3008,7 +3008,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::Service', { + expect(stack).toHaveResourceLike('AWS::ECS::Service', { ServiceRegistries: [ { RegistryArn: { 'Fn::GetAtt': ['ServiceCloudmapService046058A4', 'Arn'] }, @@ -3016,12 +3016,12 @@ nodeunitShim({ ContainerPort: 8001, }, ], - })); + }); + - test.done(); - }, + }); - 'By default, the container name is the default'(test: Test) { + test('By default, the container name is the default', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -3053,17 +3053,17 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::Service', { + expect(stack).toHaveResourceLike('AWS::ECS::Service', { ServiceRegistries: [{ ContainerName: 'main', ContainerPort: undefined, }], - })); + }); + - test.done(); - }, + }); - 'For SRV, by default, container name is default container and port is the default container port'(test: Test) { + test('For SRV, by default, container name is default container and port is the default container port', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -3097,17 +3097,17 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::Service', { + expect(stack).toHaveResourceLike('AWS::ECS::Service', { ServiceRegistries: [{ ContainerName: 'main', ContainerPort: 1234, }], - })); + }); + - test.done(); - }, + }); - 'allows SRV service discovery to select the container and port'(test: Test) { + test('allows SRV service discovery to select the container and port', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -3144,17 +3144,17 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::Service', { + expect(stack).toHaveResourceLike('AWS::ECS::Service', { ServiceRegistries: [{ ContainerName: 'second', ContainerPort: 4321, }], - })); + }); + - test.done(); - }, + }); - 'throws if SRV and container is not part of task definition'(test: Test) { + test('throws if SRV and container is not part of task definition', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -3182,7 +3182,7 @@ nodeunitShim({ }); // WHEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Service', { cluster, taskDefinition, @@ -3192,12 +3192,12 @@ nodeunitShim({ containerPort: 4321, }, }); - }, /another task definition/i); + }).toThrow(/another task definition/i); + - test.done(); - }, + }); - 'throws if SRV and the container port is not mapped'(test: Test) { + test('throws if SRV and the container port is not mapped', () => { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); @@ -3217,7 +3217,7 @@ nodeunitShim({ container.addPortMappings({ containerPort: 8000 }); - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Service', { cluster, taskDefinition, @@ -3227,13 +3227,13 @@ nodeunitShim({ containerPort: 4321, }, }); - }, /container port.*not.*mapped/i); + }).toThrow(/container port.*not.*mapped/i); + - test.done(); - }, - }, + }); + }); - 'Metric'(test: Test) { + test('Metric', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -3251,7 +3251,7 @@ nodeunitShim({ }); // THEN - test.deepEqual(stack.resolve(service.metricMemoryUtilization()), { + expect(stack.resolve(service.metricMemoryUtilization())).toEqual({ dimensions: { ClusterName: { Ref: 'EcsCluster97242B84' }, ServiceName: { 'Fn::GetAtt': ['ServiceD69D759B', 'Name'] }, @@ -3262,7 +3262,7 @@ nodeunitShim({ statistic: 'Average', }); - test.deepEqual(stack.resolve(service.metricCpuUtilization()), { + expect(stack.resolve(service.metricCpuUtilization())).toEqual({ dimensions: { ClusterName: { Ref: 'EcsCluster97242B84' }, ServiceName: { 'Fn::GetAtt': ['ServiceD69D759B', 'Name'] }, @@ -3273,11 +3273,11 @@ nodeunitShim({ statistic: 'Average', }); - test.done(); - }, - 'When import an EC2 Service': { - 'with serviceArn'(test: Test) { + }); + + describe('When import an EC2 Service', () => { + test('with serviceArn', () => { // GIVEN const stack = new cdk.Stack(); const cluster = new ecs.Cluster(stack, 'EcsCluster'); @@ -3289,13 +3289,13 @@ nodeunitShim({ }); // THEN - test.equal(service.serviceArn, 'arn:aws:ecs:us-west-2:123456789012:service/my-http-service'); - test.equal(service.serviceName, 'my-http-service'); + expect(service.serviceArn).toEqual('arn:aws:ecs:us-west-2:123456789012:service/my-http-service'); + expect(service.serviceName).toEqual('my-http-service'); - test.done(); - }, - 'with serviceName'(test: Test) { + }); + + test('with serviceName', () => { // GIVEN const stack = new cdk.Stack(); const pseudo = new cdk.ScopedAws(stack); @@ -3308,40 +3308,40 @@ nodeunitShim({ }); // THEN - test.deepEqual(stack.resolve(service.serviceArn), stack.resolve(`arn:${pseudo.partition}:ecs:${pseudo.region}:${pseudo.accountId}:service/my-http-service`)); - test.equal(service.serviceName, 'my-http-service'); + expect(stack.resolve(service.serviceArn)).toEqual(stack.resolve(`arn:${pseudo.partition}:ecs:${pseudo.region}:${pseudo.accountId}:service/my-http-service`)); + expect(service.serviceName).toEqual('my-http-service'); - test.done(); - }, - 'throws an exception if both serviceArn and serviceName were provided for fromEc2ServiceAttributes'(test: Test) { + }); + + test('throws an exception if both serviceArn and serviceName were provided for fromEc2ServiceAttributes', () => { // GIVEN const stack = new cdk.Stack(); const cluster = new ecs.Cluster(stack, 'EcsCluster'); - test.throws(() => { + expect(() => { ecs.Ec2Service.fromEc2ServiceAttributes(stack, 'EcsService', { serviceArn: 'arn:aws:ecs:us-west-2:123456789012:service/my-http-service', serviceName: 'my-http-service', cluster, }); - }, /only specify either serviceArn or serviceName/); + }).toThrow(/only specify either serviceArn or serviceName/); - test.done(); - }, - 'throws an exception if neither serviceArn nor serviceName were provided for fromEc2ServiceAttributes'(test: Test) { + }); + + test('throws an exception if neither serviceArn nor serviceName were provided for fromEc2ServiceAttributes', () => { // GIVEN const stack = new cdk.Stack(); const cluster = new ecs.Cluster(stack, 'EcsCluster'); - test.throws(() => { + expect(() => { ecs.Ec2Service.fromEc2ServiceAttributes(stack, 'EcsService', { cluster, }); - }, /only specify either serviceArn or serviceName/); + }).toThrow(/only specify either serviceArn or serviceName/); - test.done(); - }, - }, + + }); + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/environment-file.test.ts b/packages/@aws-cdk/aws-ecs/test/environment-file.test.ts index e80651cbbd65b..815973a80e602 100644 --- a/packages/@aws-cdk/aws-ecs/test/environment-file.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/environment-file.test.ts @@ -1,23 +1,23 @@ +import '@aws-cdk/assert-internal/jest'; import * as path from 'path'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../lib'; /* eslint-disable dot-notation */ -nodeunitShim({ - 'ecs.EnvironmentFile.fromAsset': { - 'fails if asset is not a single file'(test: Test) { +describe('environment file', () => { + describe('ecs.EnvironmentFile.fromAsset', () => { + test('fails if asset is not a single file', () => { // GIVEN const stack = new cdk.Stack(); const fileAsset = ecs.EnvironmentFile.fromAsset(path.join(__dirname, 'demo-envfiles')); // THEN - test.throws(() => defineContainerDefinition(stack, fileAsset), /Asset must be a single file/); - test.done(); - }, + expect(() => defineContainerDefinition(stack, fileAsset)).toThrow(/Asset must be a single file/); - 'only one environment file asset object is created even if multiple container definitions use the same file'(test: Test) { + }); + + test('only one environment file asset object is created even if multiple container definitions use the same file', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app); @@ -41,10 +41,10 @@ nodeunitShim({ const synthesized = assembly.stacks[0]; // container one has an asset, container two does not - test.deepEqual(synthesized.assets.length, 1); - test.done(); - }, - }, + expect(synthesized.assets.length).toEqual(1); + + }); + }); }); function defineContainerDefinition(stack: cdk.Stack, environmentFile: ecs.EnvironmentFile) { diff --git a/packages/@aws-cdk/aws-ecs/test/external/external-service.test.ts b/packages/@aws-cdk/aws-ecs/test/external/external-service.test.ts index d01eaa14f11b9..94384a464bb47 100644 --- a/packages/@aws-cdk/aws-ecs/test/external/external-service.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/external/external-service.test.ts @@ -4,13 +4,12 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../../lib'; import { LaunchType } from '../../lib/base/base-service'; -nodeunitShim({ - 'When creating an External Service': { - 'with only required properties set, it correctly sets default properties'(test: Test) { +describe('external service', () => { + describe('When creating an External Service', () => { + test('with only required properties set, it correctly sets default properties', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -44,13 +43,13 @@ nodeunitShim({ LaunchType: LaunchType.EXTERNAL, }); - test.notEqual(service.node.defaultChild, undefined); + expect(service.node.defaultChild).toBeDefined(); - test.done(); - }, - }, - 'with all properties set'(test: Test) { + }); + }); + + test('with all properties set', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -97,10 +96,10 @@ nodeunitShim({ ServiceName: 'bonjour', }); - test.done(); - }, - 'with cloudmap set on cluster, throw error'(test: Test) { + }); + + test('with cloudmap set on cluster, throw error', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -130,10 +129,10 @@ nodeunitShim({ serviceName: 'bonjour', })).toThrow('Cloud map integration is not supported for External service' ); - test.done(); - }, - 'with multiple security groups, it correctly updates the cfn template'(test: Test) { + }); + + test('with multiple security groups, it correctly updates the cfn template', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -205,10 +204,10 @@ nodeunitShim({ ], }); - test.done(); - }, - 'throws when task definition is not External compatible'(test: Test) { + }); + + test('throws when task definition is not External compatible', () => { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); @@ -227,10 +226,10 @@ nodeunitShim({ taskDefinition, })).toThrow('Supplied TaskDefinition is not configured for compatibility with ECS Anywhere cluster'); - test.done(); - }, - 'errors if minimum not less than maximum'(test: Test) { + }); + + test('errors if minimum not less than maximum', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -250,10 +249,10 @@ nodeunitShim({ maxHealthyPercent: 100, })).toThrow('Minimum healthy percent must be less than maximum healthy percent.'); - test.done(); - }, - 'error if cloudmap options provided with external service'(test: Test) { + }); + + test('error if cloudmap options provided with external service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -276,10 +275,10 @@ nodeunitShim({ })).toThrow('Cloud map options are not supported for External service'); // THEN - test.done(); - }, - 'error if enableExecuteCommand options provided with external service'(test: Test) { + }); + + test('error if enableExecuteCommand options provided with external service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -300,10 +299,10 @@ nodeunitShim({ })).toThrow('Enable Execute Command options are not supported for External service'); // THEN - test.done(); - }, - 'error if capacityProviderStrategies options provided with external service'(test: Test) { + }); + + test('error if capacityProviderStrategies options provided with external service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -338,10 +337,10 @@ nodeunitShim({ })).toThrow('Capacity Providers are not supported for External service'); // THEN - test.done(); - }, - 'error when performing attachToApplicationTargetGroup to an external service'(test: Test) { + }); + + test('error when performing attachToApplicationTargetGroup to an external service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -369,10 +368,10 @@ nodeunitShim({ expect(() => service.attachToApplicationTargetGroup(targetGroup)).toThrow('Application load balancer cannot be attached to an external service'); // THEN - test.done(); - }, - 'error when performing loadBalancerTarget to an external service'(test: Test) { + }); + + test('error when performing loadBalancerTarget to an external service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -396,10 +395,10 @@ nodeunitShim({ })).toThrow('External service cannot be attached as load balancer targets'); // THEN - test.done(); - }, - 'error when performing registerLoadBalancerTargets to an external service'(test: Test) { + }); + + test('error when performing registerLoadBalancerTargets to an external service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -430,10 +429,10 @@ nodeunitShim({ )).toThrow('External service cannot be registered as load balancer targets'); // THEN - test.done(); - }, - 'error when performing autoScaleTaskCount to an external service'(test: Test) { + }); + + test('error when performing autoScaleTaskCount to an external service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -458,10 +457,10 @@ nodeunitShim({ })).toThrow('Autoscaling not supported for external service'); // THEN - test.done(); - }, - 'error when performing enableCloudMap to an external service'(test: Test) { + }); + + test('error when performing enableCloudMap to an external service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -483,10 +482,10 @@ nodeunitShim({ expect(() => service.enableCloudMap({})).toThrow('Cloud map integration not supported for an external service'); // THEN - test.done(); - }, - 'error when performing associateCloudMapService to an external service'(test: Test) { + }); + + test('error when performing associateCloudMapService to an external service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -523,6 +522,6 @@ nodeunitShim({ })).toThrow('Cloud map service association is not supported for an external service'); // THEN - test.done(); - }, + + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/external/external-task-definition.test.ts b/packages/@aws-cdk/aws-ecs/test/external/external-task-definition.test.ts index c5fd8f942f1f0..c4e296b2c6831 100644 --- a/packages/@aws-cdk/aws-ecs/test/external/external-task-definition.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/external/external-task-definition.test.ts @@ -6,12 +6,11 @@ import * as iam from '@aws-cdk/aws-iam'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import * as ssm from '@aws-cdk/aws-ssm'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../../lib'; -nodeunitShim({ - 'When creating an External TaskDefinition': { - 'with only required properties set, it correctly sets default properties'(test: Test) { +describe('external task definition', () => { + describe('When creating an External TaskDefinition', () => { + test('with only required properties set, it correctly sets default properties', () => { // GIVEN const stack = new cdk.Stack(); new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); @@ -23,10 +22,10 @@ nodeunitShim({ RequiresCompatibilities: ['EXTERNAL'], }); - test.done(); - }, - 'with all properties set'(test: Test) { + }); + + test('with all properties set', () => { // GIVEN const stack = new cdk.Stack(); new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef', { @@ -64,10 +63,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 'correctly sets containers'(test: Test) { + }); + + test('correctly sets containers', () => { // GIVEN const stack = new cdk.Stack(); @@ -131,10 +130,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 'all container definition options defined'(test: Test) { + }); + + test('all container definition options defined', () => { const stack = new cdk.Stack(); const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); @@ -330,10 +329,10 @@ nodeunitShim({ ], }); - test.done(); - }, - 'correctly sets containers from ECR repository using all props'(test: Test) { + }); + + test('correctly sets containers from ECR repository using all props', () => { // GIVEN const stack = new cdk.Stack(); @@ -423,11 +422,11 @@ nodeunitShim({ }], }); - test.done(); - }, - }, - 'correctly sets containers from ECR repository using an image tag'(test: Test) { + }); + }); + + test('correctly sets containers from ECR repository using an image tag', () => { // GIVEN const stack = new cdk.Stack(); @@ -498,10 +497,10 @@ nodeunitShim({ }], }); - test.done(); - }, - 'correctly sets containers from ECR repository using an image digest'(test: Test) { + }); + + test('correctly sets containers from ECR repository using an image digest', () => { // GIVEN const stack = new cdk.Stack(); const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); @@ -571,10 +570,10 @@ nodeunitShim({ }], }); - test.done(); - }, - 'correctly sets containers from ECR repository using default props'(test: Test) { + }); + + test('correctly sets containers from ECR repository using default props', () => { // GIVEN const stack = new cdk.Stack(); const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); @@ -588,10 +587,10 @@ nodeunitShim({ // THEN expect(stack).toHaveResource('AWS::ECR::Repository', {}); - test.done(); - }, - 'warns when setting containers from ECR repository using fromRegistry method'(test: Test) { + }); + + test('warns when setting containers from ECR repository using fromRegistry method', () => { // GIVEN const stack = new cdk.Stack(); @@ -605,10 +604,10 @@ nodeunitShim({ // THEN expect(container.node.metadata[0].data).toEqual("Proper policies need to be attached before pulling from ECR repository, or use 'fromEcrRepository'."); - test.done(); - }, - 'correctly sets volumes from'(test: Test) { + }); + + test('correctly sets volumes from', () => { const stack = new cdk.Stack(); const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef', {}); @@ -620,10 +619,10 @@ nodeunitShim({ name: 'scratch', })).toThrow('External task definitions doesnt support volumes' ); - test.done(); - }, - 'error when interferenceAccelerators set'(test: Test) { + }); + + test('error when interferenceAccelerators set', () => { const stack = new cdk.Stack(); const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef', {}); @@ -633,6 +632,6 @@ nodeunitShim({ deviceType: 'eia2.medium', })).toThrow('Cannot use inference accelerators on tasks that run on External service'); - test.done(); - }, + + }); }); \ No newline at end of file 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 f7c33eb7b8af9..ef4ceae4ccedc 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 @@ -1,4 +1,5 @@ -import { expect, haveResource, haveResourceLike, ABSENT } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { ABSENT, SynthUtils } from '@aws-cdk/assert-internal'; import * as appscaling from '@aws-cdk/aws-applicationautoscaling'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; @@ -9,13 +10,12 @@ import * as s3 from '@aws-cdk/aws-s3'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../../lib'; import { DeploymentControllerType, LaunchType, PropagatedTagSource } from '../../lib/base/base-service'; -nodeunitShim({ - 'When creating a Fargate Service': { - 'with only required properties set, it correctly sets default properties'(test: Test) { +describe('fargate service', () => { + describe('When creating a Fargate Service', () => { + test('with only required properties set, it correctly sets default properties', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -32,7 +32,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { TaskDefinition: { Ref: 'FargateTaskDefC6FB60B4', }, @@ -66,9 +66,9 @@ nodeunitShim({ ], }, }, - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/FargateService/SecurityGroup', SecurityGroupEgress: [ { @@ -80,14 +80,14 @@ nodeunitShim({ VpcId: { Ref: 'MyVpcF9F0CA6F', }, - })); + }); + + expect(service.node.defaultChild).toBeDefined(); - test.notEqual(service.node.defaultChild, undefined); - test.done(); - }, + }); - 'can create service with default settings if VPC only has public subnets'(test: Test) { + test('can create service with default settings if VPC only has public subnets', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', { @@ -112,10 +112,10 @@ nodeunitShim({ }); // THEN -- did not throw - test.done(); - }, - 'does not set launchType when capacity provider strategies specified (deprecated)'(test: Test) { + }); + + test('does not set launchType when capacity provider strategies specified (deprecated)', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -147,15 +147,15 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Cluster', { + expect(stack).toHaveResource('AWS::ECS::Cluster', { CapacityProviders: ABSENT, - })); + }); - expect(stack).to(haveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + expect(stack).toHaveResource('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE', 'FARGATE_SPOT'], - })); + }); - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { TaskDefinition: { Ref: 'FargateTaskDefC6FB60B4', }, @@ -199,12 +199,12 @@ nodeunitShim({ ], }, }, - })); + }); - test.done(); - }, - 'does not set launchType when capacity provider strategies specified'(test: Test) { + }); + + test('does not set launchType when capacity provider strategies specified', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -237,15 +237,15 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Cluster', { + expect(stack).toHaveResource('AWS::ECS::Cluster', { CapacityProviders: ABSENT, - })); + }); - expect(stack).to(haveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + expect(stack).toHaveResource('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE', 'FARGATE_SPOT'], - })); + }); - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { TaskDefinition: { Ref: 'FargateTaskDefC6FB60B4', }, @@ -290,12 +290,12 @@ nodeunitShim({ ], }, }, - })); + }); + - test.done(); - }, + }); - 'with custom cloudmap namespace'(test: Test) { + test('with custom cloudmap namespace', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -324,7 +324,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ServiceDiscovery::Service', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -350,19 +350,19 @@ nodeunitShim({ 'Id', ], }, - })); + }); - expect(stack).to(haveResource('AWS::ServiceDiscovery::PrivateDnsNamespace', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::PrivateDnsNamespace', { Name: 'scorekeep.com', Vpc: { Ref: 'MyVpcF9F0CA6F', }, - })); + }); - test.done(); - }, - 'with user-provided cloudmap service'(test: Test) { + }); + + test('with user-provided cloudmap service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -399,7 +399,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { ServiceRegistries: [ { ContainerName: 'web', @@ -407,12 +407,12 @@ nodeunitShim({ RegistryArn: { 'Fn::GetAtt': ['ServiceDBC79909', 'Arn'] }, }, ], - })); + }); - test.done(); - }, - 'errors when more than one service registry used'(test: Test) { + }); + + test('errors when more than one service registry used', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -446,18 +446,18 @@ nodeunitShim({ }); // WHEN / THEN - test.throws(() => { + expect(() => { ecsService.associateCloudMapService({ service: cloudMapService, container: container, containerPort: 8000, }); - }, /at most one service registry/i); + }).toThrow(/at most one service registry/i); - test.done(); - }, - 'with all properties set'(test: Test) { + }); + + test('with all properties set', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -503,9 +503,9 @@ nodeunitShim({ }); // THEN - test.ok(svc.cloudMapService !== undefined); + expect(svc.cloudMapService).toBeDefined(); - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { TaskDefinition: { Ref: 'FargateTaskDefC6FB60B4', }, @@ -558,12 +558,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'throws when task definition is not Fargate compatible'(test: Test) { + }); + + test('throws when task definition is not Fargate compatible', () => { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); @@ -576,17 +576,17 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.FargateService(stack, 'FargateService', { cluster, taskDefinition, }); - }, /Supplied TaskDefinition is not configured for compatibility with Fargate/); + }).toThrow(/Supplied TaskDefinition is not configured for compatibility with Fargate/); - test.done(); - }, - 'throws whith secret json field on unsupported platform version'(test: Test) { + }); + + test('throws whith secret json field on unsupported platform version', () => { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); @@ -600,18 +600,18 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.FargateService(stack, 'FargateService', { cluster, taskDefinition, platformVersion: ecs.FargatePlatformVersion.VERSION1_3, }); - }, new RegExp(`uses at least one container that references a secret JSON field.+platform version ${ecs.FargatePlatformVersion.VERSION1_4} or later`)); + }).toThrow(new RegExp(`uses at least one container that references a secret JSON field.+platform version ${ecs.FargatePlatformVersion.VERSION1_4} or later`)); + - test.done(); - }, + }); - 'ignore task definition and launch type if deployment controller is set to be EXTERNAL'(test: Test) { + test('ignore task definition and launch type if deployment controller is set to be EXTERNAL', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -631,8 +631,8 @@ nodeunitShim({ }); // THEN - test.deepEqual(service.node.metadata[0].data, 'taskDefinition and launchType are blanked out when using external deployment controller.'); - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(service.node.metadata[0].data).toEqual('taskDefinition and launchType are blanked out when using external deployment controller.'); + expect(stack).toHaveResource('AWS::ECS::Service', { Cluster: { Ref: 'EcsCluster97242B84', }, @@ -665,12 +665,12 @@ nodeunitShim({ ], }, }, - })); + }); + - test.done(); - }, + }); - 'errors when no container specified on task definition'(test: Test) { + test('errors when no container specified on task definition', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -684,14 +684,14 @@ nodeunitShim({ }); // THEN - test.throws(() => { - expect(stack); - }, /one essential container/); + expect(() => { + SynthUtils.synthesize(stack); + }).toThrow(/one essential container/); + - test.done(); - }, + }); - 'allows adding the default container after creating the service'(test: Test) { + test('allows adding the default container after creating the service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -709,18 +709,18 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Name: 'main', }, ], - })); + }); + - test.done(); - }, + }); - 'allows specifying assignPublicIP as enabled'(test: Test) { + test('allows specifying assignPublicIP as enabled', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -738,18 +738,18 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::Service', { + expect(stack).toHaveResourceLike('AWS::ECS::Service', { NetworkConfiguration: { AwsvpcConfiguration: { AssignPublicIp: 'ENABLED', }, }, - })); + }); + - test.done(); - }, + }); - 'allows specifying 0 for minimumHealthyPercent'(test: Test) { + test('allows specifying 0 for minimumHealthyPercent', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -767,16 +767,16 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::Service', { + expect(stack).toHaveResourceLike('AWS::ECS::Service', { DeploymentConfiguration: { MinimumHealthyPercent: 0, }, - })); + }); - test.done(); - }, - 'throws when securityGroup and securityGroups are supplied'(test: Test) { + }); + + test('throws when securityGroup and securityGroups are supplied', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -800,19 +800,19 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.FargateService(stack, 'FargateService', { cluster, taskDefinition, securityGroup: securityGroup1, securityGroups: [securityGroup2], }); - }, /Only one of SecurityGroup or SecurityGroups can be populated./); + }).toThrow(/Only one of SecurityGroup or SecurityGroups can be populated./); + - test.done(); - }, + }); - 'with multiple securty groups, it correctly updates cloudformation template'(test: Test) { + test('with multiple securty groups, it correctly updates cloudformation template', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -842,7 +842,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { TaskDefinition: { Ref: 'FargateTaskDefC6FB60B4', }, @@ -882,9 +882,9 @@ nodeunitShim({ ], }, }, - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { GroupDescription: 'Example', GroupName: 'Bingo', SecurityGroupEgress: [ @@ -897,9 +897,9 @@ nodeunitShim({ VpcId: { Ref: 'MyVpcF9F0CA6F', }, - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { GroupDescription: 'Example', GroupName: 'Rolly', SecurityGroupEgress: [ @@ -914,15 +914,15 @@ nodeunitShim({ VpcId: { Ref: 'MyVpcF9F0CA6F', }, - })); + }); - test.done(); - }, - }, + }); + + }); - 'When setting up a health check': { - 'grace period is respected'(test: Test) { + describe('When setting up a health check', () => { + test('grace period is respected', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -940,16 +940,16 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { HealthCheckGracePeriodSeconds: 10, - })); + }); - test.done(); - }, - }, - 'When adding an app load balancer': { - 'allows auto scaling by ALB request per target'(test: Test) { + }); + }); + + describe('When adding an app load balancer', () => { + test('allows auto scaling by ALB request per target', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -976,7 +976,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { MaxCapacity: 10, MinCapacity: 1, ResourceId: { @@ -997,9 +997,9 @@ nodeunitShim({ ], ], }, - })); + }); - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { TargetTrackingScalingPolicyConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'ALBRequestCountPerTarget', @@ -1014,18 +1014,18 @@ nodeunitShim({ }, TargetValue: 1000, }, - })); + }); - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { // if any load balancer is configured and healthCheckGracePeriodSeconds is not // set, then it should default to 60 seconds. HealthCheckGracePeriodSeconds: 60, - })); + }); - test.done(); - }, - 'allows auto scaling by ALB with new service arn format'(test: Test) { + }); + + test('allows auto scaling by ALB with new service arn format', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1056,7 +1056,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { MaxCapacity: 10, MinCapacity: 1, ResourceId: { @@ -1077,13 +1077,13 @@ nodeunitShim({ ], ], }, - })); + }); - test.done(); - }, - 'allows specify any existing container name and port in a service': { - 'with default setting'(test: Test) { + }); + + describe('allows specify any existing container name and port in a service', () => { + test('with default setting', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1111,7 +1111,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'MainContainer', @@ -1121,24 +1121,24 @@ nodeunitShim({ }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { Description: 'Load balancer to target', FromPort: 8000, ToPort: 8000, - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { Description: 'Load balancer to target', FromPort: 8000, ToPort: 8000, - })); + }); - test.done(); - }, - 'with TCP protocol'(test: Test) { + }); + + test('with TCP protocol', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1169,10 +1169,10 @@ nodeunitShim({ })], }); - test.done(); - }, - 'with UDP protocol'(test: Test) { + }); + + test('with UDP protocol', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1203,10 +1203,10 @@ nodeunitShim({ })], }); - test.done(); - }, - 'throws when protocol does not match'(test: Test) { + }); + + test('throws when protocol does not match', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1228,7 +1228,7 @@ nodeunitShim({ const listener = lb.addListener('listener', { port: 80 }); // THEN - test.throws(() => { + expect(() => { listener.addTargets('target', { port: 80, targets: [service.loadBalancerTarget({ @@ -1237,12 +1237,12 @@ nodeunitShim({ protocol: ecs.Protocol.TCP, })], }); - }, /Container 'Default\/FargateTaskDef\/MainContainer' has no mapping for port 8001 and protocol tcp. Did you call "container.addPortMappings\(\)"\?/); + }).toThrow(/Container 'Default\/FargateTaskDef\/MainContainer' has no mapping for port 8001 and protocol tcp. Did you call "container.addPortMappings\(\)"\?/); - test.done(); - }, - 'throws when port does not match'(test: Test) { + }); + + test('throws when port does not match', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1264,7 +1264,7 @@ nodeunitShim({ const listener = lb.addListener('listener', { port: 80 }); // THEN - test.throws(() => { + expect(() => { listener.addTargets('target', { port: 80, targets: [service.loadBalancerTarget({ @@ -1272,12 +1272,12 @@ nodeunitShim({ containerPort: 8002, })], }); - }, /Container 'Default\/FargateTaskDef\/MainContainer' has no mapping for port 8002 and protocol tcp. Did you call "container.addPortMappings\(\)"\?/); + }).toThrow(/Container 'Default\/FargateTaskDef\/MainContainer' has no mapping for port 8002 and protocol tcp. Did you call "container.addPortMappings\(\)"\?/); - test.done(); - }, - 'throws when container does not exist'(test: Test) { + }); + + test('throws when container does not exist', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1299,7 +1299,7 @@ nodeunitShim({ const listener = lb.addListener('listener', { port: 80 }); // THEN - test.throws(() => { + expect(() => { listener.addTargets('target', { port: 80, targets: [service.loadBalancerTarget({ @@ -1307,15 +1307,15 @@ nodeunitShim({ containerPort: 8001, })], }); - }, /No container named 'SideContainer'. Did you call "addContainer()"?/); + }).toThrow(/No container named 'SideContainer'. Did you call "addContainer()"?/); - test.done(); - }, - }, - 'allows load balancing to any container and port of service': { - 'with application load balancers': { - 'with default target group port and protocol'(test: Test) { + }); + }); + + describe('allows load balancing to any container and port of service', () => { + describe('with application load balancers', () => { + test('with default target group port and protocol', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1345,7 +1345,7 @@ nodeunitShim({ ); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'MainContainer', @@ -1355,17 +1355,17 @@ nodeunitShim({ }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 80, Protocol: 'HTTP', - })); + }); - test.done(); - }, - 'with default target group port and HTTP protocol'(test: Test) { + }); + + test('with default target group port and HTTP protocol', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1397,7 +1397,7 @@ nodeunitShim({ ); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'MainContainer', @@ -1407,17 +1407,17 @@ nodeunitShim({ }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 80, Protocol: 'HTTP', - })); + }); - test.done(); - }, - 'with default target group port and HTTPS protocol'(test: Test) { + }); + + test('with default target group port and HTTPS protocol', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1449,7 +1449,7 @@ nodeunitShim({ ); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'MainContainer', @@ -1459,17 +1459,17 @@ nodeunitShim({ }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 443, Protocol: 'HTTPS', - })); + }); - test.done(); - }, - 'with any target group port and protocol'(test: Test) { + }); + + test('with any target group port and protocol', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1502,7 +1502,7 @@ nodeunitShim({ ); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'MainContainer', @@ -1512,19 +1512,19 @@ nodeunitShim({ }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 83, Protocol: 'HTTP', - })); + }); - test.done(); - }, - }, - 'with network load balancers': { - 'with default target group port'(test: Test) { + }); + }); + + describe('with network load balancers', () => { + test('with default target group port', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1554,7 +1554,7 @@ nodeunitShim({ ); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'MainContainer', @@ -1564,17 +1564,17 @@ nodeunitShim({ }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 80, Protocol: 'TCP', - })); + }); - test.done(); - }, - 'with any target group port'(test: Test) { + }); + + test('with any target group port', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1606,7 +1606,7 @@ nodeunitShim({ ); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'MainContainer', @@ -1616,20 +1616,20 @@ nodeunitShim({ }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 81, Protocol: 'TCP', - })); + }); - test.done(); - }, - }, - }, - }, - 'allows scaling on a specified scheduled time'(test: Test) { + }); + }); + }); + }); + + test('allows scaling on a specified scheduled time', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1653,7 +1653,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { ScheduledActions: [ { ScalableTargetAction: { @@ -1663,12 +1663,12 @@ nodeunitShim({ ScheduledActionName: 'ScaleOnSchedule', }, ], - })); + }); - test.done(); - }, - 'allows scaling on a specified metric value'(test: Test) { + }); + + test('allows scaling on a specified metric value', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1696,7 +1696,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'StepScaling', ScalingTargetId: { Ref: 'ServiceTaskCountTarget23E25614', @@ -1711,12 +1711,12 @@ nodeunitShim({ }, ], }, - })); + }); + - test.done(); - }, + }); - 'allows scaling on a target CPU utilization'(test: Test) { + test('allows scaling on a target CPU utilization', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1739,18 +1739,18 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'ECSServiceAverageCPUUtilization' }, TargetValue: 30, }, - })); + }); + - test.done(); - }, + }); - 'allows scaling on memory utilization'(test: Test) { + test('allows scaling on memory utilization', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1773,18 +1773,18 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'ECSServiceAverageMemoryUtilization' }, TargetValue: 30, }, - })); + }); - test.done(); - }, - 'allows scaling on custom CloudWatch metric'(test: Test) { + }); + + test('allows scaling on custom CloudWatch metric', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1808,7 +1808,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { CustomizedMetricSpecification: { @@ -1818,13 +1818,13 @@ nodeunitShim({ }, TargetValue: 5, }, - })); + }); + - test.done(); - }, + }); - 'When enabling service discovery': { - 'throws if namespace has not been added to cluster'(test: Test) { + describe('When enabling service discovery', () => { + test('throws if namespace has not been added to cluster', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1837,7 +1837,7 @@ nodeunitShim({ container.addPortMappings({ containerPort: 8000 }); // THEN - test.throws(() => { + expect(() => { new ecs.FargateService(stack, 'Service', { cluster, taskDefinition, @@ -1845,12 +1845,12 @@ nodeunitShim({ name: 'myApp', }, }); - }, /Cannot enable service discovery if a Cloudmap Namespace has not been created in the cluster./); + }).toThrow(/Cannot enable service discovery if a Cloudmap Namespace has not been created in the cluster./); - test.done(); - }, - 'creates cloud map service for Private DNS namespace'(test: Test) { + }); + + test('creates cloud map service for Private DNS namespace', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1876,7 +1876,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ServiceDiscovery::Service', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -1902,12 +1902,12 @@ nodeunitShim({ 'Id', ], }, - })); + }); + - test.done(); - }, + }); - 'creates AWS Cloud Map service for Private DNS namespace with SRV records with proper defaults'(test: Test) { + test('creates AWS Cloud Map service for Private DNS namespace with SRV records with proper defaults', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1937,7 +1937,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ServiceDiscovery::Service', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -1963,12 +1963,12 @@ nodeunitShim({ 'Id', ], }, - })); + }); + - test.done(); - }, + }); - 'creates AWS Cloud Map service for Private DNS namespace with SRV records with overriden defaults'(test: Test) { + test('creates AWS Cloud Map service for Private DNS namespace with SRV records with overriden defaults', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1999,7 +1999,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ServiceDiscovery::Service', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -2025,12 +2025,12 @@ nodeunitShim({ 'Id', ], }, - })); + }); + - test.done(); - }, + }); - 'user can select any container and port'(test: Test) { + test('user can select any container and port', () => { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); @@ -2063,7 +2063,7 @@ nodeunitShim({ }, }); - expect(stack).to(haveResourceLike('AWS::ECS::Service', { + expect(stack).toHaveResourceLike('AWS::ECS::Service', { ServiceRegistries: [ { RegistryArn: { 'Fn::GetAtt': ['ServiceCloudmapService046058A4', 'Arn'] }, @@ -2071,13 +2071,13 @@ nodeunitShim({ ContainerPort: 8001, }, ], - })); + }); + - test.done(); - }, - }, + }); + }); - 'Metric'(test: Test) { + test('Metric', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2094,7 +2094,7 @@ nodeunitShim({ }); // THEN - test.deepEqual(stack.resolve(service.metricCpuUtilization()), { + expect(stack.resolve(service.metricCpuUtilization())).toEqual({ dimensions: { ClusterName: { Ref: 'EcsCluster97242B84' }, ServiceName: { 'Fn::GetAtt': ['ServiceD69D759B', 'Name'] }, @@ -2105,11 +2105,11 @@ nodeunitShim({ statistic: 'Average', }); - test.done(); - }, - 'When import a Fargate Service': { - 'with serviceArn'(test: Test) { + }); + + describe('When import a Fargate Service', () => { + test('with serviceArn', () => { // GIVEN const stack = new cdk.Stack(); const cluster = new ecs.Cluster(stack, 'EcsCluster'); @@ -2121,13 +2121,13 @@ nodeunitShim({ }); // THEN - test.equal(service.serviceArn, 'arn:aws:ecs:us-west-2:123456789012:service/my-http-service'); - test.equal(service.serviceName, 'my-http-service'); + expect(service.serviceArn).toEqual('arn:aws:ecs:us-west-2:123456789012:service/my-http-service'); + expect(service.serviceName).toEqual('my-http-service'); + - test.done(); - }, + }); - 'with serviceName'(test: Test) { + test('with serviceName', () => { // GIVEN const stack = new cdk.Stack(); const pseudo = new cdk.ScopedAws(stack); @@ -2140,13 +2140,13 @@ nodeunitShim({ }); // THEN - test.deepEqual(stack.resolve(service.serviceArn), stack.resolve(`arn:${pseudo.partition}:ecs:${pseudo.region}:${pseudo.accountId}:service/my-http-service`)); - test.equal(service.serviceName, 'my-http-service'); + expect(stack.resolve(service.serviceArn)).toEqual(stack.resolve(`arn:${pseudo.partition}:ecs:${pseudo.region}:${pseudo.accountId}:service/my-http-service`)); + expect(service.serviceName).toEqual('my-http-service'); + - test.done(); - }, + }); - 'with circuit breaker'(test: Test) { + test('with circuit breaker', () => { // GIVEN const stack = new cdk.Stack(); const cluster = new ecs.Cluster(stack, 'EcsCluster'); @@ -2164,7 +2164,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { DeploymentConfiguration: { MaximumPercent: 200, MinimumHealthyPercent: 50, @@ -2176,42 +2176,42 @@ nodeunitShim({ DeploymentController: { Type: ecs.DeploymentControllerType.ECS, }, - })); + }); + - test.done(); - }, + }); - 'throws an exception if both serviceArn and serviceName were provided for fromEc2ServiceAttributes'(test: Test) { + test('throws an exception if both serviceArn and serviceName were provided for fromEc2ServiceAttributes', () => { // GIVEN const stack = new cdk.Stack(); const cluster = new ecs.Cluster(stack, 'EcsCluster'); - test.throws(() => { + expect(() => { ecs.FargateService.fromFargateServiceAttributes(stack, 'EcsService', { serviceArn: 'arn:aws:ecs:us-west-2:123456789012:service/my-http-service', serviceName: 'my-http-service', cluster, }); - }, /only specify either serviceArn or serviceName/); + }).toThrow(/only specify either serviceArn or serviceName/); - test.done(); - }, - 'throws an exception if neither serviceArn nor serviceName were provided for fromEc2ServiceAttributes'(test: Test) { + }); + + test('throws an exception if neither serviceArn nor serviceName were provided for fromEc2ServiceAttributes', () => { // GIVEN const stack = new cdk.Stack(); const cluster = new ecs.Cluster(stack, 'EcsCluster'); - test.throws(() => { + expect(() => { ecs.FargateService.fromFargateServiceAttributes(stack, 'EcsService', { cluster, }); - }, /only specify either serviceArn or serviceName/); + }).toThrow(/only specify either serviceArn or serviceName/); - test.done(); - }, - 'allows setting enable execute command'(test: Test) { + }); + + test('allows setting enable execute command', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2229,7 +2229,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { TaskDefinition: { Ref: 'FargateTaskDefC6FB60B4', }, @@ -2264,9 +2264,9 @@ nodeunitShim({ ], }, }, - })); + }); - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2302,12 +2302,12 @@ nodeunitShim({ Ref: 'FargateTaskDefTaskRole0B257552', }, ], - })); + }); + - test.done(); - }, + }); - 'no logging enabled when logging field is set to NONE'(test: Test) { + test('no logging enabled when logging field is set to NONE', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2340,7 +2340,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2362,12 +2362,12 @@ nodeunitShim({ Ref: 'FargateTaskDefTaskRole0B257552', }, ], - })); + }); + - test.done(); - }, + }); - 'enables execute command logging with logging field set to OVERRIDE'(test: Test) { + test('enables execute command logging with logging field set to OVERRIDE', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2403,7 +2403,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2479,12 +2479,12 @@ nodeunitShim({ Ref: 'FargateTaskDefTaskRole0B257552', }, ], - })); + }); + - test.done(); - }, + }); - 'enables only execute command session encryption'(test: Test) { + test('enables only execute command session encryption', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2523,7 +2523,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2612,9 +2612,9 @@ nodeunitShim({ Ref: 'FargateTaskDefTaskRole0B257552', }, ], - })); + }); - expect(stack).to(haveResource('AWS::KMS::Key', { + expect(stack).toHaveResource('AWS::KMS::Key', { KeyPolicy: { Statement: [ { @@ -2678,12 +2678,12 @@ nodeunitShim({ ], Version: '2012-10-17', }, - })); + }); - test.done(); - }, - 'enables encryption for execute command logging'(test: Test) { + }); + + test('enables encryption for execute command logging', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2729,7 +2729,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2833,9 +2833,9 @@ nodeunitShim({ Ref: 'FargateTaskDefTaskRole0B257552', }, ], - })); + }); - expect(stack).to(haveResource('AWS::KMS::Key', { + expect(stack).toHaveResource('AWS::KMS::Key', { KeyPolicy: { Statement: [ { @@ -2944,12 +2944,12 @@ nodeunitShim({ ], Version: '2012-10-17', }, - })); + }); + - test.done(); - }, + }); - 'with both propagateTags and propagateTaskTagsFrom defined'(test: Test) { + test('with both propagateTags and propagateTaskTagsFrom defined', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2962,15 +2962,15 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.FargateService(stack, 'FargateService', { cluster, taskDefinition, propagateTags: PropagatedTagSource.SERVICE, propagateTaskTagsFrom: PropagatedTagSource.SERVICE, }); - }, /You can only specify either propagateTags or propagateTaskTagsFrom. Alternatively, you can leave both blank/); - test.done(); - }, - }, + }).toThrow(/You can only specify either propagateTags or propagateTaskTagsFrom. Alternatively, you can leave both blank/); + + }); + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-task-definition.test.ts b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-task-definition.test.ts index 294ae3001aa64..3ff15ac22057e 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-task-definition.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-task-definition.test.ts @@ -1,29 +1,28 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../../lib'; -nodeunitShim({ - 'When creating a Fargate TaskDefinition': { - 'with only required properties set, it correctly sets default properties'(test: Test) { +describe('fargate task definition', () => { + describe('When creating a Fargate TaskDefinition', () => { + test('with only required properties set, it correctly sets default properties', () => { // GIVEN const stack = new cdk.Stack(); new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { Family: 'FargateTaskDef', NetworkMode: ecs.NetworkMode.AWS_VPC, RequiresCompatibilities: ['FARGATE'], Cpu: '256', Memory: '512', - })); + }); + - test.done(); - }, + }); - 'support lazy cpu and memory values'(test: Test) { + test('support lazy cpu and memory values', () => { // GIVEN const stack = new cdk.Stack(); @@ -33,15 +32,15 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { Cpu: '128', Memory: '1024', - })); + }); - test.done(); - }, - 'with all properties set'(test: Test) { + }); + + test('with all properties set', () => { // GIVEN const stack = new cdk.Stack(); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef', { @@ -69,7 +68,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { Cpu: '128', ExecutionRoleArn: { 'Fn::GetAtt': [ @@ -100,25 +99,25 @@ nodeunitShim({ Name: 'scratch', }, ], - })); + }); + - test.done(); - }, + }); - 'throws when adding placement constraint'(test: Test) { + test('throws when adding placement constraint', () => { // GIVEN const stack = new cdk.Stack(); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); // THEN - test.throws(() => { + expect(() => { taskDefinition.addPlacementConstraint(ecs.PlacementConstraint.memberOf('attribute:ecs.instance-type =~ t2.*')); - }, /Cannot set placement constraints on tasks that run on Fargate/); + }).toThrow(/Cannot set placement constraints on tasks that run on Fargate/); - test.done(); - }, - 'throws when adding inference accelerators'(test: Test) { + }); + + test('throws when adding inference accelerators', () => { // GIVEN const stack = new cdk.Stack(); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); @@ -129,42 +128,40 @@ nodeunitShim({ }; // THEN - test.throws(() => { + expect(() => { taskDefinition.addInferenceAccelerator(inferenceAccelerator); - }, /Cannot use inference accelerators on tasks that run on Fargate/); + }).toThrow(/Cannot use inference accelerators on tasks that run on Fargate/); + - test.done(); - }, + }); - 'throws when ephemeral storage request is too high'(test: Test) { + test('throws when ephemeral storage request is too high', () => { // GIVEN const stack = new cdk.Stack(); - test.throws(() => { + expect(() => { new ecs.FargateTaskDefinition(stack, 'FargateTaskDef', { ephemeralStorageGiB: 201, }); - }, /Ephemeral storage size must be between 21GiB and 200GiB/); + }).toThrow(/Ephemeral storage size must be between 21GiB and 200GiB/); // THEN - test.done(); - }, + }); - 'throws when ephemeral storage request is too low'(test: Test) { + test('throws when ephemeral storage request is too low', () => { // GIVEN const stack = new cdk.Stack(); - test.throws(() => { + expect(() => { new ecs.FargateTaskDefinition(stack, 'FargateTaskDef', { ephemeralStorageGiB: 20, }); - }, /Ephemeral storage size must be between 21GiB and 200GiB/); + }).toThrow(/Ephemeral storage size must be between 21GiB and 200GiB/); // THEN - test.done(); - }, - }, + }); + }); - 'When importing from an existing Fargate TaskDefinition': { - 'can succeed using TaskDefinition Arn'(test: Test) { + describe('When importing from an existing Fargate TaskDefinition', () => { + test('can succeed using TaskDefinition Arn', () => { // GIVEN const stack = new cdk.Stack(); const expectTaskDefinitionArn = 'TD_ARN'; @@ -173,11 +170,11 @@ nodeunitShim({ const taskDefinition = ecs.FargateTaskDefinition.fromFargateTaskDefinitionArn(stack, 'FARGATE_TD_ID', expectTaskDefinitionArn); // THEN - test.equal(taskDefinition.taskDefinitionArn, expectTaskDefinitionArn); - test.done(); - }, + expect(taskDefinition.taskDefinitionArn).toEqual(expectTaskDefinitionArn); + + }); - 'can succeed using attributes'(test: Test) { + test('can succeed using attributes', () => { // GIVEN const stack = new cdk.Stack(); const expectTaskDefinitionArn = 'TD_ARN'; @@ -194,17 +191,17 @@ nodeunitShim({ }); // THEN - test.equal(taskDefinition.taskDefinitionArn, expectTaskDefinitionArn); - test.equal(taskDefinition.compatibility, ecs.Compatibility.FARGATE); - test.ok(taskDefinition.isFargateCompatible); - test.equal(taskDefinition.isEc2Compatible, false); - test.equal(taskDefinition.networkMode, expectNetworkMode); - test.equal(taskDefinition.taskRole, expectTaskRole); + expect(taskDefinition.taskDefinitionArn).toEqual(expectTaskDefinitionArn); + expect(taskDefinition.compatibility).toEqual(ecs.Compatibility.FARGATE); + expect(taskDefinition.isFargateCompatible).toEqual(true); + expect(taskDefinition.isEc2Compatible).toEqual(false); + expect(taskDefinition.networkMode).toEqual(expectNetworkMode); + expect(taskDefinition.taskRole).toEqual(expectTaskRole); - test.done(); - }, - 'returns a Fargate TaskDefinition that will throw an error when trying to access its networkMode but its networkMode is undefined'(test: Test) { + }); + + test('returns a Fargate TaskDefinition that will throw an error when trying to access its networkMode but its networkMode is undefined', () => { // GIVEN const stack = new cdk.Stack(); const expectTaskDefinitionArn = 'TD_ARN'; @@ -219,15 +216,15 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { taskDefinition.networkMode; - }, 'This operation requires the networkMode in ImportedTaskDefinition to be defined. ' + + }).toThrow('This operation requires the networkMode in ImportedTaskDefinition to be defined. ' + 'Add the \'networkMode\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); - test.done(); - }, - 'returns a Fargate TaskDefinition that will throw an error when trying to access its taskRole but its taskRole is undefined'(test: Test) { + }); + + test('returns a Fargate TaskDefinition that will throw an error when trying to access its taskRole but its taskRole is undefined', () => { // GIVEN const stack = new cdk.Stack(); const expectTaskDefinitionArn = 'TD_ARN'; @@ -240,12 +237,12 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { taskDefinition.taskRole; - }, 'This operation requires the taskRole in ImportedTaskDefinition to be defined. ' + + }).toThrow('This operation requires the taskRole in ImportedTaskDefinition to be defined. ' + 'Add the \'taskRole\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); - test.done(); - }, - }, + + }); + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/firelens-log-driver.test.ts b/packages/@aws-cdk/aws-ecs/test/firelens-log-driver.test.ts index aff395c8598d2..6ab96b05df364 100644 --- a/packages/@aws-cdk/aws-ecs/test/firelens-log-driver.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/firelens-log-driver.test.ts @@ -1,22 +1,21 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import * as ssm from '@aws-cdk/aws-ssm'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../lib'; let stack: cdk.Stack; let td: ecs.TaskDefinition; const image = ecs.ContainerImage.fromRegistry('test-image'); -nodeunitShim({ - 'setUp'(cb: () => void) { +describe('firelens log driver', () => { + beforeEach(() => { stack = new cdk.Stack(); td = new ecs.Ec2TaskDefinition(stack, 'TaskDefinition'); - cb(); - }, - 'create a firelens log driver with default options'(test: Test) { + + }); + test('create a firelens log driver with default options', () => { // WHEN td.addContainer('Container', { image, @@ -25,7 +24,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -39,12 +38,12 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); - 'create a firelens log driver with secret options'(test: Test) { + test('create a firelens log driver with secret options', () => { const secret = new secretsmanager.Secret(stack, 'Secret'); const parameter = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'Parameter', { parameterName: '/host', @@ -72,7 +71,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -125,9 +124,9 @@ nodeunitShim({ }, }, ], - })); + }); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -172,12 +171,12 @@ nodeunitShim({ ], Version: '2012-10-17', }, - })); + }); - test.done(); - }, - 'create a firelens log driver to route logs to CloudWatch Logs with Fluent Bit'(test: Test) { + }); + + test('create a firelens log driver to route logs to CloudWatch Logs with Fluent Bit', () => { // WHEN td.addContainer('Container', { image, @@ -194,7 +193,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -215,12 +214,12 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); - 'create a firelens log driver to route logs to kinesis firehose Logs with Fluent Bit'(test: Test) { + test('create a firelens log driver to route logs to kinesis firehose Logs with Fluent Bit', () => { // WHEN td.addContainer('Container', { image, @@ -235,7 +234,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -254,13 +253,13 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); - 'Firelens Configuration': { - 'fluentd log router container'(test: Test) { + describe('Firelens Configuration', () => { + test('fluentd log router container', () => { // GIVEN td.addFirelensLogRouter('log_router', { image: ecs.ContainerImage.fromRegistry('fluent/fluentd'), @@ -271,7 +270,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Essential: true, @@ -283,11 +282,11 @@ nodeunitShim({ }, }, ], - })); - test.done(); - }, + }); + + }); - 'fluent-bit log router container with options'(test: Test) { + test('fluent-bit log router container with options', () => { // GIVEN const stack2 = new cdk.Stack(undefined, 'Stack2', { env: { region: 'us-east-1' } }); const td2 = new ecs.Ec2TaskDefinition(stack2, 'TaskDefinition'); @@ -305,7 +304,7 @@ nodeunitShim({ }); // THEN - expect(stack2).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack2).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Essential: true, @@ -321,12 +320,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'fluent-bit log router with file config type'(test: Test) { + }); + + test('fluent-bit log router with file config type', () => { // GIVEN td.addFirelensLogRouter('log_router', { image: ecs.obtainDefaultFluentBitECRImage(td, undefined, '2.1.0'), @@ -343,7 +342,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Essential: true, @@ -359,9 +358,9 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - }, + + }); + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/fluentd-log-driver.test.ts b/packages/@aws-cdk/aws-ecs/test/fluentd-log-driver.test.ts index 54ac824fb6c47..81c17b8c0b76f 100644 --- a/packages/@aws-cdk/aws-ecs/test/fluentd-log-driver.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/fluentd-log-driver.test.ts @@ -1,21 +1,20 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../lib'; let stack: cdk.Stack; let td: ecs.TaskDefinition; const image = ecs.ContainerImage.fromRegistry('test-image'); -nodeunitShim({ - 'setUp'(cb: () => void) { +describe('fluentd log driver', () => { + beforeEach(() => { stack = new cdk.Stack(); td = new ecs.Ec2TaskDefinition(stack, 'TaskDefinition'); - cb(); - }, - 'create a fluentd log driver with options'(test: Test) { + }); + + test('create a fluentd log driver with options', () => { // WHEN td.addContainer('Container', { image, @@ -26,7 +25,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -37,12 +36,12 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); - 'create a fluentd log driver without options'(test: Test) { + test('create a fluentd log driver without options', () => { // WHEN td.addContainer('Container', { image, @@ -51,7 +50,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -59,12 +58,12 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); - 'create a fluentd log driver with all possible options'(test: Test) { + test('create a fluentd log driver with all possible options', () => { // WHEN td.addContainer('Container', { image, @@ -92,7 +91,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -112,12 +111,12 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); - 'create a fluentd log driver using fluentd'(test: Test) { + test('create a fluentd log driver using fluentd', () => { // WHEN td.addContainer('Container', { image, @@ -126,7 +125,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -134,8 +133,8 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/gelf-log-driver.test.ts b/packages/@aws-cdk/aws-ecs/test/gelf-log-driver.test.ts index 9f6663304d353..e8cdce10736e1 100644 --- a/packages/@aws-cdk/aws-ecs/test/gelf-log-driver.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/gelf-log-driver.test.ts @@ -1,21 +1,20 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../lib'; let stack: cdk.Stack; let td: ecs.TaskDefinition; const image = ecs.ContainerImage.fromRegistry('test-image'); -nodeunitShim({ - 'setUp'(cb: () => void) { +describe('gelf log driver', () => { + beforeEach(() => { stack = new cdk.Stack(); td = new ecs.Ec2TaskDefinition(stack, 'TaskDefinition'); - cb(); - }, - 'create a gelf log driver with minimum options'(test: Test) { + }); + + test('create a gelf log driver with minimum options', () => { // WHEN td.addContainer('Container', { image, @@ -26,7 +25,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -37,12 +36,12 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); - 'create a gelf log driver using gelf with minimum options'(test: Test) { + test('create a gelf log driver using gelf with minimum options', () => { // WHEN td.addContainer('Container', { image, @@ -53,7 +52,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -64,8 +63,8 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/images/tag-parameter-container-image.test.ts b/packages/@aws-cdk/aws-ecs/test/images/tag-parameter-container-image.test.ts index d782d39d874cb..3f409739d76ed 100644 --- a/packages/@aws-cdk/aws-ecs/test/images/tag-parameter-container-image.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/images/tag-parameter-container-image.test.ts @@ -1,12 +1,12 @@ -import { expect, haveResourceLike, SynthUtils } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { SynthUtils } from '@aws-cdk/assert-internal'; import * as ecr from '@aws-cdk/aws-ecr'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../../lib'; -nodeunitShim({ - 'TagParameter container image': { - 'throws an error when tagParameterName() is used without binding the image'(test: Test) { +describe('tag parameter container image', () => { + describe('TagParameter container image', () => { + test('throws an error when tagParameterName() is used without binding the image', () => { // GIVEN const stack = new cdk.Stack(); const repository = new ecr.Repository(stack, 'Repository'); @@ -15,14 +15,14 @@ nodeunitShim({ value: tagParameterContainerImage.tagParameterName, }); - test.throws(() => { + expect(() => { SynthUtils.synthesize(stack); - }, /TagParameterContainerImage must be used in a container definition when using tagParameterName/); + }).toThrow(/TagParameterContainerImage must be used in a container definition when using tagParameterName/); - test.done(); - }, - 'throws an error when tagParameterValue() is used without binding the image'(test: Test) { + }); + + test('throws an error when tagParameterValue() is used without binding the image', () => { // GIVEN const stack = new cdk.Stack(); const repository = new ecr.Repository(stack, 'Repository'); @@ -31,14 +31,14 @@ nodeunitShim({ value: tagParameterContainerImage.tagParameterValue, }); - test.throws(() => { + expect(() => { SynthUtils.synthesize(stack); - }, /TagParameterContainerImage must be used in a container definition when using tagParameterValue/); + }).toThrow(/TagParameterContainerImage must be used in a container definition when using tagParameterValue/); + - test.done(); - }, + }); - 'can be used in a cross-account manner'(test: Test) { + test('can be used in a cross-account manner', () => { // GIVEN const app = new cdk.App(); const pipelineStack = new cdk.Stack(app, 'PipelineStack', { @@ -67,7 +67,7 @@ nodeunitShim({ }); // THEN - expect(pipelineStack).to(haveResourceLike('AWS::ECR::Repository', { + expect(pipelineStack).toHaveResourceLike('AWS::ECR::Repository', { RepositoryName: repositoryName, RepositoryPolicyText: { Statement: [{ @@ -88,11 +88,11 @@ nodeunitShim({ }, }], }, - })); - expect(serviceStack).to(haveResourceLike('AWS::IAM::Role', { + }); + expect(serviceStack).toHaveResourceLike('AWS::IAM::Role', { RoleName: 'servicestackionexecutionrolee7e2d9a783a54eb795f4', - })); - expect(serviceStack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + }); + expect(serviceStack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Image: { @@ -128,9 +128,9 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, - }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/journald-log-driver.test.ts b/packages/@aws-cdk/aws-ecs/test/journald-log-driver.test.ts index aeffbc2073629..fdf67efc3bb4f 100644 --- a/packages/@aws-cdk/aws-ecs/test/journald-log-driver.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/journald-log-driver.test.ts @@ -1,21 +1,20 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../lib'; let stack: cdk.Stack; let td: ecs.TaskDefinition; const image = ecs.ContainerImage.fromRegistry('test-image'); -nodeunitShim({ - 'setUp'(cb: () => void) { +describe('journald log driver', () => { + beforeEach(() => { stack = new cdk.Stack(); td = new ecs.Ec2TaskDefinition(stack, 'TaskDefinition'); - cb(); - }, - 'create a journald log driver with options'(test: Test) { + }); + + test('create a journald log driver with options', () => { // WHEN td.addContainer('Container', { image, @@ -26,7 +25,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -37,12 +36,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'create a journald log driver without options'(test: Test) { + }); + + test('create a journald log driver without options', () => { // WHEN td.addContainer('Container', { image, @@ -51,7 +50,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -59,12 +58,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'create a journald log driver using journald'(test: Test) { + }); + + test('create a journald log driver using journald', () => { // WHEN td.addContainer('Container', { image, @@ -73,7 +72,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -82,8 +81,8 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/json-file-log-driver.test.ts b/packages/@aws-cdk/aws-ecs/test/json-file-log-driver.test.ts index b282a61c29f88..0c21ff2d0b5f1 100644 --- a/packages/@aws-cdk/aws-ecs/test/json-file-log-driver.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/json-file-log-driver.test.ts @@ -1,21 +1,20 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../lib'; let stack: cdk.Stack; let td: ecs.TaskDefinition; const image = ecs.ContainerImage.fromRegistry('test-image'); -nodeunitShim({ - 'setUp'(cb: () => void) { +describe('json file log driver', () => { + beforeEach(() => { stack = new cdk.Stack(); td = new ecs.Ec2TaskDefinition(stack, 'TaskDefinition'); - cb(); - }, - 'create a json-file log driver with options'(test: Test) { + }); + + test('create a json-file log driver with options', () => { // WHEN td.addContainer('Container', { image, @@ -26,7 +25,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -37,12 +36,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'create a json-file log driver without options'(test: Test) { + }); + + test('create a json-file log driver without options', () => { // WHEN td.addContainer('Container', { image, @@ -51,7 +50,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -59,12 +58,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'create a json-file log driver using json-file'(test: Test) { + }); + + test('create a json-file log driver using json-file', () => { // WHEN td.addContainer('Container', { image, @@ -73,7 +72,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -82,8 +81,8 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/splunk-log-driver.test.ts b/packages/@aws-cdk/aws-ecs/test/splunk-log-driver.test.ts index ee7d3f4b68ee8..d9902cb9728e3 100644 --- a/packages/@aws-cdk/aws-ecs/test/splunk-log-driver.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/splunk-log-driver.test.ts @@ -1,23 +1,22 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; -import * as cdk from '@aws-cdk/core'; +import '@aws-cdk/assert-internal/jest'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import * as ssm from '@aws-cdk/aws-ssm'; -import { nodeunitShim, Test } from 'nodeunit-shim'; +import * as cdk from '@aws-cdk/core'; import * as ecs from '../lib'; let stack: cdk.Stack; let td: ecs.TaskDefinition; const image = ecs.ContainerImage.fromRegistry('test-image'); -nodeunitShim({ - 'setUp'(cb: () => void) { +describe('splunk log driver', () => { + beforeEach(() => { stack = new cdk.Stack(); td = new ecs.Ec2TaskDefinition(stack, 'TaskDefinition'); - cb(); - }, - 'create a splunk log driver with minimum options'(test: Test) { + }); + + test('create a splunk log driver with minimum options', () => { // WHEN td.addContainer('Container', { image, @@ -29,7 +28,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -41,12 +40,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'create a splunk log driver using splunk with minimum options'(test: Test) { + }); + + test('create a splunk log driver using splunk with minimum options', () => { // WHEN td.addContainer('Container', { image, @@ -58,7 +57,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -70,12 +69,12 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); - 'create a splunk log driver using splunk with sourcetype defined'(test: Test) { + test('create a splunk log driver using splunk with sourcetype defined', () => { // WHEN td.addContainer('Container', { image, @@ -88,7 +87,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -101,12 +100,12 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); - 'create a splunk log driver using secret splunk token from secrets manager'(test: Test) { + test('create a splunk log driver using secret splunk token from secrets manager', () => { const secret = new secretsmanager.Secret(stack, 'Secret'); // WHEN td.addContainer('Container', { @@ -119,7 +118,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -138,12 +137,12 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); - 'create a splunk log driver using secret splunk token from systems manager parameter store'(test: Test) { + test('create a splunk log driver using secret splunk token from systems manager parameter store', () => { const parameter = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'Parameter', { parameterName: '/token', version: 1, @@ -159,7 +158,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -195,13 +194,13 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); - 'throws when neither token nor secret token are provided'(test: Test) { - test.throws(() => { + test('throws when neither token nor secret token are provided', () => { + expect(() => { td.addContainer('Container', { image, logging: ecs.LogDrivers.splunk({ @@ -209,8 +208,8 @@ nodeunitShim({ }), memoryLimitMiB: 128, }); - }, 'Please provide either token or secretToken.'); + }).toThrow('Please provide either token or secretToken.'); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/syslog-log-driver.test.ts b/packages/@aws-cdk/aws-ecs/test/syslog-log-driver.test.ts index db328772d7548..ae32f55ecd863 100644 --- a/packages/@aws-cdk/aws-ecs/test/syslog-log-driver.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/syslog-log-driver.test.ts @@ -1,21 +1,20 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../lib'; let stack: cdk.Stack; let td: ecs.TaskDefinition; const image = ecs.ContainerImage.fromRegistry('test-image'); -nodeunitShim({ - 'setUp'(cb: () => void) { +describe('syslog log driver', () => { + beforeEach(() => { stack = new cdk.Stack(); td = new ecs.Ec2TaskDefinition(stack, 'TaskDefinition'); - cb(); - }, - 'create a syslog log driver with options'(test: Test) { + }); + + test('create a syslog log driver with options', () => { // WHEN td.addContainer('Container', { image, @@ -26,7 +25,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -37,12 +36,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'create a syslog log driver without options'(test: Test) { + }); + + test('create a syslog log driver without options', () => { // WHEN td.addContainer('Container', { image, @@ -51,7 +50,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -59,12 +58,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'create a syslog log driver using syslog'(test: Test) { + }); + + test('create a syslog log driver using syslog', () => { // WHEN td.addContainer('Container', { image, @@ -73,7 +72,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -82,8 +81,8 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/task-definition.test.ts b/packages/@aws-cdk/aws-ecs/test/task-definition.test.ts index 6e578952a5765..07b3a8211da06 100644 --- a/packages/@aws-cdk/aws-ecs/test/task-definition.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/task-definition.test.ts @@ -1,12 +1,11 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../lib'; -nodeunitShim({ - 'When creating a new TaskDefinition': { - 'A task definition with both compatibilities defaults to networkmode AwsVpc'(test: Test) { +describe('task definition', () => { + describe('When creating a new TaskDefinition', () => { + test('A task definition with both compatibilities defaults to networkmode AwsVpc', () => { // GIVEN const stack = new cdk.Stack(); @@ -18,16 +17,16 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { NetworkMode: 'awsvpc', - })); + }); + - test.done(); - }, - }, + }); + }); - 'When importing from an existing Task definition': { - 'can import using a task definition arn'(test: Test) { + describe('When importing from an existing Task definition', () => { + test('can import using a task definition arn', () => { // GIVEN const stack = new cdk.Stack(); const taskDefinitionArn = 'TDArn'; @@ -36,14 +35,14 @@ nodeunitShim({ const taskDefinition = ecs.TaskDefinition.fromTaskDefinitionArn(stack, 'TD_ID', taskDefinitionArn); // THEN - test.equal(taskDefinition.taskDefinitionArn, taskDefinitionArn); - test.equal(taskDefinition.compatibility, ecs.Compatibility.EC2_AND_FARGATE); - test.equal(taskDefinition.executionRole, undefined); + expect(taskDefinition.taskDefinitionArn).toEqual(taskDefinitionArn); + expect(taskDefinition.compatibility).toEqual(ecs.Compatibility.EC2_AND_FARGATE); + expect(taskDefinition.executionRole).toEqual(undefined); + - test.done(); - }, + }); - 'can import a Task Definition using attributes'(test: Test) { + test('can import a Task Definition using attributes', () => { // GIVEN const stack = new cdk.Stack(); const expectTaskDefinitionArn = 'TD_ARN'; @@ -62,16 +61,16 @@ nodeunitShim({ }); // THEN - test.equal(taskDefinition.taskDefinitionArn, expectTaskDefinitionArn); - test.equal(taskDefinition.compatibility, expectCompatibility); - test.equal(taskDefinition.executionRole, undefined); - test.equal(taskDefinition.networkMode, expectNetworkMode); - test.equal(taskDefinition.taskRole, expectTaskRole); + expect(taskDefinition.taskDefinitionArn).toEqual(expectTaskDefinitionArn); + expect(taskDefinition.compatibility).toEqual(expectCompatibility); + expect(taskDefinition.executionRole).toEqual(undefined); + expect(taskDefinition.networkMode).toEqual(expectNetworkMode); + expect(taskDefinition.taskRole).toEqual(expectTaskRole); - test.done(); - }, - 'returns an imported TaskDefinition that will throw an error when trying to access its yet to defined networkMode'(test: Test) { + }); + + test('returns an imported TaskDefinition that will throw an error when trying to access its yet to defined networkMode', () => { // GIVEN const stack = new cdk.Stack(); const expectTaskDefinitionArn = 'TD_ARN'; @@ -88,15 +87,15 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { taskDefinition.networkMode; - }, 'This operation requires the networkMode in ImportedTaskDefinition to be defined. ' + + }).toThrow('This operation requires the networkMode in ImportedTaskDefinition to be defined. ' + 'Add the \'networkMode\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); - test.done(); - }, - 'returns an imported TaskDefinition that will throw an error when trying to access its yet to defined taskRole'(test: Test) { + }); + + test('returns an imported TaskDefinition that will throw an error when trying to access its yet to defined taskRole', () => { // GIVEN const stack = new cdk.Stack(); const expectTaskDefinitionArn = 'TD_ARN'; @@ -111,12 +110,12 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { taskDefinition.taskRole; - }, 'This operation requires the taskRole in ImportedTaskDefinition to be defined. ' + + }).toThrow('This operation requires the taskRole in ImportedTaskDefinition to be defined. ' + 'Add the \'taskRole\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); - test.done(); - }, - }, + + }); + }); }); diff --git a/packages/@aws-cdk/aws-route53/package.json b/packages/@aws-cdk/aws-route53/package.json index 90608982f090b..24e72e9300c93 100644 --- a/packages/@aws-cdk/aws-route53/package.json +++ b/packages/@aws-cdk/aws-route53/package.json @@ -81,7 +81,6 @@ "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "jest": "^26.6.3", - "nodeunit-shim": "0.0.0", "pkglint": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-route53/test/hosted-zone-provider.test.ts b/packages/@aws-cdk/aws-route53/test/hosted-zone-provider.test.ts index bf4fba67df048..0b66c9589cd50 100644 --- a/packages/@aws-cdk/aws-route53/test/hosted-zone-provider.test.ts +++ b/packages/@aws-cdk/aws-route53/test/hosted-zone-provider.test.ts @@ -1,11 +1,11 @@ +import '@aws-cdk/assert-internal/jest'; import { SynthUtils } from '@aws-cdk/assert-internal'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import { HostedZone } from '../lib'; -nodeunitShim({ - 'Hosted Zone Provider': { - 'HostedZoneProvider will return context values if available'(test: Test) { +describe('hosted zone provider', () => { + describe('Hosted Zone Provider', () => { + test('HostedZoneProvider will return context values if available', () => { // GIVEN const stack = new cdk.Stack(undefined, 'TestStack', { env: { account: '12345', region: 'us-east-1' }, @@ -15,7 +15,7 @@ nodeunitShim({ HostedZone.fromLookup(stack, 'Ref', filter); const missing = SynthUtils.synthesize(stack).assembly.manifest.missing!; - test.ok(missing && missing.length === 1); + expect(missing && missing.length === 1).toEqual(true); const fakeZoneId = '11111111111111'; const fakeZone = { @@ -38,12 +38,10 @@ nodeunitShim({ const zoneRef = HostedZone.fromLookup(stack2, 'MyZoneProvider', filter); // THEN - test.deepEqual(zoneRef.hostedZoneId, fakeZoneId); - test.done(); - }, - 'HostedZoneProvider will return context values if available when using plain hosted zone id'( - test: Test, - ) { + expect(zoneRef.hostedZoneId).toEqual(fakeZoneId); + + }); + test('HostedZoneProvider will return context values if available when using plain hosted zone id', () => { // GIVEN const stack = new cdk.Stack(undefined, 'TestStack', { env: { account: '12345', region: 'us-east-1' }, @@ -53,7 +51,7 @@ nodeunitShim({ HostedZone.fromLookup(stack, 'Ref', filter); const missing = SynthUtils.synthesize(stack).assembly.manifest.missing!; - test.ok(missing && missing.length === 1); + expect(missing && missing.length === 1).toEqual(true); const fakeZoneId = '11111111111111'; const fakeZone = { @@ -78,8 +76,8 @@ nodeunitShim({ const zoneId = zone.hostedZoneId; // THEN - test.deepEqual(fakeZoneId, zoneId); - test.done(); - }, - }, + expect(fakeZoneId).toEqual(zoneId); + + }); + }); }); diff --git a/packages/@aws-cdk/aws-route53/test/hosted-zone.test.ts b/packages/@aws-cdk/aws-route53/test/hosted-zone.test.ts index a11b3308641d2..7939452fd3421 100644 --- a/packages/@aws-cdk/aws-route53/test/hosted-zone.test.ts +++ b/packages/@aws-cdk/aws-route53/test/hosted-zone.test.ts @@ -1,12 +1,11 @@ -import { expect } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import { HostedZone, PublicHostedZone } from '../lib'; -nodeunitShim({ - 'Hosted Zone': { - 'Hosted Zone constructs the ARN'(test: Test) { +describe('hosted zone', () => { + describe('Hosted Zone', () => { + test('Hosted Zone constructs the ARN', () => { // GIVEN const stack = new cdk.Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' }, @@ -16,7 +15,7 @@ nodeunitShim({ zoneName: 'testZone', }); - test.deepEqual(stack.resolve(testZone.hostedZoneArn), { + expect(stack.resolve(testZone.hostedZoneArn)).toEqual({ 'Fn::Join': [ '', [ @@ -28,11 +27,11 @@ nodeunitShim({ ], }); - test.done(); - }, - }, - 'Supports tags'(test: Test) { + }); + }); + + test('Supports tags', () => { // GIVEN const stack = new cdk.Stack(); @@ -43,7 +42,7 @@ nodeunitShim({ cdk.Tags.of(hostedZone).add('zoneTag', 'inMyZone'); // THEN - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ Resources: { HostedZoneDB99F866: { Type: 'AWS::Route53::HostedZone', @@ -60,10 +59,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 'with crossAccountZoneDelegationPrincipal'(test: Test) { + }); + + test('with crossAccountZoneDelegationPrincipal', () => { // GIVEN const stack = new cdk.Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' }, @@ -77,7 +76,7 @@ nodeunitShim({ }); // THEN - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ Resources: { HostedZoneDB99F866: { Type: 'AWS::Route53::HostedZone', @@ -151,23 +150,23 @@ nodeunitShim({ }, }); - test.done(); - }, - 'with crossAccountZoneDelegationPrincipal, throws if name provided without principal'(test: Test) { + }); + + test('with crossAccountZoneDelegationPrincipal, throws if name provided without principal', () => { // GIVEN const stack = new cdk.Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' }, }); // THEN - test.throws(() => { + expect(() => { new PublicHostedZone(stack, 'HostedZone', { zoneName: 'testZone', crossAccountZoneDelegationRoleName: 'myrole', }); - }, /crossAccountZoneDelegationRoleName property is not supported without crossAccountZoneDelegationPrincipal/); + }).toThrow(/crossAccountZoneDelegationRoleName property is not supported without crossAccountZoneDelegationPrincipal/); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-route53/test/record-set.test.ts b/packages/@aws-cdk/aws-route53/test/record-set.test.ts index a071ad2a1aeea..8e380e3dfcd81 100644 --- a/packages/@aws-cdk/aws-route53/test/record-set.test.ts +++ b/packages/@aws-cdk/aws-route53/test/record-set.test.ts @@ -1,11 +1,11 @@ -import { expect, haveResource, ResourcePart } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { ResourcePart } from '@aws-cdk/assert-internal'; import * as iam from '@aws-cdk/aws-iam'; import { Duration, RemovalPolicy, Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as route53 from '../lib'; -nodeunitShim({ - 'with default ttl'(test: Test) { +describe('record set', () => { + test('with default ttl', () => { // GIVEN const stack = new Stack(); @@ -22,7 +22,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'CNAME', HostedZoneId: { @@ -32,11 +32,11 @@ nodeunitShim({ 'zzz', ], TTL: '1800', - })); - test.done(); - }, + }); + + }); - 'with custom ttl'(test: Test) { + test('with custom ttl', () => { // GIVEN const stack = new Stack(); @@ -54,7 +54,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'aa.myzone.', Type: 'CNAME', HostedZoneId: { @@ -64,11 +64,11 @@ nodeunitShim({ 'bbb', ], TTL: '6077', - })); - test.done(); - }, + }); - 'with ttl of 0'(test: Test) { + }); + + test('with ttl of 0', () => { // GIVEN const stack = new Stack(); @@ -86,13 +86,13 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { TTL: '0', - })); - test.done(); - }, + }); + + }); - 'defaults to zone root'(test: Test) { + test('defaults to zone root', () => { // GIVEN const stack = new Stack(); @@ -108,7 +108,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'myzone.', Type: 'A', HostedZoneId: { @@ -117,11 +117,11 @@ nodeunitShim({ ResourceRecords: [ '1.2.3.4', ], - })); - test.done(); - }, + }); + + }); - 'A record with ip addresses'(test: Test) { + test('A record with ip addresses', () => { // GIVEN const stack = new Stack(); @@ -137,7 +137,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'A', HostedZoneId: { @@ -148,11 +148,11 @@ nodeunitShim({ '5.6.7.8', ], TTL: '1800', - })); - test.done(); - }, + }); - 'A record with alias'(test: Test) { + }); + + test('A record with alias', () => { // GIVEN const stack = new Stack(); @@ -177,7 +177,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: '_foo.myzone.', HostedZoneId: { Ref: 'HostedZoneDB99F866', @@ -187,12 +187,12 @@ nodeunitShim({ HostedZoneId: 'Z2P70J7EXAMPLE', DNSName: 'foo.example.com', }, - })); + }); - test.done(); - }, - 'AAAA record with ip addresses'(test: Test) { + }); + + test('AAAA record with ip addresses', () => { // GIVEN const stack = new Stack(); @@ -208,7 +208,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'AAAA', HostedZoneId: { @@ -218,11 +218,11 @@ nodeunitShim({ '2001:0db8:85a3:0000:0000:8a2e:0370:7334', ], TTL: '1800', - })); - test.done(); - }, + }); + + }); - 'AAAA record with alias on zone root'(test: Test) { + test('AAAA record with alias on zone root', () => { // GIVEN const stack = new Stack(); const zone = new route53.HostedZone(stack, 'HostedZone', { @@ -245,7 +245,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'myzone.', HostedZoneId: { Ref: 'HostedZoneDB99F866', @@ -255,12 +255,12 @@ nodeunitShim({ HostedZoneId: 'Z2P70J7EXAMPLE', DNSName: 'foo.example.com', }, - })); + }); + - test.done(); - }, + }); - 'CNAME record'(test: Test) { + test('CNAME record', () => { // GIVEN const stack = new Stack(); @@ -276,7 +276,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'CNAME', HostedZoneId: { @@ -286,11 +286,11 @@ nodeunitShim({ 'hello', ], TTL: '1800', - })); - test.done(); - }, + }); - 'TXT record'(test: Test) { + }); + + test('TXT record', () => { // GIVEN const stack = new Stack(); @@ -306,7 +306,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'TXT', HostedZoneId: { @@ -316,11 +316,11 @@ nodeunitShim({ '"should be enclosed with double quotes"', ], TTL: '1800', - })); - test.done(); - }, + }); + + }); - 'TXT record with value longer than 255 chars'(test: Test) { + test('TXT record with value longer than 255 chars', () => { // GIVEN const stack = new Stack(); @@ -336,7 +336,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'TXT', HostedZoneId: { @@ -346,11 +346,11 @@ nodeunitShim({ '"hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello""hello"', ], TTL: '1800', - })); - test.done(); - }, + }); + + }); - 'SRV record'(test: Test) { + test('SRV record', () => { // GIVEN const stack = new Stack(); @@ -371,7 +371,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'SRV', HostedZoneId: { @@ -381,11 +381,11 @@ nodeunitShim({ '10 5 8080 aws.com', ], TTL: '1800', - })); - test.done(); - }, + }); + + }); - 'CAA record'(test: Test) { + test('CAA record', () => { // GIVEN const stack = new Stack(); @@ -405,7 +405,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'CAA', HostedZoneId: { @@ -415,11 +415,11 @@ nodeunitShim({ '0 issue "ssl.com"', ], TTL: '1800', - })); - test.done(); - }, + }); - 'CAA Amazon record'(test: Test) { + }); + + test('CAA Amazon record', () => { // GIVEN const stack = new Stack(); @@ -433,7 +433,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'myzone.', Type: 'CAA', HostedZoneId: { @@ -443,11 +443,11 @@ nodeunitShim({ '0 issue "amazon.com"', ], TTL: '1800', - })); - test.done(); - }, + }); + + }); - 'CAA Amazon record with record name'(test: Test) { + test('CAA Amazon record with record name', () => { // GIVEN const stack = new Stack(); @@ -462,7 +462,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'CAA', HostedZoneId: { @@ -472,11 +472,11 @@ nodeunitShim({ '0 issue "amazon.com"', ], TTL: '1800', - })); - test.done(); - }, + }); + + }); - 'MX record'(test: Test) { + test('MX record', () => { // GIVEN const stack = new Stack(); @@ -495,7 +495,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'mail.myzone.', Type: 'MX', HostedZoneId: { @@ -505,11 +505,11 @@ nodeunitShim({ '10 workmail.aws', ], TTL: '1800', - })); - test.done(); - }, + }); - 'NS record'(test: Test) { + }); + + test('NS record', () => { // GIVEN const stack = new Stack(); @@ -525,7 +525,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'NS', HostedZoneId: { @@ -536,11 +536,11 @@ nodeunitShim({ 'ns-2.awsdns.com.', ], TTL: '1800', - })); - test.done(); - }, + }); - 'DS record'(test: Test) { + }); + + test('DS record', () => { // GIVEN const stack = new Stack(); @@ -556,7 +556,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'DS', HostedZoneId: { @@ -566,11 +566,11 @@ nodeunitShim({ '12345 3 1 123456789abcdef67890123456789abcdef67890', ], TTL: '1800', - })); - test.done(); - }, + }); + + }); - 'Zone delegation record'(test: Test) { + test('Zone delegation record', () => { // GIVEN const stack = new Stack(); @@ -586,7 +586,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'foo.myzone.', Type: 'NS', HostedZoneId: { @@ -596,11 +596,11 @@ nodeunitShim({ 'ns-1777.awsdns-30.co.uk.', ], TTL: '172800', - })); - test.done(); - }, + }); + + }); - 'Cross account zone delegation record with parentHostedZoneId'(test: Test) { + test('Cross account zone delegation record with parentHostedZoneId', () => { // GIVEN const stack = new Stack(); const parentZone = new route53.PublicHostedZone(stack, 'ParentHostedZone', { @@ -621,7 +621,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('Custom::CrossAccountZoneDelegation', { + expect(stack).toHaveResource('Custom::CrossAccountZoneDelegation', { ServiceToken: { 'Fn::GetAtt': [ 'CustomCrossAccountZoneDelegationCustomResourceProviderHandler44A84265', @@ -645,15 +645,15 @@ nodeunitShim({ ], }, TTL: 60, - })); - expect(stack).to(haveResource('Custom::CrossAccountZoneDelegation', { + }); + expect(stack).toHaveResource('Custom::CrossAccountZoneDelegation', { DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition)); - test.done(); - }, + }, ResourcePart.CompleteDefinition); + + }); - 'Cross account zone delegation record with parentHostedZoneName'(test: Test) { + test('Cross account zone delegation record with parentHostedZoneName', () => { // GIVEN const stack = new Stack(); const parentZone = new route53.PublicHostedZone(stack, 'ParentHostedZone', { @@ -673,7 +673,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('Custom::CrossAccountZoneDelegation', { + expect(stack).toHaveResource('Custom::CrossAccountZoneDelegation', { ServiceToken: { 'Fn::GetAtt': [ 'CustomCrossAccountZoneDelegationCustomResourceProviderHandler44A84265', @@ -695,11 +695,11 @@ nodeunitShim({ ], }, TTL: 60, - })); - test.done(); - }, + }); - 'Cross account zone delegation record throws when parent id and name both/nither are supplied'(test: Test) { + }); + + test('Cross account zone delegation record throws when parent id and name both/nither are supplied', () => { // GIVEN const stack = new Stack(); const parentZone = new route53.PublicHostedZone(stack, 'ParentHostedZone', { @@ -712,15 +712,15 @@ nodeunitShim({ zoneName: 'sub.myzone.com', }); - test.throws(() => { + expect(() => { new route53.CrossAccountZoneDelegationRecord(stack, 'Delegation1', { delegatedZone: childZone, delegationRole: parentZone.crossAccountZoneDelegationRole!, ttl: Duration.seconds(60), }); - }, /At least one of parentHostedZoneName or parentHostedZoneId is required/); + }).toThrow(/At least one of parentHostedZoneName or parentHostedZoneId is required/); - test.throws(() => { + expect(() => { new route53.CrossAccountZoneDelegationRecord(stack, 'Delegation2', { delegatedZone: childZone, parentHostedZoneId: parentZone.hostedZoneId, @@ -728,8 +728,8 @@ nodeunitShim({ delegationRole: parentZone.crossAccountZoneDelegationRole!, ttl: Duration.seconds(60), }); - }, /Only one of parentHostedZoneName and parentHostedZoneId is supported/); + }).toThrow(/Only one of parentHostedZoneName and parentHostedZoneId is supported/); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-route53/test/route53.test.ts b/packages/@aws-cdk/aws-route53/test/route53.test.ts index e5dd624163a9b..f86b155592db7 100644 --- a/packages/@aws-cdk/aws-route53/test/route53.test.ts +++ b/packages/@aws-cdk/aws-route53/test/route53.test.ts @@ -1,15 +1,15 @@ -import { beASupersetOfTemplate, exactlyMatchTemplate, expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { MatchStyle } from '@aws-cdk/assert-internal'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import { HostedZone, PrivateHostedZone, PublicHostedZone, TxtRecord } from '../lib'; -nodeunitShim({ - 'default properties': { - 'public hosted zone'(test: Test) { +describe('route53', () => { + describe('default properties', () => { + test('public hosted zone', () => { const app = new TestApp(); new PublicHostedZone(app.stack, 'HostedZone', { zoneName: 'test.public' }); - expect(app.stack).to(exactlyMatchTemplate({ + expect(app.stack).toMatchTemplate({ Resources: { HostedZoneDB99F866: { Type: 'AWS::Route53::HostedZone', @@ -18,14 +18,14 @@ nodeunitShim({ }, }, }, - })); - test.done(); - }, - 'private hosted zone'(test: Test) { + }); + + }); + test('private hosted zone', () => { const app = new TestApp(); const vpcNetwork = new ec2.Vpc(app.stack, 'VPC'); new PrivateHostedZone(app.stack, 'HostedZone', { zoneName: 'test.private', vpc: vpcNetwork }); - expect(app.stack).to(beASupersetOfTemplate({ + expect(app.stack).toMatchTemplate({ Resources: { HostedZoneDB99F866: { Type: 'AWS::Route53::HostedZone', @@ -38,16 +38,16 @@ nodeunitShim({ }, }, }, - })); - test.done(); - }, - 'when specifying multiple VPCs'(test: Test) { + }, MatchStyle.SUPERSET); + + }); + test('when specifying multiple VPCs', () => { const app = new TestApp(); const vpcNetworkA = new ec2.Vpc(app.stack, 'VPC1'); const vpcNetworkB = new ec2.Vpc(app.stack, 'VPC2'); new PrivateHostedZone(app.stack, 'HostedZone', { zoneName: 'test.private', vpc: vpcNetworkA }) .addVpc(vpcNetworkB); - expect(app.stack).to(beASupersetOfTemplate({ + expect(app.stack).toMatchTemplate({ Resources: { HostedZoneDB99F866: { Type: 'AWS::Route53::HostedZone', @@ -64,12 +64,12 @@ nodeunitShim({ }, }, }, - })); - test.done(); - }, - }, + }, MatchStyle.SUPERSET); - 'exporting and importing works'(test: Test) { + }); + }); + + test('exporting and importing works', () => { const stack2 = new cdk.Stack(); const importedZone = HostedZone.fromHostedZoneAttributes(stack2, 'Imported', { @@ -83,17 +83,17 @@ nodeunitShim({ values: ['SeeThere'], }); - expect(stack2).to(haveResource('AWS::Route53::RecordSet', { + expect(stack2).toHaveResource('AWS::Route53::RecordSet', { HostedZoneId: 'hosted-zone-id', Name: 'lookHere.cdk.local.', ResourceRecords: ['"SeeThere"'], Type: 'TXT', - })); + }); - test.done(); - }, - 'adds period to name if not provided'(test: Test) { + }); + + test('adds period to name if not provided', () => { // GIVEN const stack = new cdk.Stack(); @@ -103,22 +103,22 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::HostedZone', { + expect(stack).toHaveResource('AWS::Route53::HostedZone', { Name: 'zonename.', - })); - test.done(); - }, + }); - 'fails if zone name ends with a trailing dot'(test: Test) { + }); + + test('fails if zone name ends with a trailing dot', () => { // GIVEN const stack = new cdk.Stack(); // THEN - test.throws(() => new HostedZone(stack, 'MyHostedZone', { zoneName: 'zonename.' }), /zone name must not end with a trailing dot/); - test.done(); - }, + expect(() => new HostedZone(stack, 'MyHostedZone', { zoneName: 'zonename.' })).toThrow(/zone name must not end with a trailing dot/); + + }); - 'a hosted zone can be assiciated with a VPC either upon creation or using "addVpc"'(test: Test) { + test('a hosted zone can be assiciated with a VPC either upon creation or using "addVpc"', () => { // GIVEN const stack = new cdk.Stack(); const vpc1 = new ec2.Vpc(stack, 'VPC1'); @@ -133,7 +133,7 @@ nodeunitShim({ zone.addVpc(vpc3); // THEN - expect(stack).to(haveResource('AWS::Route53::HostedZone', { + expect(stack).toHaveResource('AWS::Route53::HostedZone', { VPCs: [ { VPCId: { @@ -160,22 +160,22 @@ nodeunitShim({ }, }, ], - })); - test.done(); - }, + }); - 'public zone cannot be associated with a vpc (runtime error)'(test: Test) { + }); + + test('public zone cannot be associated with a vpc (runtime error)', () => { // GIVEN const stack = new cdk.Stack(); const zone = new PublicHostedZone(stack, 'MyHostedZone', { zoneName: 'zonename' }); const vpc = new ec2.Vpc(stack, 'VPC'); // THEN - test.throws(() => zone.addVpc(vpc), /Cannot associate public hosted zones with a VPC/); - test.done(); - }, + expect(() => zone.addVpc(vpc)).toThrow(/Cannot associate public hosted zones with a VPC/); + + }); - 'setting up zone delegation'(test: Test) { + test('setting up zone delegation', () => { // GIVEN const stack = new cdk.Stack(); const zone = new PublicHostedZone(stack, 'TopZone', { zoneName: 'top.test' }); @@ -185,17 +185,17 @@ nodeunitShim({ zone.addDelegation(delegate, { ttl: cdk.Duration.seconds(1337) }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Type: 'NS', Name: 'sub.top.test.', HostedZoneId: stack.resolve(zone.hostedZoneId), ResourceRecords: stack.resolve(delegate.hostedZoneNameServers), TTL: '1337', - })); - test.done(); - }, + }); + + }); - 'public hosted zone wiht caaAmazon set to true'(test: Test) { + test('public hosted zone wiht caaAmazon set to true', () => { // GIVEN const stack = new cdk.Stack(); @@ -206,15 +206,15 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Type: 'CAA', Name: 'protected.com.', ResourceRecords: [ '0 issue "amazon.com"', ], - })); - test.done(); - }, + }); + + }); }); class TestApp { diff --git a/packages/@aws-cdk/aws-route53/test/util.test.ts b/packages/@aws-cdk/aws-route53/test/util.test.ts index c6ded4e74f7b4..f6ec2b7cd36a6 100644 --- a/packages/@aws-cdk/aws-route53/test/util.test.ts +++ b/packages/@aws-cdk/aws-route53/test/util.test.ts @@ -1,21 +1,20 @@ import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import { HostedZone } from '../lib'; import * as util from '../lib/util'; -nodeunitShim({ - 'throws when zone name ending with a \'.\''(test: Test) { - test.throws(() => util.validateZoneName('zone.name.'), /trailing dot/); - test.done(); - }, +describe('util', () => { + test('throws when zone name ending with a \'.\'', () => { + expect(() => util.validateZoneName('zone.name.')).toThrow(/trailing dot/); - 'accepts a valid domain name'(test: Test) { + }); + + test('accepts a valid domain name', () => { const domainName = 'amazonaws.com'; util.validateZoneName(domainName); - test.done(); - }, - 'providedName ending with a dot returns the name'(test: Test) { + }); + + test('providedName ending with a dot returns the name', () => { // GIVEN const stack = new cdk.Stack(); @@ -27,11 +26,11 @@ nodeunitShim({ })); // THEN - test.equal(qualified, 'test.domain.com.'); - test.done(); - }, + expect(qualified).toEqual('test.domain.com.'); + + }); - 'providedName that matches zoneName returns providedName with a trailing dot'(test: Test) { + test('providedName that matches zoneName returns providedName with a trailing dot', () => { // GIVEN const stack = new cdk.Stack(); @@ -43,11 +42,11 @@ nodeunitShim({ })); // THEN - test.equal(qualified, 'test.domain.com.'); - test.done(); - }, + expect(qualified).toEqual('test.domain.com.'); - 'providedName that ends with zoneName returns providedName with a trailing dot'(test: Test) { + }); + + test('providedName that ends with zoneName returns providedName with a trailing dot', () => { // GIVEN const stack = new cdk.Stack(); @@ -59,11 +58,11 @@ nodeunitShim({ })); // THEN - test.equal(qualified, 'test.domain.com.'); - test.done(); - }, + expect(qualified).toEqual('test.domain.com.'); + + }); - 'providedName that does not match zoneName concatenates providedName and zoneName'(test: Test) { + test('providedName that does not match zoneName concatenates providedName and zoneName', () => { // GIVEN const stack = new cdk.Stack(); @@ -75,7 +74,7 @@ nodeunitShim({ })); // THEN - test.equal(qualified, 'test.domain.com.'); - test.done(); - }, + expect(qualified).toEqual('test.domain.com.'); + + }); }); diff --git a/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts b/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts index 82706fe29d0ea..35b7ac9c67596 100644 --- a/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts +++ b/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable jest/no-disabled-tests */ import { expect as cdkExpect, haveResource, haveResourceLike, ResourcePart } from '@aws-cdk/assert-internal'; import '@aws-cdk/assert-internal/jest'; import { IVpcEndpointServiceLoadBalancer, VpcEndpointService } from '@aws-cdk/aws-ec2'; diff --git a/packages/@aws-cdk/aws-s3/package.json b/packages/@aws-cdk/aws-s3/package.json index da9bdd3d05df4..865be5d5232b8 100644 --- a/packages/@aws-cdk/aws-s3/package.json +++ b/packages/@aws-cdk/aws-s3/package.json @@ -78,8 +78,8 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", + "jest": "^26.6.3", "pkglint": "0.0.0", - "nodeunit-shim": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-s3/test/aspect.test.ts b/packages/@aws-cdk/aws-s3/test/aspect.test.ts index ef0394a4cf9cb..023ef826c18f8 100644 --- a/packages/@aws-cdk/aws-s3/test/aspect.test.ts +++ b/packages/@aws-cdk/aws-s3/test/aspect.test.ts @@ -1,12 +1,11 @@ -// import { expect, haveResource, haveResourceLike, SynthUtils } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import { SynthUtils } from '@aws-cdk/assert-internal'; import * as cdk from '@aws-cdk/core'; import { IConstruct } from 'constructs'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as s3 from '../lib'; -nodeunitShim({ - 'bucket must have versioning: failure'(test: Test) { +describe('aspect', () => { + test('bucket must have versioning: failure', () => { // GIVEN const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket'); @@ -17,12 +16,12 @@ nodeunitShim({ // THEN const assembly = SynthUtils.synthesize(stack); const errorMessage = assembly.messages.find(m => m.entry.data === 'Bucket versioning is not enabled'); - test.ok(errorMessage, 'Error message not reported'); + expect(errorMessage).toBeDefined(); - test.done(); - }, - 'bucket must have versioning: success'(test: Test) { + }); + + test('bucket must have versioning: success', () => { // GIVEN const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { @@ -34,10 +33,10 @@ nodeunitShim({ // THEN const assembly = SynthUtils.synthesize(stack); - test.deepEqual(assembly.messages, []); + expect(assembly.messages.length).toEqual(0); + - test.done(); - }, + }); }); class BucketVersioningChecker implements cdk.IAspect { diff --git a/packages/@aws-cdk/aws-s3/test/bucket-policy.test.ts b/packages/@aws-cdk/aws-s3/test/bucket-policy.test.ts index b64ab3d99df43..13b4b798bbd09 100644 --- a/packages/@aws-cdk/aws-s3/test/bucket-policy.test.ts +++ b/packages/@aws-cdk/aws-s3/test/bucket-policy.test.ts @@ -1,14 +1,13 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import { AnyPrincipal, PolicyStatement } from '@aws-cdk/aws-iam'; import { RemovalPolicy, Stack, App } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as s3 from '../lib'; // to make it easy to copy & paste from output: /* eslint-disable quote-props */ -nodeunitShim({ - 'default properties'(test: Test) { +describe('bucket policy', () => { + test('default properties', () => { const stack = new Stack(); const myBucket = new s3.Bucket(stack, 'MyBucket'); @@ -21,7 +20,7 @@ nodeunitShim({ principals: [new AnyPrincipal()], })); - expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + expect(stack).toHaveResource('AWS::S3::BucketPolicy', { Bucket: { 'Ref': 'MyBucketF68F3FF0', }, @@ -36,12 +35,12 @@ nodeunitShim({ }, ], }, - })); + }); - test.done(); - }, - 'when specifying a removalPolicy at creation'(test: Test) { + }); + + test('when specifying a removalPolicy at creation', () => { const stack = new Stack(); const myBucket = new s3.Bucket(stack, 'MyBucket'); @@ -55,7 +54,7 @@ nodeunitShim({ principals: [new AnyPrincipal()], })); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -86,10 +85,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 'when specifying a removalPolicy after creation'(test: Test) { + }); + + test('when specifying a removalPolicy after creation', () => { const stack = new Stack(); const myBucket = new s3.Bucket(stack, 'MyBucket'); @@ -100,7 +99,7 @@ nodeunitShim({ })); myBucket.policy?.applyRemovalPolicy(RemovalPolicy.RETAIN); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -131,10 +130,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 'fails if bucket policy has no actions'(test: Test) { + }); + + test('fails if bucket policy has no actions', () => { const app = new App(); const stack = new Stack(app, 'my-stack'); const myBucket = new s3.Bucket(stack, 'MyBucket'); @@ -143,12 +142,12 @@ nodeunitShim({ principals: [new AnyPrincipal()], })); - test.throws(() => app.synth(), /A PolicyStatement must specify at least one \'action\' or \'notAction\'/); + expect(() => app.synth()).toThrow(/A PolicyStatement must specify at least one \'action\' or \'notAction\'/); + - test.done(); - }, + }); - 'fails if bucket policy has no IAM principals'(test: Test) { + test('fails if bucket policy has no IAM principals', () => { const app = new App(); const stack = new Stack(app, 'my-stack'); const myBucket = new s3.Bucket(stack, 'MyBucket'); @@ -157,8 +156,8 @@ nodeunitShim({ actions: ['s3:GetObject*'], })); - test.throws(() => app.synth(), /A PolicyStatement used in a resource-based policy must specify at least one IAM principal/); + expect(() => app.synth()).toThrow(/A PolicyStatement used in a resource-based policy must specify at least one IAM principal/); + - test.done(); - }, + }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3/test/cors.test.ts b/packages/@aws-cdk/aws-s3/test/cors.test.ts index 5769c1059640d..feddf0b159669 100644 --- a/packages/@aws-cdk/aws-s3/test/cors.test.ts +++ b/packages/@aws-cdk/aws-s3/test/cors.test.ts @@ -1,10 +1,9 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import { Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import { Bucket, HttpMethods } from '../lib'; -nodeunitShim({ - 'Can use addCors() to add a CORS configuration'(test: Test) { +describe('cors', () => { + test('Can use addCors() to add a CORS configuration', () => { // GIVEN const stack = new Stack(); @@ -16,19 +15,19 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { CorsConfiguration: { CorsRules: [{ AllowedMethods: ['GET', 'HEAD'], AllowedOrigins: ['https://example.com'], }], }, - })); + }); + - test.done(); - }, + }); - 'Bucket with multiple cors configurations'(test: Test) { + test('Bucket with multiple cors configurations', () => { // GIVEN const stack = new Stack(); @@ -74,7 +73,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { CorsConfiguration: { CorsRules: [ { @@ -114,8 +113,8 @@ nodeunitShim({ }, ], }, - })); + }); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-s3/test/metrics.test.ts b/packages/@aws-cdk/aws-s3/test/metrics.test.ts index bf7e57e04b557..9072c33a5fcb6 100644 --- a/packages/@aws-cdk/aws-s3/test/metrics.test.ts +++ b/packages/@aws-cdk/aws-s3/test/metrics.test.ts @@ -1,10 +1,9 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import { Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import { Bucket } from '../lib'; -nodeunitShim({ - 'Can use addMetrics() to add a metric configuration'(test: Test) { +describe('metrics', () => { + test('Can use addMetrics() to add a metric configuration', () => { // GIVEN const stack = new Stack(); @@ -15,16 +14,16 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { MetricsConfigurations: [{ Id: 'test', }], - })); + }); + - test.done(); - }, + }); - 'Bucket with metrics on prefix'(test: Test) { + test('Bucket with metrics on prefix', () => { // GIVEN const stack = new Stack(); @@ -37,17 +36,17 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { MetricsConfigurations: [{ Id: 'test', Prefix: 'prefix', }], - })); + }); + - test.done(); - }, + }); - 'Bucket with metrics on tag filter'(test: Test) { + test('Bucket with metrics on tag filter', () => { // GIVEN const stack = new Stack(); @@ -60,7 +59,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { MetricsConfigurations: [{ Id: 'test', TagFilters: [ @@ -68,12 +67,12 @@ nodeunitShim({ { Key: 'tagname2', Value: 'tagvalue2' }, ], }], - })); + }); + - test.done(); - }, + }); - 'Bucket with multiple metric configurations'(test: Test) { + test('Bucket with multiple metric configurations', () => { // GIVEN const stack = new Stack(); @@ -93,7 +92,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { MetricsConfigurations: [{ Id: 'test', TagFilters: [ @@ -105,8 +104,8 @@ nodeunitShim({ Id: 'test2', Prefix: 'prefix', }], - })); + }); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-s3/test/notification.test.ts b/packages/@aws-cdk/aws-s3/test/notification.test.ts index 75906826ddeff..2f3178fb42af0 100644 --- a/packages/@aws-cdk/aws-s3/test/notification.test.ts +++ b/packages/@aws-cdk/aws-s3/test/notification.test.ts @@ -1,10 +1,10 @@ -import { expect, haveResource, haveResourceLike, ResourcePart } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { ResourcePart } from '@aws-cdk/assert-internal'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as s3 from '../lib'; -nodeunitShim({ - 'when notification is added a custom s3 bucket notification resource is provisioned'(test: Test) { +describe('notification', () => { + test('when notification is added a custom s3 bucket notification resource is provisioned', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket'); @@ -16,8 +16,8 @@ nodeunitShim({ }), }); - expect(stack).to(haveResource('AWS::S3::Bucket')); - expect(stack).to(haveResource('Custom::S3BucketNotifications', { + expect(stack).toHaveResource('AWS::S3::Bucket'); + expect(stack).toHaveResource('Custom::S3BucketNotifications', { NotificationConfiguration: { TopicConfigurations: [ { @@ -28,12 +28,12 @@ nodeunitShim({ }, ], }, - })); + }); + - test.done(); - }, + }); - 'can specify prefix and suffix filter rules'(test: Test) { + test('can specify prefix and suffix filter rules', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket'); @@ -45,7 +45,7 @@ nodeunitShim({ }), }, { prefix: 'images/', suffix: '.png' }); - expect(stack).to(haveResource('Custom::S3BucketNotifications', { + expect(stack).toHaveResource('Custom::S3BucketNotifications', { NotificationConfiguration: { TopicConfigurations: [ { @@ -70,12 +70,12 @@ nodeunitShim({ }, ], }, - })); + }); + - test.done(); - }, + }); - 'the notification lambda handler must depend on the role to prevent executing too early'(test: Test) { + test('the notification lambda handler must depend on the role to prevent executing too early', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket'); @@ -87,7 +87,7 @@ nodeunitShim({ }), }); - expect(stack).to(haveResourceLike('AWS::Lambda::Function', { + expect(stack).toHaveResourceLike('AWS::Lambda::Function', { Type: 'AWS::Lambda::Function', Properties: { Role: { @@ -99,38 +99,38 @@ nodeunitShim({ }, DependsOn: ['BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36', 'BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC'], - }, ResourcePart.CompleteDefinition ) ); + }, ResourcePart.CompleteDefinition ); - test.done(); - }, - 'throws with multiple prefix rules in a filter'(test: Test) { + }); + + test('throws with multiple prefix rules in a filter', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket'); - test.throws(() => bucket.addEventNotification(s3.EventType.OBJECT_CREATED, { + expect(() => bucket.addEventNotification(s3.EventType.OBJECT_CREATED, { bind: () => ({ arn: 'ARN', type: s3.BucketNotificationDestinationType.TOPIC, }), - }, { prefix: 'images/' }, { prefix: 'archive/' }), /prefix rule/); + }, { prefix: 'images/' }, { prefix: 'archive/' })).toThrow(/prefix rule/); + - test.done(); - }, + }); - 'throws with multiple suffix rules in a filter'(test: Test) { + test('throws with multiple suffix rules in a filter', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket'); - test.throws(() => bucket.addEventNotification(s3.EventType.OBJECT_CREATED, { + expect(() => bucket.addEventNotification(s3.EventType.OBJECT_CREATED, { bind: () => ({ arn: 'ARN', type: s3.BucketNotificationDestinationType.TOPIC, }), - }, { suffix: '.png' }, { suffix: '.zip' }), /suffix rule/); + }, { suffix: '.png' }, { suffix: '.zip' })).toThrow(/suffix rule/); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-s3/test/rules.test.ts b/packages/@aws-cdk/aws-s3/test/rules.test.ts index fb613bcf109e8..818ae7d830439 100644 --- a/packages/@aws-cdk/aws-s3/test/rules.test.ts +++ b/packages/@aws-cdk/aws-s3/test/rules.test.ts @@ -1,10 +1,9 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import { Duration, Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import { Bucket, StorageClass } from '../lib'; -nodeunitShim({ - 'Bucket with expiration days'(test: Test) { +describe('rules', () => { + test('Bucket with expiration days', () => { // GIVEN const stack = new Stack(); @@ -16,19 +15,19 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { LifecycleConfiguration: { Rules: [{ ExpirationInDays: 30, Status: 'Enabled', }], }, - })); + }); + - test.done(); - }, + }); - 'Can use addLifecycleRule() to add a lifecycle rule'(test: Test) { + test('Can use addLifecycleRule() to add a lifecycle rule', () => { // GIVEN const stack = new Stack(); @@ -39,19 +38,19 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { LifecycleConfiguration: { Rules: [{ ExpirationInDays: 30, Status: 'Enabled', }], }, - })); + }); - test.done(); - }, - 'Bucket with expiration date'(test: Test) { + }); + + test('Bucket with expiration date', () => { // GIVEN const stack = new Stack(); @@ -63,19 +62,19 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { LifecycleConfiguration: { Rules: [{ ExpirationDate: '2018-01-01T00:00:00', Status: 'Enabled', }], }, - })); + }); + - test.done(); - }, + }); - 'Bucket with transition rule'(test: Test) { + test('Bucket with transition rule', () => { // GIVEN const stack = new Stack(); @@ -90,7 +89,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { LifecycleConfiguration: { Rules: [{ Transitions: [{ @@ -100,23 +99,23 @@ nodeunitShim({ Status: 'Enabled', }], }, - })); + }); + - test.done(); - }, + }); - 'Noncurrent rule on nonversioned bucket fails'(test: Test) { + test('Noncurrent rule on nonversioned bucket fails', () => { // GIVEN const stack = new Stack(); // WHEN: Fail because of lack of versioning - test.throws(() => { + expect(() => { new Bucket(stack, 'Bucket1', { lifecycleRules: [{ noncurrentVersionExpiration: Duration.days(10), }], }); - }); + }).toThrow(); // WHEN: Succeeds because versioning is enabled new Bucket(stack, 'Bucket2', { @@ -126,10 +125,10 @@ nodeunitShim({ }], }); - test.done(); - }, - 'Bucket with expiredObjectDeleteMarker'(test: Test) { + }); + + test('Bucket with expiredObjectDeleteMarker', () => { // GIVEN const stack = new Stack(); @@ -141,15 +140,15 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { LifecycleConfiguration: { Rules: [{ ExpiredObjectDeleteMarker: true, Status: 'Enabled', }], }, - })); + }); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-s3/test/util.test.ts b/packages/@aws-cdk/aws-s3/test/util.test.ts index 528b83b10404b..e688932a0f7eb 100644 --- a/packages/@aws-cdk/aws-s3/test/util.test.ts +++ b/packages/@aws-cdk/aws-s3/test/util.test.ts @@ -1,68 +1,69 @@ +import '@aws-cdk/assert-internal/jest'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import { parseBucketArn, parseBucketName } from '../lib/util'; -nodeunitShim({ - parseBucketArn: { - 'explicit arn'(test: Test) { +describe('utils', () => { + describe('parseBucketArn', () => { + test('explicit arn', () => { const stack = new cdk.Stack(); const bucketArn = 'my:bucket:arn'; - test.deepEqual(parseBucketArn(stack, { bucketArn }), bucketArn); - test.done(); - }, + expect(parseBucketArn(stack, { bucketArn })).toEqual(bucketArn); - 'produce arn from bucket name'(test: Test) { + }); + + test('produce arn from bucket name', () => { const stack = new cdk.Stack(); const bucketName = 'hello'; - test.deepEqual(stack.resolve(parseBucketArn(stack, { bucketName })), { + expect(stack.resolve(parseBucketArn(stack, { bucketName }))).toEqual({ 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':s3:::hello']], }); - test.done(); - }, - 'fails if neither arn nor name are provided'(test: Test) { + }); + + test('fails if neither arn nor name are provided', () => { const stack = new cdk.Stack(); - test.throws(() => parseBucketArn(stack, {}), /Cannot determine bucket ARN. At least `bucketArn` or `bucketName` is needed/); - test.done(); - }, - }, + expect(() => parseBucketArn(stack, {})).toThrow(/Cannot determine bucket ARN. At least `bucketArn` or `bucketName` is needed/); - parseBucketName: { + }); + }); - 'explicit name'(test: Test) { + describe('parseBucketName', () => { + + test('explicit name', () => { const stack = new cdk.Stack(); const bucketName = 'foo'; - test.deepEqual(stack.resolve(parseBucketName(stack, { bucketName })), 'foo'); - test.done(); - }, + expect(stack.resolve(parseBucketName(stack, { bucketName }))).toEqual('foo'); + + }); - 'extract bucket name from string arn'(test: Test) { + test('extract bucket name from string arn', () => { const stack = new cdk.Stack(); const bucketArn = 'arn:aws:s3:::my-bucket'; - test.deepEqual(stack.resolve(parseBucketName(stack, { bucketArn })), 'my-bucket'); - test.done(); - }, + expect(stack.resolve(parseBucketName(stack, { bucketArn }))).toEqual('my-bucket'); - 'can parse bucket name even if it contains a token'(test: Test) { + }); + + test('can parse bucket name even if it contains a token', () => { const stack = new cdk.Stack(); const bucketArn = `arn:aws:s3:::${cdk.Token.asString({ Ref: 'my-bucket' })}`; - test.deepEqual( + expect( stack.resolve(parseBucketName(stack, { bucketArn })), + ).toEqual( { Ref: 'my-bucket' }, ); - test.done(); - }, - 'fails if ARN has invalid format'(test: Test) { + }); + + test('fails if ARN has invalid format', () => { const stack = new cdk.Stack(); const bucketArn = 'invalid-arn'; - test.throws(() => parseBucketName(stack, { bucketArn }), /ARNs must/); - test.done(); - }, - }, + expect(() => parseBucketName(stack, { bucketArn })).toThrow(/ARNs must/); + + }); + }); }); From 01bf6e2922994e7d41c8c6b171aa1693835f2b53 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 30 Aug 2021 17:46:22 +0200 Subject: [PATCH 37/91] feat(core): normalize line endings in asset hash calculation (#16276) Replace CRLF with LF so asset hashes are identical across platforms. The hash still includes the size but it is now the size after converting line endings. Addresses #14555 (closes it?) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/core/lib/fs/fingerprint.ts | 52 ++++++++++++++++--- packages/@aws-cdk/core/test/fs/eol/lf.txt | 2 + .../core/test/fs/fs-fingerprint.test.ts | 24 +++++++++ 3 files changed, 71 insertions(+), 7 deletions(-) create mode 100644 packages/@aws-cdk/core/test/fs/eol/lf.txt diff --git a/packages/@aws-cdk/core/lib/fs/fingerprint.ts b/packages/@aws-cdk/core/lib/fs/fingerprint.ts index 50a81fd53982d..7ba7214109b2c 100644 --- a/packages/@aws-cdk/core/lib/fs/fingerprint.ts +++ b/packages/@aws-cdk/core/lib/fs/fingerprint.ts @@ -9,14 +9,18 @@ const BUFFER_SIZE = 8 * 1024; const CTRL_SOH = '\x01'; const CTRL_SOT = '\x02'; const CTRL_ETX = '\x03'; +const CR = '\r'; +const LF = '\n'; +const CRLF = `${CR}${LF}`; /** * Produces fingerprint based on the contents of a single file or an entire directory tree. * + * Line endings are converted from CRLF to LF. + * * The fingerprint will also include: * 1. An extra string if defined in `options.extra`. - * 2. The set of exclude patterns, if defined in `options.exclude` - * 3. The symlink follow mode value. + * 2. The symlink follow mode value. * * @param fileOrDirectory The directory or file to fingerprint * @param options Fingerprinting options @@ -60,7 +64,7 @@ export function fingerprint(fileOrDirectory: string, options: FingerprintOptions _hashField(hash, `link:${relativePath}`, linkTarget); } } else if (stat.isFile()) { - _hashField(hash, `file:${relativePath}`, _contentFingerprint(realPath, stat)); + _hashField(hash, `file:${relativePath}`, contentFingerprint(realPath)); } else if (stat.isDirectory()) { for (const item of fs.readdirSync(realPath).sort()) { _processFileOrDirectory(path.join(symbolicPath, item), false, path.join(realPath, item)); @@ -71,20 +75,54 @@ export function fingerprint(fileOrDirectory: string, options: FingerprintOptions } } -function _contentFingerprint(file: string, stat: fs.Stats): string { +export function contentFingerprint(file: string): string { const hash = crypto.createHash('sha256'); const buffer = Buffer.alloc(BUFFER_SIZE); // eslint-disable-next-line no-bitwise const fd = fs.openSync(file, fs.constants.O_DSYNC | fs.constants.O_RDONLY | fs.constants.O_SYNC); + let size = 0; + let isBinary = false; + let lastStr = ''; + let read = 0; try { - let read = 0; while ((read = fs.readSync(fd, buffer, 0, BUFFER_SIZE, null)) !== 0) { - hash.update(buffer.slice(0, read)); + const slicedBuffer = buffer.slice(0, read); + + // Detect if file is binary by checking the first 8k bytes for the + // null character (git like implementation) + if (size === 0) { + isBinary = slicedBuffer.indexOf(0) !== -1; + } + + let dataBuffer = slicedBuffer; + if (!isBinary) { // Line endings normalization (CRLF -> LF) + const str = buffer.slice(0, read).toString(); + + // We are going to normalize line endings to LF. So if the current + // buffer ends with CR, it could be that the next one starts with + // LF so we need to save it for later use. + if (new RegExp(`${CR}$`).test(str)) { + lastStr += str; + continue; + } + + const data = lastStr + str; + const normalizedData = data.replace(new RegExp(CRLF, 'g'), LF); + dataBuffer = Buffer.from(normalizedData); + lastStr = ''; + } + + size += dataBuffer.length; + hash.update(dataBuffer); + } + + if (lastStr) { + hash.update(Buffer.from(lastStr)); } } finally { fs.closeSync(fd); } - return `${stat.size}:${hash.digest('hex')}`; + return `${size}:${hash.digest('hex')}`; } function _hashField(hash: crypto.Hash, header: string, value: string | Buffer | DataView) { diff --git a/packages/@aws-cdk/core/test/fs/eol/lf.txt b/packages/@aws-cdk/core/test/fs/eol/lf.txt new file mode 100644 index 0000000000000..f41bc690ba927 --- /dev/null +++ b/packages/@aws-cdk/core/test/fs/eol/lf.txt @@ -0,0 +1,2 @@ +hello word +this a new line! diff --git a/packages/@aws-cdk/core/test/fs/fs-fingerprint.test.ts b/packages/@aws-cdk/core/test/fs/fs-fingerprint.test.ts index 6a589b3ba159e..be093c32cbffc 100644 --- a/packages/@aws-cdk/core/test/fs/fs-fingerprint.test.ts +++ b/packages/@aws-cdk/core/test/fs/fs-fingerprint.test.ts @@ -3,6 +3,7 @@ import * as os from 'os'; import * as path from 'path'; import { nodeunitShim, Test } from 'nodeunit-shim'; import { FileSystem, SymlinkFollowMode } from '../../lib/fs'; +import { contentFingerprint } from '../../lib/fs/fingerprint'; nodeunitShim({ files: { @@ -155,4 +156,27 @@ nodeunitShim({ test.done(); }, }, + + eol: { + 'normalizes line endings'(test: Test) { + // GIVEN + const lf = path.join(__dirname, 'eol', 'lf.txt'); + const crlf = path.join(__dirname, 'eol', 'crlf.txt'); + fs.writeFileSync(crlf, fs.readFileSync(lf, 'utf8').replace(/\n/g, '\r\n')); + + const lfStat = fs.statSync(lf); + const crlfStat = fs.statSync(crlf); + + // WHEN + const crlfHash = contentFingerprint(crlf); + const lfHash = contentFingerprint(lf); + + // THEN + test.notEqual(crlfStat.size, lfStat.size); // Difference in size due to different line endings + test.deepEqual(crlfHash, lfHash); // Same hash + + fs.unlinkSync(crlf); + test.done(); + }, + }, }); From f6fbcb8a4de611d2a6e8968665e893d9b67d3874 Mon Sep 17 00:00:00 2001 From: Ben Chaimberg Date: Mon, 30 Aug 2021 15:48:51 -0400 Subject: [PATCH 38/91] chore: upgrade jsii packages (#16184) Ran npm-check-updates and yarn upgrade to keep the `yarn.lock` file up-to-date. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- package.json | 8 +- tools/cdk-integ-tools/lib/integ-helpers.ts | 2 +- yarn.lock | 103 +++++++++++++++++++-- 3 files changed, 101 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index a18d46e63c20d..383ddf1c8e4c5 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,10 @@ "fs-extra": "^9.1.0", "graceful-fs": "^4.2.6", "jest-junit": "^12.2.0", - "jsii-diff": "^1.31.0", - "jsii-pacmak": "^1.31.0", - "jsii-reflect": "^1.31.0", - "jsii-rosetta": "^1.31.0", + "jsii-diff": "^1.34.0", + "jsii-pacmak": "^1.34.0", + "jsii-reflect": "^1.34.0", + "jsii-rosetta": "^1.34.0", "lerna": "^4.0.0", "patch-package": "^6.4.7", "standard-version": "^9.3.1", diff --git a/tools/cdk-integ-tools/lib/integ-helpers.ts b/tools/cdk-integ-tools/lib/integ-helpers.ts index bf52cbc4247cc..6b725d980a6f6 100644 --- a/tools/cdk-integ-tools/lib/integ-helpers.ts +++ b/tools/cdk-integ-tools/lib/integ-helpers.ts @@ -41,7 +41,7 @@ export class IntegrationTests { public async discover(): Promise { const files = await this.readTree(); const integs = files.filter(fileName => path.basename(fileName).startsWith('integ.') && path.basename(fileName).endsWith('.js')); - return await this.request(integs); + return this.request(integs); } public async request(files: string[]): Promise { diff --git a/yarn.lock b/yarn.lock index dc35a18bf0134..ddf6785bfb202 100644 --- a/yarn.lock +++ b/yarn.lock @@ -565,6 +565,14 @@ chalk "^4.1.2" semver "^7.3.5" +"@jsii/check-node@1.34.0": + version "1.34.0" + resolved "https://registry.yarnpkg.com/@jsii/check-node/-/check-node-1.34.0.tgz#24da38da36e18639c84787dabd464b8474ddee22" + integrity sha512-Z+eGyIoV6B6RNFCR+Z/p0ANnZA++bmCXhoU1RIwGh9RG39PAT38KkZZNr9ZHNTTQbVoTJMSatoX/9WQ33pQxAw== + dependencies: + chalk "^4.1.2" + semver "^7.3.5" + "@jsii/spec@^1.31.0": version "1.31.0" resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.31.0.tgz#9298dc163fdae0bab4006b817592235a29922871" @@ -572,6 +580,13 @@ dependencies: jsonschema "^1.4.0" +"@jsii/spec@^1.34.0": + version "1.34.0" + resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.34.0.tgz#8b78adf07518f567d2c44bf6c98c426e5c18e0cf" + integrity sha512-yAK8FrTRrZ3lQ+DmdyAFZuHmsTJ1ej0719+sVgjr5ahE9i64huStaraX/jJM+PniuUQwE7N+B49ue6X9qj7vJA== + dependencies: + jsonschema "^1.4.0" + "@lerna/add@4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@lerna/add/-/add-4.0.0.tgz#c36f57d132502a57b9e7058d1548b7a565ef183f" @@ -1886,6 +1901,11 @@ "@typescript-eslint/types" "4.28.4" eslint-visitor-keys "^2.0.0" +"@xmldom/xmldom@^0.7.0": + version "0.7.3" + resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.3.tgz#55de695f77afd3cc0e5bee0aa900040bc63c0f63" + integrity sha512-8XmJdPut2XGtfFcsNsqEsvMUmAwk7xLq7m+E/GcsU9b5qyFFIsiX4Fvnb5UoQ4wo12Wlm07YFJERoyWUYdbIpw== + "@yarnpkg/lockfile@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" @@ -2843,6 +2863,15 @@ codemaker@^1.31.0: decamelize "^5.0.0" fs-extra "^9.1.0" +codemaker@^1.34.0: + version "1.34.0" + resolved "https://registry.yarnpkg.com/codemaker/-/codemaker-1.34.0.tgz#dba5dbd9ca6d1d9d9af64d64f17bde45882b0aa0" + integrity sha512-NHwy6TxMh21ygch7+K/OwtdN3BjxhAMoP5QXqzkkR0TDP2kEdKCNc31EChz3Xcmxk1qkdJN5CpXMnLjo7f07sQ== + dependencies: + camelcase "^6.2.0" + decamelize "^5.0.0" + fs-extra "^9.1.0" + collect-v8-coverage@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" @@ -2930,6 +2959,16 @@ commonmark@^0.29.3: minimist ">=1.2.2" string.prototype.repeat "^0.2.0" +commonmark@^0.30.0: + version "0.30.0" + resolved "https://registry.yarnpkg.com/commonmark/-/commonmark-0.30.0.tgz#38811dc7bbf0f59d277ae09054d4d73a332f2e45" + integrity sha512-j1yoUo4gxPND1JWV9xj5ELih0yMv1iCWDG6eEQIPLSWLxzCXiFoyS7kvB+WwU+tZMf4snwJMMtaubV0laFpiBA== + dependencies: + entities "~2.0" + mdurl "~1.0.1" + minimist ">=1.2.2" + string.prototype.repeat "^0.2.0" + compare-func@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/compare-func/-/compare-func-2.0.0.tgz#fb65e75edbddfd2e568554e8b5b05fff7a51fcb3" @@ -6007,16 +6046,17 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -jsii-diff@^1.31.0: - version "1.31.0" - resolved "https://registry.yarnpkg.com/jsii-diff/-/jsii-diff-1.31.0.tgz#7f32b340cf340cc1929f4d534bdfa6495fc09bed" - integrity sha512-eEKFfZXGXxlWFg7E0F4h2UGOnpVCzHclM586SE4KnMwHzSlpRrdYrXa2KhFQSLs/gpZofDV4rPLZ9UDLvNu75Q== +jsii-diff@^1.34.0: + version "1.34.0" + resolved "https://registry.yarnpkg.com/jsii-diff/-/jsii-diff-1.34.0.tgz#86742f21f5ab809df2ce8b6902f390c6379a9a39" + integrity sha512-fPVBoixNjo3kQs8WgjNeE7M5HxcK3jqfgM/IC6fpzrsGDjjqnH7emtVH3gvS18uQxUfYKKkXEKbkHsywQe6EkA== dependencies: - "@jsii/spec" "^1.31.0" + "@jsii/check-node" "1.34.0" + "@jsii/spec" "^1.34.0" fs-extra "^9.1.0" - jsii-reflect "^1.31.0" + jsii-reflect "^1.34.0" log4js "^6.3.0" - typescript "~3.9.9" + typescript "~3.9.10" yargs "^16.2.0" jsii-pacmak@^1.31.0: @@ -6037,6 +6077,25 @@ jsii-pacmak@^1.31.0: xmlbuilder "^15.1.1" yargs "^16.2.0" +jsii-pacmak@^1.34.0: + version "1.34.0" + resolved "https://registry.yarnpkg.com/jsii-pacmak/-/jsii-pacmak-1.34.0.tgz#7fc6a79fb72bd791a0cac5877339253fd26e14e3" + integrity sha512-OngbNHieb5g7B1VkRSZkZq1vgoflhjX4heTJnQJZYbG59j2qVgD7E/o/Dl2OTBLrGRms8e2oCsYc7XROt2htSA== + dependencies: + "@jsii/check-node" "1.34.0" + "@jsii/spec" "^1.34.0" + clone "^2.1.2" + codemaker "^1.34.0" + commonmark "^0.30.0" + escape-string-regexp "^4.0.0" + fs-extra "^9.1.0" + jsii-reflect "^1.34.0" + jsii-rosetta "^1.34.0" + semver "^7.3.5" + spdx-license-list "^6.4.0" + xmlbuilder "^15.1.1" + yargs "^16.2.0" + jsii-reflect@^1.31.0: version "1.31.0" resolved "https://registry.yarnpkg.com/jsii-reflect/-/jsii-reflect-1.31.0.tgz#83acdae835071c734bb8847cf3cad7ccc4497540" @@ -6048,6 +6107,18 @@ jsii-reflect@^1.31.0: oo-ascii-tree "^1.31.0" yargs "^16.2.0" +jsii-reflect@^1.34.0: + version "1.34.0" + resolved "https://registry.yarnpkg.com/jsii-reflect/-/jsii-reflect-1.34.0.tgz#3d5c8f7c2e8310df2c8dea3aad5bef487fe1d0d9" + integrity sha512-IOEdwgeDCOq821PM3OfRro1Pgu0QzHFW7zQy3aN7/w5Fcb/tSYGxI9+Ykr6JCdg681LFzcMEgwJpCUHnfi/shw== + dependencies: + "@jsii/check-node" "1.34.0" + "@jsii/spec" "^1.34.0" + colors "^1.4.0" + fs-extra "^9.1.0" + oo-ascii-tree "^1.34.0" + yargs "^16.2.0" + jsii-rosetta@^1.31.0: version "1.31.0" resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.31.0.tgz#f5174b532b4c3a79eadd9ed059aa33bee21e3225" @@ -6060,6 +6131,19 @@ jsii-rosetta@^1.31.0: xmldom "^0.6.0" yargs "^16.2.0" +jsii-rosetta@^1.34.0: + version "1.34.0" + resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.34.0.tgz#64b1233726a98a992be5cffd1d0f4b824346dbef" + integrity sha512-GOGAy5b+zCGeyYziBoNVXgamL2CEZKMj5moeemkyN4AUHUqugNk3fSul2Zdbxs2S13Suk0D9iYAgChDxew0bOw== + dependencies: + "@jsii/check-node" "1.34.0" + "@jsii/spec" "^1.34.0" + "@xmldom/xmldom" "^0.7.0" + commonmark "^0.30.0" + fs-extra "^9.1.0" + typescript "~3.9.10" + yargs "^16.2.0" + jsii@^1.31.0: version "1.31.0" resolved "https://registry.yarnpkg.com/jsii/-/jsii-1.31.0.tgz#513ff04581eae233accef2e2ce06a19d9bd4d972" @@ -7481,6 +7565,11 @@ oo-ascii-tree@^1.31.0: resolved "https://registry.yarnpkg.com/oo-ascii-tree/-/oo-ascii-tree-1.31.0.tgz#36e10dcad35ba767db41c2d2050ff2174f3d5e6f" integrity sha512-gNb2MyP1ZcF7cX0WgsAjYe4gZcx7BMLBWKE2TJZZbQ9/j4D8gbJh5Aq6RlXBgev74ODlgAVVcPr2wKU4Dufhqg== +oo-ascii-tree@^1.34.0: + version "1.34.0" + resolved "https://registry.yarnpkg.com/oo-ascii-tree/-/oo-ascii-tree-1.34.0.tgz#5528a52d92ef18b3860d0e784383007e476c18b3" + integrity sha512-gAY+yfKCskAk7mkfI8nOhkP12iTGE7b8UxnQuscN80vghrozt/E/2rLeKKMJFagJlm/NnnUmBA0tBQZ3oPHEKg== + open@^7.4.2: version "7.4.2" resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" From ab44d42e85ab432e1f5ca426f2652e0e94768a9e Mon Sep 17 00:00:00 2001 From: Eli Polonsky Date: Mon, 30 Aug 2021 14:24:57 -0700 Subject: [PATCH 39/91] chore: upgrade jsii version (#16285) Following https://github.com/aws/aws-cdk/pull/16184 - there are more places to upgrade jsii in. This addresses https://github.com/aws/aws-cdk/security/dependabot/yarn.lock/xmldom/open ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- tools/cdk-build-tools/package.json | 6 +-- yarn.lock | 64 +++++++++--------------------- 2 files changed, 22 insertions(+), 48 deletions(-) diff --git a/tools/cdk-build-tools/package.json b/tools/cdk-build-tools/package.json index c4c3df70fc154..91fad0c39faf3 100644 --- a/tools/cdk-build-tools/package.json +++ b/tools/cdk-build-tools/package.json @@ -55,9 +55,9 @@ "fs-extra": "^9.1.0", "jest": "^26.6.3", "jest-junit": "^11.1.0", - "jsii": "^1.31.0", - "jsii-pacmak": "^1.31.0", - "jsii-reflect": "^1.31.0", + "jsii": "^1.34.0", + "jsii-pacmak": "^1.34.0", + "jsii-reflect": "^1.34.0", "markdownlint-cli": "^0.27.1", "nodeunit": "^0.11.3", "nyc": "^15.1.0", diff --git a/yarn.lock b/yarn.lock index ddf6785bfb202..c553079426b7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2949,16 +2949,6 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= -commonmark@^0.29.3: - version "0.29.3" - resolved "https://registry.yarnpkg.com/commonmark/-/commonmark-0.29.3.tgz#bb1d5733bfe3ea213b412f33f16439cc12999c2c" - integrity sha512-fvt/NdOFKaL2gyhltSy6BC4LxbbxbnPxBMl923ittqO/JBM0wQHaoYZliE4tp26cRxX/ZZtRsJlZzQrVdUkXAA== - dependencies: - entities "~2.0" - mdurl "~1.0.1" - minimist ">=1.2.2" - string.prototype.repeat "^0.2.0" - commonmark@^0.30.0: version "0.30.0" resolved "https://registry.yarnpkg.com/commonmark/-/commonmark-0.30.0.tgz#38811dc7bbf0f59d277ae09054d4d73a332f2e45" @@ -6059,24 +6049,6 @@ jsii-diff@^1.34.0: typescript "~3.9.10" yargs "^16.2.0" -jsii-pacmak@^1.31.0: - version "1.31.0" - resolved "https://registry.yarnpkg.com/jsii-pacmak/-/jsii-pacmak-1.31.0.tgz#7e4fa67f1de582be04263904aa45966d84210996" - integrity sha512-fGiAoooRPMadwTWU0vfHJdcNzeYdESnkU/8LmlI4k6yF1iIlFMIbWPulBxP6fV7SqV3CZQKGpUbcPD/Uzf1glg== - dependencies: - "@jsii/spec" "^1.31.0" - clone "^2.1.2" - codemaker "^1.31.0" - commonmark "^0.29.3" - escape-string-regexp "^4.0.0" - fs-extra "^9.1.0" - jsii-reflect "^1.31.0" - jsii-rosetta "^1.31.0" - semver "^7.3.5" - spdx-license-list "^6.4.0" - xmlbuilder "^15.1.1" - yargs "^16.2.0" - jsii-pacmak@^1.34.0: version "1.34.0" resolved "https://registry.yarnpkg.com/jsii-pacmak/-/jsii-pacmak-1.34.0.tgz#7fc6a79fb72bd791a0cac5877339253fd26e14e3" @@ -6119,18 +6091,6 @@ jsii-reflect@^1.34.0: oo-ascii-tree "^1.34.0" yargs "^16.2.0" -jsii-rosetta@^1.31.0: - version "1.31.0" - resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.31.0.tgz#f5174b532b4c3a79eadd9ed059aa33bee21e3225" - integrity sha512-Heu6D+yI5mmUklLQdX3PdDvHUQm14618Fj4PQM9seKa4cohxzJ7EHopfRObKYHMko9awopx4Qr7Gtu6u/QPqfw== - dependencies: - "@jsii/spec" "^1.31.0" - commonmark "^0.29.3" - fs-extra "^9.1.0" - typescript "~3.9.9" - xmldom "^0.6.0" - yargs "^16.2.0" - jsii-rosetta@^1.34.0: version "1.34.0" resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.34.0.tgz#64b1233726a98a992be5cffd1d0f4b824346dbef" @@ -6162,6 +6122,25 @@ jsii@^1.31.0: typescript "~3.9.9" yargs "^16.2.0" +jsii@^1.34.0: + version "1.34.0" + resolved "https://registry.yarnpkg.com/jsii/-/jsii-1.34.0.tgz#d009d7ae36f069819f97dcade4203722a5ec524f" + integrity sha512-z/p8cuWdRntQzdZ1Fq/hvXHPjq/HjZhQzTF/GmYrH3s7Wsb14LphHGAENTZwICBaSovoqSRIboOb2FbPLsCjoA== + dependencies: + "@jsii/check-node" "1.34.0" + "@jsii/spec" "^1.34.0" + case "^1.6.3" + colors "^1.4.0" + deep-equal "^2.0.5" + fs-extra "^9.1.0" + log4js "^6.3.0" + semver "^7.3.5" + semver-intersect "^1.4.0" + sort-json "^2.0.0" + spdx-license-list "^6.4.0" + typescript "~3.9.10" + yargs "^16.2.0" + json-diff@^0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/json-diff/-/json-diff-0.5.4.tgz#7bc8198c441756632aab66c7d9189d365a7a035a" @@ -10209,11 +10188,6 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xmldom@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.6.0.tgz#43a96ecb8beece991cef382c08397d82d4d0c46f" - integrity sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg== - xregexp@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943" From d210bb8b657e1a611074c583946798a5085dcbe1 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Mon, 30 Aug 2021 15:54:57 -0700 Subject: [PATCH 40/91] chore: assign AppMesh issues to Seiya6329 (#16248) --- .github/workflows/issue-label-assign.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue-label-assign.yml b/.github/workflows/issue-label-assign.yml index 64e3daf97887b..9451bd1b960ae 100644 --- a/.github/workflows/issue-label-assign.yml +++ b/.github/workflows/issue-label-assign.yml @@ -37,7 +37,7 @@ jobs: {"keywords":["(@aws-cdk/aws-appintegrations)","(aws-appintegrations)","(appintegrations)"],"labels":["@aws-cdk/aws-appintegrations"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-applicationautoscaling)","(aws-applicationautoscaling)","(applicationautoscaling)","(application autoscaling)","(application-autoscaling)"],"labels":["@aws-cdk/aws-applicationautoscaling"],"assignees":["comcalvi"]}, {"keywords":["(@aws-cdk/aws-applicationinsights)","(aws-applicationinsights)","(applicationinsights)","(application insights)","(application-insights)"],"labels":["@aws-cdk/aws-applicationinsights"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-appmesh)","(aws-appmesh)","(appmesh)","(app mesh)","(app-mesh)"],"labels":["@aws-cdk/aws-appmesh"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-appmesh)","(aws-appmesh)","(appmesh)","(app mesh)","(app-mesh)"],"labels":["@aws-cdk/aws-appmesh"],"assignees":["Seiya6329"]}, {"keywords":["(@aws-cdk/aws-appstream)","(aws-appstream)","(appstream)","(app stream)","(app-stream)"],"labels":["@aws-cdk/aws-appstream"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-appsync)","(aws-appsync)","(appsync)","(app sync)","(app-sync)"],"labels":["@aws-cdk/aws-appsync"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-athena)","(aws-athena)","(athena)"],"labels":["@aws-cdk/aws-athena"],"assignees":["BenChaimberg"]}, From 41b831a6698ee6c7a3c8968bff8273a0c7f35448 Mon Sep 17 00:00:00 2001 From: Calvin Combs <66279577+comcalvi@users.noreply.github.com> Date: Mon, 30 Aug 2021 17:47:29 -0700 Subject: [PATCH 41/91] fix(aws-rds): fromDatabaseInstanceAttributes incorrectly stringifies ports with tokens (#16286) Closes #11813. Fixes `port` with tokens being incorrectly stringified in `fromDatabaseInstanceAttributes()`. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-rds/lib/instance.ts | 4 +-- .../@aws-cdk/aws-rds/test/instance.test.ts | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-rds/lib/instance.ts b/packages/@aws-cdk/aws-rds/lib/instance.ts index 20073ad021b40..6ea2418ee1a97 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance.ts @@ -5,7 +5,7 @@ import * as kms from '@aws-cdk/aws-kms'; import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; -import { ArnComponents, Duration, FeatureFlags, IResource, Lazy, RemovalPolicy, Resource, Stack, Token } from '@aws-cdk/core'; +import { ArnComponents, Duration, FeatureFlags, IResource, Lazy, RemovalPolicy, Resource, Stack, Token, Tokenization } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { DatabaseSecret } from './database-secret'; @@ -124,7 +124,7 @@ export abstract class DatabaseInstanceBase extends Resource implements IDatabase }); public readonly instanceIdentifier = attrs.instanceIdentifier; public readonly dbInstanceEndpointAddress = attrs.instanceEndpointAddress; - public readonly dbInstanceEndpointPort = attrs.port.toString(); + public readonly dbInstanceEndpointPort = Tokenization.stringifyNumber(attrs.port); public readonly instanceEndpoint = new Endpoint(attrs.instanceEndpointAddress, attrs.port); public readonly engine = attrs.engine; protected enableIamAuthentication = true; diff --git a/packages/@aws-cdk/aws-rds/test/instance.test.ts b/packages/@aws-cdk/aws-rds/test/instance.test.ts index 4621a5bf6512d..f0591eb2469d1 100644 --- a/packages/@aws-cdk/aws-rds/test/instance.test.ts +++ b/packages/@aws-cdk/aws-rds/test/instance.test.ts @@ -401,6 +401,35 @@ describe('instance', () => { }); + + test('can create a new database instance with fromDatabaseInstanceAttributes using a token for the port', () => { + // GIVEN + const databasePort = new cdk.CfnParameter(stack, 'DatabasePort', { + type: 'Number', + default: 5432, + }).valueAsNumber; + + // WHEN + const instance = rds.DatabaseInstance.fromDatabaseInstanceAttributes(stack, 'DatabaseInstance', { + instanceIdentifier: '', + securityGroups: [], + instanceEndpointAddress: '', + port: databasePort, + }); + + new cdk.CfnOutput(stack, 'portOutput', { + exportName: 'databaseUrl', + value: `${instance.dbInstanceEndpointPort}`, + }); + + // THEN + expect(stack).toHaveOutput({ + exportName: 'databaseUrl', + outputValue: { + Ref: 'DatabasePort', + }, + }); + }); }); test('create a read replica in the same region - with the subnet group name', () => { From ffdcd94405c160763e396a191d5af793ac8db998 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 31 Aug 2021 12:02:29 +0200 Subject: [PATCH 42/91] feat(route53resolver): DNS Firewall (#15031) Add L2s for `FirewallDomainList`, `FirewallRuleGroup` and `FirewallRuleGroupAssociation`. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-route53resolver/README.md | 93 ++++- .../lib/firewall-domain-list.ts | 234 +++++++++++++ .../lib/firewall-rule-group-association.ts | 129 +++++++ .../lib/firewall-rule-group.ts | 277 +++++++++++++++ .../@aws-cdk/aws-route53resolver/lib/index.ts | 4 + .../@aws-cdk/aws-route53resolver/package.json | 16 +- .../aws-route53resolver/test/domains.txt | 4 + .../test/firewall-domain-list.test.ts | 123 +++++++ .../test/firewall-rule-group.test.ts | 137 ++++++++ .../test/integ.firewall.expected.json | 321 ++++++++++++++++++ .../test/integ.firewall.ts | 46 +++ .../test/route53resolver.test.ts | 6 - 12 files changed, 1382 insertions(+), 8 deletions(-) create mode 100644 packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts create mode 100644 packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group-association.ts create mode 100644 packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group.ts create mode 100644 packages/@aws-cdk/aws-route53resolver/test/domains.txt create mode 100644 packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts create mode 100644 packages/@aws-cdk/aws-route53resolver/test/firewall-rule-group.test.ts create mode 100644 packages/@aws-cdk/aws-route53resolver/test/integ.firewall.expected.json create mode 100644 packages/@aws-cdk/aws-route53resolver/test/integ.firewall.ts delete mode 100644 packages/@aws-cdk/aws-route53resolver/test/route53resolver.test.ts diff --git a/packages/@aws-cdk/aws-route53resolver/README.md b/packages/@aws-cdk/aws-route53resolver/README.md index 9cf4ab7748b3d..a3bf5946ead7d 100644 --- a/packages/@aws-cdk/aws-route53resolver/README.md +++ b/packages/@aws-cdk/aws-route53resolver/README.md @@ -9,10 +9,101 @@ > > [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + --- +## DNS Firewall + +With Route 53 Resolver DNS Firewall, you can filter and regulate outbound DNS traffic for your +virtual private connections (VPCs). To do this, you create reusable collections of filtering rules +in DNS Firewall rule groups and associate the rule groups to your VPC. + +DNS Firewall provides protection for outbound DNS requests from your VPCs. These requests route +through Resolver for domain name resolution. A primary use of DNS Firewall protections is to help +prevent DNS exfiltration of your data. DNS exfiltration can happen when a bad actor compromises +an application instance in your VPC and then uses DNS lookup to send data out of the VPC to a domain +that they control. With DNS Firewall, you can monitor and control the domains that your applications +can query. You can deny access to the domains that you know to be bad and allow all other queries +to pass through. Alternately, you can deny access to all domains except for the ones that you +explicitly trust. + +### Domain lists + +Domain lists can be created using a list of strings, a text file stored in Amazon S3 or a local +text file: + +```ts +const blockList = new route53resolver.FirewallDomainList(this, 'BlockList', { + domains: route53resolver.FirewallDomains.fromList(['bad-domain.com', 'bot-domain.net']), +}); + +const s3List = new route53resolver.FirewallDomainList(this, 'S3List', { + domains: route53resolver.FirewallDomains.fromS3Url('s3://bucket/prefix/object'), +}); + +const assetList = new route53resolver.FirewallDomainList(this, 'AssetList', { + domains: route53resolver.FirewallDomains.fromAsset('/path/to/domains.txt'), +}); +``` + +The file must be a text file and must contain a single domain per line. + +Use `FirewallDomainList.fromFirewallDomainListId()` to import an existing or [AWS managed domain list](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resolver-dns-firewall-managed-domain-lists.html): + +```ts +// AWSManagedDomainsMalwareDomainList in us-east-1 +const malwareList = route53resolver.FirewallDomainList.fromFirewallDomainListId(this, 'Malware', 'rslvr-fdl-2c46f2ecbfec4dcc'); +``` + +### Rule group + +Create a rule group: + +```ts +new route53resolver.FirewallRuleGroup(this, 'RuleGroup', { + rules: [ + { + priority: 10, + firewallDomainList: myBlockList, + // block and reply with NODATA + action: route53resolver.FirewallRuleAction.block(), + }, + ], +}); +``` + +Rules can be added at construction time or using `addRule()`: + +```ts +ruleGroup.addRule({ + priority: 10, + firewallDomainList: blockList, + // block and reply with NXDOMAIN + action: route53resolver.FirewallRuleAction.block(route53resolver.DnsBlockResponse.nxDomain()), +}); + +ruleGroup.addRule({ + priority: 20, + firewallDomainList: blockList, + // block and override DNS response with a custom domain + action: route53resolver.FirewallRuleAction.block(route53resolver.DnsBlockResponse.override('amazon.com')), +}); +``` + +Use `associate()` to associate a rule group with a VPC: + ```ts -import * as route53resolver from '@aws-cdk/aws-route53resolver'; +ruleGroup.associate({ + priority: 101, + vpc: myVpc, +}) ``` diff --git a/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts b/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts new file mode 100644 index 0000000000000..a6303b2c78114 --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts @@ -0,0 +1,234 @@ +import * as path from 'path'; +import { IBucket } from '@aws-cdk/aws-s3'; +import { Asset } from '@aws-cdk/aws-s3-assets'; +import { IResource, Resource, Token } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnFirewallDomainList } from './route53resolver.generated'; + +/** + * A Firewall Domain List + */ +export interface IFirewallDomainList extends IResource { + /** + * The ID of the domain list + * + * @attribute + */ + readonly firewallDomainListId: string; +} + +/** + * Properties for a Firewall Domain List + */ +export interface FirewallDomainListProps { + /** + * A name for the domain list + * + * @default - a CloudFormation generated name + */ + readonly name?: string; + + /** + * A list of domains + */ + readonly domains: FirewallDomains; +} + +/** + * A list of domains + */ +export abstract class FirewallDomains { + /** + * Firewall domains created from a list of domains + * + * @param list the list of domains + */ + public static fromList(list: string[]): FirewallDomains { + for (const domain of list) { + if (!/^[\w-.]+$/.test(domain)) { + throw new Error(`Invalid domain: ${domain}. Valid characters: A-Z, a-z, 0-9, _, -, .`); + } + } + + return { + bind(_scope: Construct): DomainsConfig { + return { domains: list }; + }, + }; + } + + /** + * Firewall domains created from the URL of a file stored in Amazon S3. + * The file must be a text file and must contain a single domain per line. + * The content type of the S3 object must be `plain/text`. + * + * @param url S3 bucket url (s3://bucket/prefix/objet). + */ + public static fromS3Url(url: string): FirewallDomains { + if (!Token.isUnresolved(url) && !url.startsWith('s3://')) { + throw new Error(`The S3 URL must start with s3://, got ${url}`); + } + + return { + bind(_scope: Construct): DomainsConfig { + return { domainFileUrl: url }; + }, + }; + } + + /** + * Firewall domains created from a file stored in Amazon S3. + * The file must be a text file and must contain a single domain per line. + * The content type of the S3 object must be `plain/text`. + * + * @param bucket S3 bucket + * @param key S3 key + */ + public static fromS3(bucket: IBucket, key: string): FirewallDomains { + return this.fromS3Url(bucket.s3UrlForObject(key)); + } + + /** + * Firewall domains created from a local disk path to a text file. + * The file must be a text file (`.txt` extension) and must contain a single + * domain per line. It will be uploaded to S3. + * + * @param assetPath path to the text file + */ + public static fromAsset(assetPath: string): FirewallDomains { + // cdk-assets will correctly set the content type for the S3 object + // if the file has the correct extension + if (path.extname(assetPath) !== '.txt') { + throw new Error(`FirewallDomains.fromAsset() expects a file with the .txt extension, got ${assetPath}`); + } + + return { + bind(scope: Construct): DomainsConfig { + const asset = new Asset(scope, 'Domains', { path: assetPath }); + + if (!asset.isFile) { + throw new Error('FirewallDomains.fromAsset() expects a file'); + } + + return { domainFileUrl: asset.s3ObjectUrl }; + }, + }; + + } + + /** Binds the domains to a domain list */ + public abstract bind(scope: Construct): DomainsConfig; +} + +/** + * Domains configuration + */ +export interface DomainsConfig { + /** + * The fully qualified URL or URI of the file stored in Amazon S3 that contains + * the list of domains to import. The file must be a text file and must contain + * a single domain per line. The content type of the S3 object must be `plain/text`. + * + * @default - use `domains` + */ + readonly domainFileUrl?: string; + + /** + * A list of domains + * + * @default - use `domainFileUrl` + */ + readonly domains?: string[]; +} + +/** + * A Firewall Domain List + */ +export class FirewallDomainList extends Resource implements IFirewallDomainList { + /** + * Import an existing Firewall Rule Group + */ + public static fromFirewallDomainListId(scope: Construct, id: string, firewallDomainListId: string): IFirewallDomainList { + class Import extends Resource implements IFirewallDomainList { + public readonly firewallDomainListId = firewallDomainListId; + } + return new Import(scope, id); + } + + public readonly firewallDomainListId: string; + + /** + * The ARN (Amazon Resource Name) of the domain list + * @attribute + */ + public readonly firewallDomainListArn: string; + + /** + * The date and time that the domain list was created + * @attribute + */ + public readonly firewallDomainListCreationTime: string; + + /** + * The creator request ID + * @attribute + */ + public readonly firewallDomainListCreatorRequestId: string; + + /** + * The number of domains in the list + * @attribute + */ + public readonly firewallDomainListDomainCount: number; + + /** + * The owner of the list, used only for lists that are not managed by you. + * For example, the managed domain list `AWSManagedDomainsMalwareDomainList` + * has the managed owner name `Route 53 Resolver DNS Firewall`. + * @attribute + */ + public readonly firewallDomainListManagedOwnerName: string; + + /** + * The date and time that the domain list was last modified + * @attribute + */ + public readonly firewallDomainListModificationTime: string; + + /** + * The status of the domain list + * @attribute + */ + public readonly firewallDomainListStatus: string; + + /** + * Additional information about the status of the rule group + * @attribute + */ + public readonly firewallDomainListStatusMessage: string; + + constructor(scope: Construct, id: string, props: FirewallDomainListProps) { + super(scope, id); + + if (props.name && !Token.isUnresolved(props.name) && !/^[\w-.]{1,128}$/.test(props.name)) { + throw new Error(`Invalid domain list name: ${props.name}. The name must have 1-128 characters. Valid characters: A-Z, a-z, 0-9, _, -, .`); + } + + const domainsConfig = props.domains.bind(this); + const domainList = new CfnFirewallDomainList(this, 'Resource', { + name: props.name, + domainFileUrl: domainsConfig.domainFileUrl, + domains: domainsConfig.domains, + }); + + this.firewallDomainListId = domainList.attrId; + this.firewallDomainListArn = domainList.attrArn; + this.firewallDomainListCreationTime = domainList.attrCreationTime; + this.firewallDomainListCreatorRequestId = domainList.attrCreatorRequestId; + this.firewallDomainListDomainCount = domainList.attrDomainCount; + this.firewallDomainListManagedOwnerName = domainList.attrManagedOwnerName; + this.firewallDomainListModificationTime = domainList.attrModificationTime; + this.firewallDomainListStatus = domainList.attrStatus; + this.firewallDomainListStatusMessage = domainList.attrStatusMessage; + } +} diff --git a/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group-association.ts b/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group-association.ts new file mode 100644 index 0000000000000..10281eba1dda1 --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group-association.ts @@ -0,0 +1,129 @@ +import { IVpc } from '@aws-cdk/aws-ec2'; +import { Resource, Token } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { IFirewallRuleGroup } from './firewall-rule-group'; +import { CfnFirewallRuleGroupAssociation } from './route53resolver.generated'; + +/** + * Options for a Firewall Rule Group Association + */ +export interface FirewallRuleGroupAssociationOptions { + /** + * If enabled, this setting disallows modification or removal of the + * association, to help prevent against accidentally altering DNS firewall + * protections. + * + * @default true + */ + readonly mutationProtection?: boolean; + + /** + * The name of the association + * + * @default - a CloudFormation generated name + */ + readonly name?: string; + + /** + * The setting that determines the processing order of the rule group among + * the rule groups that are associated with a single VPC. DNS Firewall filters VPC + * traffic starting from rule group with the lowest numeric priority setting. + * + * This value must be greater than 100 and less than 9,000 + */ + readonly priority: number; + + /** + * The VPC that to associate with the rule group. + */ + readonly vpc: IVpc; +} + +/** + * Properties for a Firewall Rule Group Association + */ +export interface FirewallRuleGroupAssociationProps extends FirewallRuleGroupAssociationOptions { + /** + * The firewall rule group which must be associated + */ + readonly firewallRuleGroup: IFirewallRuleGroup; +} + +/** + * A Firewall Rule Group Association + */ +export class FirewallRuleGroupAssociation extends Resource { + /** + * The ARN (Amazon Resource Name) of the association + * @attribute + */ + public readonly firewallRuleGroupAssociationArn: string; + + /** + * The date and time that the association was created + * @attribute + */ + public readonly firewallRuleGroupAssociationCreationTime: string; + + /** + * The creator request ID + * @attribute + */ + public readonly firewallRuleGroupAssociationCreatorRequestId: string; + + /** + * The ID of the association + * + * @attribute + */ + public readonly firewallRuleGroupAssociationId: string; + + /** + * The owner of the association, used only for lists that are not managed by you. + * If you use AWS Firewall Manager to manage your firewallls from DNS Firewall, + * then this reports Firewall Manager as the managed owner. + * @attribute + */ + public readonly firewallRuleGroupAssociationManagedOwnerName: string; + + /** + * The date and time that the association was last modified + * @attribute + */ + public readonly firewallRuleGroupAssociationModificationTime: string; + + /** + * The status of the association + * @attribute + */ + public readonly firewallRuleGroupAssociationStatus: string; + + /** + * Additional information about the status of the association + * @attribute + */ + public readonly firewallRuleGroupAssociationStatusMessage: string; + + constructor(scope: Construct, id: string, props: FirewallRuleGroupAssociationProps) { + super(scope, id); + + if (!Token.isUnresolved(props.priority) && (props.priority <= 100 || props.priority >= 9000)) { + throw new Error(`Priority must be greater than 100 and less than 9000, got ${props.priority}`); + } + + const association = new CfnFirewallRuleGroupAssociation(this, 'Resource', { + firewallRuleGroupId: props.firewallRuleGroup.firewallRuleGroupId, + priority: props.priority, + vpcId: props.vpc.vpcId, + }); + + this.firewallRuleGroupAssociationArn = association.attrArn; + this.firewallRuleGroupAssociationCreationTime = association.attrCreationTime; + this.firewallRuleGroupAssociationCreatorRequestId = association.attrCreatorRequestId; + this.firewallRuleGroupAssociationId = association.attrId; + this.firewallRuleGroupAssociationManagedOwnerName = association.attrManagedOwnerName; + this.firewallRuleGroupAssociationModificationTime = association.attrModificationTime; + this.firewallRuleGroupAssociationStatus = association.attrStatus; + this.firewallRuleGroupAssociationStatusMessage = association.attrStatusMessage; + } +} diff --git a/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group.ts b/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group.ts new file mode 100644 index 0000000000000..4346e23e46435 --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group.ts @@ -0,0 +1,277 @@ +import { Duration, IResource, Lazy, Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { IFirewallDomainList } from './firewall-domain-list'; +import { FirewallRuleGroupAssociation, FirewallRuleGroupAssociationOptions } from './firewall-rule-group-association'; +import { CfnFirewallRuleGroup } from './route53resolver.generated'; + +/** + * A Firewall Rule Group + */ +export interface IFirewallRuleGroup extends IResource { + /** + * The ID of the rule group + * + * @attribute + */ + readonly firewallRuleGroupId: string; +} + +/** + * Properties for a Firewall Rule Group + */ +export interface FirewallRuleGroupProps { + /** + * The name of the rule group. + * + * @default - a CloudFormation generated name + */ + readonly name?: string; + + /** + * A list of rules for this group + * + * @default - no rules + */ + readonly rules?: FirewallRule[]; +} + +/** + * A Firewall Rule + */ +export interface FirewallRule { + /** + * The action for this rule + */ + readonly action: FirewallRuleAction; + + /** + * The domain list for this rule + */ + readonly firewallDomainList: IFirewallDomainList; + + /** + * The priority of the rule in the rule group. This value must be unique within + * the rule group. + */ + readonly priority: number; +} + +/** + * A Firewall Rule + */ +export abstract class FirewallRuleAction { + /** + * Permit the request to go through + */ + public static allow(): FirewallRuleAction { + return { action: 'ALLOW' }; + } + + /** + * Permit the request to go through but send an alert to the logs + */ + public static alert(): FirewallRuleAction { + return { action: 'ALERT' }; + } + + /** + * Disallow the request + * + * @param [response=DnsBlockResponse.noData()] The way that you want DNS Firewall to block the request + */ + public static block(response?: DnsBlockResponse): FirewallRuleAction { + return { + action: 'BLOCK', + blockResponse: response ?? DnsBlockResponse.noData(), + }; + } + + /** + * The action that DNS Firewall should take on a DNS query when it matches + * one of the domains in the rule's domain list + */ + public abstract readonly action: string; + + /** + * The way that you want DNS Firewall to block the request + */ + public abstract readonly blockResponse?: DnsBlockResponse; +} + +/** + * The way that you want DNS Firewall to block the request + */ +export abstract class DnsBlockResponse { + /** + * Respond indicating that the query was successful, but no + * response is available for it. + */ + public static noData(): DnsBlockResponse { + return { blockResponse: 'NODATA' }; + } + + /** + * Respond indicating that the domain name that's in the query + * doesn't exist. + */ + public static nxDomain(): DnsBlockResponse { + return { blockResponse: 'NXDOMAIN' }; + } + + /** + * Provides a custom override response to the query + * + * @param domain The custom DNS record to send back in response to the query + * @param [ttl=0] The recommended amount of time for the DNS resolver or + * web browser to cache the provided override record + */ + public static override(domain: string, ttl?: Duration): DnsBlockResponse { + return { + blockResponse: 'OVERRIDE', + blockOverrideDnsType: 'CNAME', + blockOverrideDomain: domain, + blockOverrideTtl: ttl ?? Duration.seconds(0), + }; + } + + /** The DNS record's type */ + public abstract readonly blockOverrideDnsType?: string; + + /** The custom DNS record to send back in response to the query */ + public abstract readonly blockOverrideDomain?: string; + + /** + * The recommended amount of time for the DNS resolver or + * web browser to cache the provided override record + */ + public abstract readonly blockOverrideTtl?: Duration; + + /** The way that you want DNS Firewall to block the request */ + public abstract readonly blockResponse?: string; +} + +/** + * A Firewall Rule Group + */ +export class FirewallRuleGroup extends Resource implements IFirewallRuleGroup { + /** + * Import an existing Firewall Rule Group + */ + public static fromFirewallRuleGroupId(scope: Construct, id: string, firewallRuleGroupId: string): IFirewallRuleGroup { + class Import extends Resource implements IFirewallRuleGroup { + public readonly firewallRuleGroupId = firewallRuleGroupId; + } + return new Import(scope, id); + } + + public readonly firewallRuleGroupId: string; + + /** + * The ARN (Amazon Resource Name) of the rule group + * @attribute + */ + public readonly firewallRuleGroupArn: string; + + /** + * The date and time that the rule group was created + * @attribute + */ + public readonly firewallRuleGroupCreationTime: string; + + /** + * The creator request ID + * @attribute + */ + public readonly firewallRuleGroupCreatorRequestId: string; + + /** + * The date and time that the rule group was last modified + * @attribute + */ + public readonly firewallRuleGroupModificationTime: string; + + /** + * The AWS account ID for the account that created the rule group + * @attribute + */ + public readonly firewallRuleGroupOwnerId: string; + + /** + * The number of rules in the rule group + * @attribute + */ + public readonly firewallRuleGroupRuleCount: number; + + /** + * Whether the rule group is shared with other AWS accounts, + * or was shared with the current account by another AWS account + * @attribute + */ + public readonly firewallRuleGroupShareStatus: string; + + /** + * The status of the rule group + * @attribute + */ + public readonly firewallRuleGroupStatus: string; + + /** + * Additional information about the status of the rule group + * @attribute + */ + public readonly firewallRuleGroupStatusMessage: string; + + private readonly rules: FirewallRule[]; + + constructor(scope: Construct, id: string, props: FirewallRuleGroupProps = {}) { + super(scope, id); + + this.rules = props.rules ?? []; + + const ruleGroup = new CfnFirewallRuleGroup(this, 'Resource', { + name: props.name, + firewallRules: Lazy.any({ produce: () => this.rules.map(renderRule) }), + }); + + this.firewallRuleGroupId = ruleGroup.attrId; + this.firewallRuleGroupArn = ruleGroup.attrArn; + this.firewallRuleGroupCreationTime = ruleGroup.attrCreationTime; + this.firewallRuleGroupCreatorRequestId = ruleGroup.attrCreatorRequestId; + this.firewallRuleGroupModificationTime = ruleGroup.attrModificationTime; + this.firewallRuleGroupOwnerId = ruleGroup.attrOwnerId; + this.firewallRuleGroupRuleCount = ruleGroup.attrRuleCount; + this.firewallRuleGroupShareStatus = ruleGroup.attrShareStatus; + this.firewallRuleGroupStatus = ruleGroup.attrStatus; + this.firewallRuleGroupStatusMessage = ruleGroup.attrStatusMessage; + } + + /** + * Adds a rule to this group + */ + public addRule(rule: FirewallRule): FirewallRuleGroup { + this.rules.push(rule); + return this; + } + + /** + * Associates this Firewall Rule Group with a VPC + */ + public associate(id: string, props: FirewallRuleGroupAssociationOptions): FirewallRuleGroupAssociation { + return new FirewallRuleGroupAssociation(this, id, { + ...props, + firewallRuleGroup: this, + }); + } +} + +function renderRule(rule: FirewallRule): CfnFirewallRuleGroup.FirewallRuleProperty { + return { + action: rule.action.action, + firewallDomainListId: rule.firewallDomainList.firewallDomainListId, + priority: rule.priority, + blockOverrideDnsType: rule.action.blockResponse?.blockOverrideDnsType, + blockOverrideDomain: rule.action.blockResponse?.blockOverrideDomain, + blockOverrideTtl: rule.action.blockResponse?.blockOverrideTtl?.toSeconds(), + blockResponse: rule.action.blockResponse?.blockResponse, + }; +} diff --git a/packages/@aws-cdk/aws-route53resolver/lib/index.ts b/packages/@aws-cdk/aws-route53resolver/lib/index.ts index 7a6d65433e61f..97baf0759d24a 100644 --- a/packages/@aws-cdk/aws-route53resolver/lib/index.ts +++ b/packages/@aws-cdk/aws-route53resolver/lib/index.ts @@ -1,2 +1,6 @@ +export * from './firewall-domain-list'; +export * from './firewall-rule-group'; +export * from './firewall-rule-group-association'; + // AWS::Route53Resolver CloudFormation Resources: export * from './route53resolver.generated'; diff --git a/packages/@aws-cdk/aws-route53resolver/package.json b/packages/@aws-cdk/aws-route53resolver/package.json index a515fecb3c120..5e24de4caaffb 100644 --- a/packages/@aws-cdk/aws-route53resolver/package.json +++ b/packages/@aws-cdk/aws-route53resolver/package.json @@ -76,15 +76,22 @@ "devDependencies": { "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", "@aws-cdk/assertions": "0.0.0" }, "dependencies": { + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, "peerDependencies": { + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, @@ -92,7 +99,14 @@ "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", - "maturity": "cfn-only", + "maturity": "experimental", + "awslint": { + "exclude": [ + "props-physical-name:@aws-cdk/aws-route53resolver.FirewallDomainListProps", + "props-physical-name:@aws-cdk/aws-route53resolver.FirewallRuleGroupProps", + "props-physical-name:@aws-cdk/aws-route53resolver.FirewallRuleGroupAssociationProps" + ] + }, "awscdkio": { "announce": false }, diff --git a/packages/@aws-cdk/aws-route53resolver/test/domains.txt b/packages/@aws-cdk/aws-route53resolver/test/domains.txt new file mode 100644 index 0000000000000..872337c768ca7 --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/test/domains.txt @@ -0,0 +1,4 @@ +amazon.com +amazon.co.uk +amazon.fr +amazon.de diff --git a/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts b/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts new file mode 100644 index 0000000000000..3806a59c670ba --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts @@ -0,0 +1,123 @@ +import * as path from 'path'; +import { Template } from '@aws-cdk/assertions'; +import { Bucket } from '@aws-cdk/aws-s3'; +import { Stack } from '@aws-cdk/core'; +import { FirewallDomainList, FirewallDomains } from '../lib'; + +let stack: Stack; +beforeEach(() => { + stack = new Stack(); +}); + +test('domain list from strings', () => { + // WHEN + new FirewallDomainList(stack, 'List', { + domains: FirewallDomains.fromList(['first-domain.com', 'second-domain.net']), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Route53Resolver::FirewallDomainList', { + Domains: [ + 'first-domain.com', + 'second-domain.net', + ], + }); +}); + +test('domain list from S3 URL', () => { + // WHEN + new FirewallDomainList(stack, 'List', { + domains: FirewallDomains.fromS3Url('s3://bucket/prefix/object'), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Route53Resolver::FirewallDomainList', { + DomainFileUrl: 's3://bucket/prefix/object', + }); +}); + +test('domain list from S3', () => { + // WHEN + new FirewallDomainList(stack, 'List', { + domains: FirewallDomains.fromS3(Bucket.fromBucketName(stack, 'Bucket', 'bucket'), 'prefix/object'), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Route53Resolver::FirewallDomainList', { + DomainFileUrl: 's3://bucket/prefix/object', + }); +}); + +test('domain list from asset', () => { + // WHEN + new FirewallDomainList(stack, 'List', { + domains: FirewallDomains.fromAsset(path.join(__dirname, 'domains.txt')), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Route53Resolver::FirewallDomainList', { + DomainFileUrl: { + 'Fn::Join': [ + '', + [ + 's3://', + { + Ref: 'AssetParameterse820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35aS3BucketD6778673', + }, + '/', + { + 'Fn::Select': [ + 0, + { + 'Fn::Split': [ + '||', + { + Ref: 'AssetParameterse820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35aS3VersionKey1A69D23D', + }, + ], + }, + ], + }, + { + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + '||', + { + Ref: 'AssetParameterse820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35aS3VersionKey1A69D23D', + }, + ], + }, + ], + }, + ], + ], + }, + }); +}); + +test('throws with invalid name', () => { + expect(() => new FirewallDomainList(stack, 'List', { + name: 'Inv@lid', + domains: FirewallDomains.fromList(['domain.com']), + })).toThrow(/Invalid domain list name/); +}); + +test('throws with invalid domain', () => { + expect(() => new FirewallDomainList(stack, 'List', { + domains: FirewallDomains.fromList(['valid.fr', 'inv@lid.com']), + })).toThrow(/Invalid domain/); +}); + +test('throws with fromAsset and not .txt', () => { + expect(() => new FirewallDomainList(stack, 'List', { + domains: FirewallDomains.fromAsset('image.jpg'), + })).toThrow(/expects a file with the .txt extension/); +}); + +test('throws with invalid S3 URL', () => { + expect(() => new FirewallDomainList(stack, 'List', { + domains: FirewallDomains.fromS3Url('https://invalid/bucket/url'), + })).toThrow(/The S3 URL must start with s3:\/\//); +}); diff --git a/packages/@aws-cdk/aws-route53resolver/test/firewall-rule-group.test.ts b/packages/@aws-cdk/aws-route53resolver/test/firewall-rule-group.test.ts new file mode 100644 index 0000000000000..f8868d2bb31d9 --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/test/firewall-rule-group.test.ts @@ -0,0 +1,137 @@ +import { Template } from '@aws-cdk/assertions'; +import { Vpc } from '@aws-cdk/aws-ec2'; +import { Duration, Stack } from '@aws-cdk/core'; +import { DnsBlockResponse, FirewallDomainList, FirewallRuleAction, FirewallRuleGroup, IFirewallDomainList } from '../lib'; + +let stack: Stack; +let firewallDomainList: IFirewallDomainList; +beforeEach(() => { + stack = new Stack(); + firewallDomainList = FirewallDomainList.fromFirewallDomainListId(stack, 'List', 'domain-list-id'); +}); + +test('basic rule group', () => { + // WHEN + new FirewallRuleGroup(stack, 'RuleGroup', { + rules: [ + { + priority: 10, + firewallDomainList, + action: FirewallRuleAction.block(), + }, + ], + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Route53Resolver::FirewallRuleGroup', { + FirewallRules: [ + { + Action: 'BLOCK', + BlockResponse: 'NODATA', + FirewallDomainListId: 'domain-list-id', + Priority: 10, + }, + ], + }); +}); + +test('use addRule to add rules', () => { + // GIVEN + const ruleGroup = new FirewallRuleGroup(stack, 'RuleGroup', { + rules: [ + { + priority: 10, + firewallDomainList, + action: FirewallRuleAction.allow(), + }, + ], + }); + + // WHEN + ruleGroup.addRule({ + priority: 20, + firewallDomainList: FirewallDomainList.fromFirewallDomainListId(stack, 'OtherList', 'other-list-id'), + action: FirewallRuleAction.allow(), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Route53Resolver::FirewallRuleGroup', { + FirewallRules: [ + { + Action: 'ALLOW', + FirewallDomainListId: 'domain-list-id', + Priority: 10, + }, + { + Action: 'ALLOW', + FirewallDomainListId: 'other-list-id', + Priority: 20, + }, + ], + }); +}); + +test('rule with response override', () => { + // GIVEN + const ruleGroup = new FirewallRuleGroup(stack, 'RuleGroup'); + + // WHEN + ruleGroup.addRule({ + priority: 10, + firewallDomainList, + action: FirewallRuleAction.block(DnsBlockResponse.override('amazon.com', Duration.minutes(5))), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Route53Resolver::FirewallRuleGroup', { + FirewallRules: [ + { + Action: 'BLOCK', + BlockOverrideDnsType: 'CNAME', + BlockOverrideDomain: 'amazon.com', + BlockOverrideTtl: 300, + BlockResponse: 'OVERRIDE', + FirewallDomainListId: 'domain-list-id', + Priority: 10, + }, + ], + }); +}); + +test('associate rule group with a vpc', () => { + // GIVEN + const vpc = new Vpc(stack, 'Vpc'); + const ruleGroup = new FirewallRuleGroup(stack, 'RuleGroup'); + + // WHEN + ruleGroup.associate('Association', { + priority: 101, + vpc, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Route53Resolver::FirewallRuleGroupAssociation', { + FirewallRuleGroupId: { + 'Fn::GetAtt': [ + 'RuleGroup06BA8844', + 'Id', + ], + }, + Priority: 101, + VpcId: { + Ref: 'Vpc8378EB38', + }, + }); +}); + +test('throws when associating with a priority not between 100-9,000', () => { + // GIVEN + const vpc = new Vpc(stack, 'Vpc'); + const ruleGroup = new FirewallRuleGroup(stack, 'RuleGroup'); + + // THEN + expect(() => ruleGroup.associate('Association', { + priority: 100, + vpc, + })).toThrow(/Priority must be greater than 100 and less than 9000/); +}); diff --git a/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.expected.json b/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.expected.json new file mode 100644 index 0000000000000..c6b43a9c3ccfd --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.expected.json @@ -0,0 +1,321 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "cdk-route53-resolver-firewall/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/17", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "cdk-route53-resolver-firewall/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "cdk-route53-resolver-firewall/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "cdk-route53-resolver-firewall/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "cdk-route53-resolver-firewall/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/17", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "cdk-route53-resolver-firewall/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "cdk-route53-resolver-firewall/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "cdk-route53-resolver-firewall/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "BlockListC03D0423": { + "Type": "AWS::Route53Resolver::FirewallDomainList", + "Properties": { + "Domains": [ + "bad-domain.com", + "bot-domain.net" + ] + } + }, + "OverrideListF573FB0F": { + "Type": "AWS::Route53Resolver::FirewallDomainList", + "Properties": { + "Domains": [ + "override-domain.com" + ] + } + }, + "OtherListBA4427B5": { + "Type": "AWS::Route53Resolver::FirewallDomainList", + "Properties": { + "DomainFileUrl": { + "Fn::Join": [ + "", + [ + "s3://", + { + "Ref": "AssetParameterse820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35aS3BucketD6778673" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterse820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35aS3VersionKey1A69D23D" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterse820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35aS3VersionKey1A69D23D" + } + ] + } + ] + } + ] + ] + } + } + }, + "RuleGroup06BA8844": { + "Type": "AWS::Route53Resolver::FirewallRuleGroup", + "Properties": { + "FirewallRules": [ + { + "Action": "BLOCK", + "BlockResponse": "NODATA", + "FirewallDomainListId": { + "Fn::GetAtt": [ + "BlockListC03D0423", + "Id" + ] + }, + "Priority": 10 + }, + { + "Action": "BLOCK", + "BlockOverrideDnsType": "CNAME", + "BlockOverrideDomain": "amazon.com", + "BlockOverrideTtl": 0, + "BlockResponse": "OVERRIDE", + "FirewallDomainListId": { + "Fn::GetAtt": [ + "OverrideListF573FB0F", + "Id" + ] + }, + "Priority": 20 + } + ] + } + }, + "RuleGroupAssociation5494BFB1": { + "Type": "AWS::Route53Resolver::FirewallRuleGroupAssociation", + "Properties": { + "FirewallRuleGroupId": { + "Fn::GetAtt": [ + "RuleGroup06BA8844", + "Id" + ] + }, + "Priority": 101, + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + } + }, + "Parameters": { + "AssetParameterse820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35aS3BucketD6778673": { + "Type": "String", + "Description": "S3 bucket for asset \"e820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35a\"" + }, + "AssetParameterse820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35aS3VersionKey1A69D23D": { + "Type": "String", + "Description": "S3 key for asset version \"e820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35a\"" + }, + "AssetParameterse820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35aArtifactHashFF61A347": { + "Type": "String", + "Description": "Artifact hash for asset \"e820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35a\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.ts b/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.ts new file mode 100644 index 0000000000000..7c4b2498af6f5 --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.ts @@ -0,0 +1,46 @@ +import * as path from 'path'; +import { Vpc } from '@aws-cdk/aws-ec2'; +import { App, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as route53resolver from '../lib'; + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const vpc = new Vpc(this, 'Vpc', { maxAzs: 1 }); + + const blockList = new route53resolver.FirewallDomainList(this, 'BlockList', { + domains: route53resolver.FirewallDomains.fromList(['bad-domain.com', 'bot-domain.net']), + }); + const overrideList = new route53resolver.FirewallDomainList(this, 'OverrideList', { + domains: route53resolver.FirewallDomains.fromList(['override-domain.com']), + }); + + new route53resolver.FirewallDomainList(this, 'OtherList', { + domains: route53resolver.FirewallDomains.fromAsset(path.join(__dirname, 'domains.txt')), + }); + + const ruleGroup = new route53resolver.FirewallRuleGroup(this, 'RuleGroup'); + + ruleGroup.addRule({ + priority: 10, + firewallDomainList: blockList, + action: route53resolver.FirewallRuleAction.block(), + }); + ruleGroup.addRule({ + priority: 20, + firewallDomainList: overrideList, + action: route53resolver.FirewallRuleAction.block(route53resolver.DnsBlockResponse.override('amazon.com')), + }); + + ruleGroup.associate('Association', { + priority: 101, + vpc, + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-route53-resolver-firewall'); +app.synth(); diff --git a/packages/@aws-cdk/aws-route53resolver/test/route53resolver.test.ts b/packages/@aws-cdk/aws-route53resolver/test/route53resolver.test.ts deleted file mode 100644 index 465c7bdea0693..0000000000000 --- a/packages/@aws-cdk/aws-route53resolver/test/route53resolver.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import '@aws-cdk/assertions'; -import {} from '../lib'; - -test('No tests are specified for this package', () => { - expect(true).toBe(true); -}); From d763d9092289d0b28b2695b8474b44ed7d0bce54 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 31 Aug 2021 13:35:15 +0200 Subject: [PATCH 43/91] feat(cli): support `--no-rollback` flag (#16293) CloudFormation recently released a feature that makes it easier to iterate quickly on development stacks, where the rollback step of a deployment is skipped if a deployment fails. https://aws.amazon.com/de/blogs/aws/new-for-aws-cloudformation-quickly-retry-stack-operations-from-the-point-of-failure/ This allows a developer to fix their code and retry quickly without losing time waiting for rollbacks. Fixes #16289. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/aws-cdk/README.md | 20 ++++++++++++- packages/aws-cdk/bin/cdk.ts | 16 +++++++++- .../lib/api/cloudformation-deployments.ts | 8 +++++ packages/aws-cdk/lib/api/deploy-stack.ts | 14 ++++++++- packages/aws-cdk/lib/cdk-toolkit.ts | 8 +++++ packages/aws-cdk/lib/settings.ts | 9 ++++++ packages/aws-cdk/package.json | 2 +- .../aws-cdk/test/api/deploy-stack.test.ts | 29 +++++++++++++++++++ yarn.lock | 15 ++++++++++ 9 files changed, 117 insertions(+), 4 deletions(-) diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index cc6bfe3bbd39a..729413ae6859d 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -140,7 +140,7 @@ $ cdk diff --app='node bin/main.js' MyStackName --template=path/to/template.yml ### `cdk deploy` -Deploys a stack of your CDK app to it's environment. During the deployment, the toolkit will output progress +Deploys a stack of your CDK app to its environment. During the deployment, the toolkit will output progress indications, similar to what can be observed in the AWS CloudFormation Console. If the environment was never bootstrapped (using `cdk bootstrap`), only stacks that are not using assets and synthesize to a template that is under 51,200 bytes will successfully deploy. @@ -154,6 +154,24 @@ currently deployed stack to the template and tags that are about to be deployed will skip deployment if they are identical. Use `--force` to override this behavior and always deploy the stack. +#### Disabling Rollback + +If a resource fails to be created or updated, the deployment will *roll back* before the CLI returns. All changes made +up to that point will be undone (resources that were created will be deleted, updates that were made will be changed +back) in order to leave the stack in a consistent state at the end of the operation. If you are using the CDK CLI +to iterate on a development stack in your personal account, you might not require CloudFormation to leave your +stack in a consistent state, but instead would prefer to update your CDK application and try again. + +To disable the rollback feature, specify `--no-rollback` (`-R` for short): + +```console +$ cdk deploy --no-rollback +$ cdk deploy -R +``` + +NOTE: you cannot use `--no-rollback` for any updates that would cause a resource replacement, only for updates +and creations of new resources. + #### Deploying multiple stacks You can have multiple stacks in a cdk app. An example can be found in [how to create multiple stacks](https://docs.aws.amazon.com/cdk/latest/guide/stack_how_to_create_multiple_stacks.html). diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index 29f217360355f..a21a350415223 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -104,7 +104,11 @@ async function parseCommandLineArguments() { .option('parameters', { type: 'array', desc: 'Additional parameters passed to CloudFormation at deploy time (STACK:KEY=VALUE)', nargs: 1, requiresArg: true, default: {} }) .option('outputs-file', { type: 'string', alias: 'O', desc: 'Path to file where stack outputs will be written as JSON', requiresArg: true }) .option('previous-parameters', { type: 'boolean', default: true, desc: 'Use previous values for existing parameters (you must specify all parameters on every deployment if this is disabled)' }) - .option('progress', { type: 'string', choices: [StackActivityProgress.BAR, StackActivityProgress.EVENTS], desc: 'Display mode for stack activity events' }), + .option('progress', { type: 'string', choices: [StackActivityProgress.BAR, StackActivityProgress.EVENTS], desc: 'Display mode for stack activity events' }) + .option('rollback', { type: 'boolean', default: true, desc: 'Rollback stack to stable state on failure (iterate more rapidly with --no-rollback or -R)' }) + // Hack to get '-R' as an alias for '--no-rollback', suggested by: https://github.com/yargs/yargs/issues/1729 + .option('R', { type: 'boolean', hidden: true }) + .middleware(yargsNegativeAlias('R', 'rollback'), true), ) .command('destroy [STACKS..]', 'Destroy the stack(s) named STACKS', yargs => yargs .option('all', { type: 'boolean', default: false, desc: 'Destroy all available stacks' }) @@ -319,6 +323,7 @@ async function initCommandLine() { outputsFile: configuration.settings.get(['outputsFile']), progress: configuration.settings.get(['progress']), ci: args.ci, + rollback: configuration.settings.get(['rollback']), }); case 'destroy': @@ -421,6 +426,15 @@ function arrayFromYargs(xs: string[]): string[] | undefined { return xs.filter(x => x !== ''); } +function yargsNegativeAlias(shortName: S, longName: L) { + return (argv: T) => { + if (shortName in argv && argv[shortName]) { + (argv as any)[longName] = false; + } + return argv; + }; +} + initCommandLine() .then(value => { if (value == null) { return; } diff --git a/packages/aws-cdk/lib/api/cloudformation-deployments.ts b/packages/aws-cdk/lib/api/cloudformation-deployments.ts index ed783e0826fd7..8cea2ae411212 100644 --- a/packages/aws-cdk/lib/api/cloudformation-deployments.ts +++ b/packages/aws-cdk/lib/api/cloudformation-deployments.ts @@ -129,6 +129,13 @@ export interface DeployStackOptions { * @default false */ readonly ci?: boolean; + + /** + * Rollback failed deployments + * + * @default true + */ + readonly rollback?: boolean; } export interface DestroyStackOptions { @@ -204,6 +211,7 @@ export class CloudFormationDeployments { usePreviousParameters: options.usePreviousParameters, progress: options.progress, ci: options.ci, + rollback: options.rollback, }); } diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index df811e39587be..62c5815197fc2 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -175,6 +175,13 @@ export interface DeployStackOptions { * @default false */ readonly ci?: boolean; + + /** + * Rollback failed deployments + * + * @default true + */ + readonly rollback?: boolean; } const LARGE_TEMPLATE_SIZE_KB = 50; @@ -282,7 +289,12 @@ export async function deployStack(options: DeployStackOptions): Promise { + test('by default, we do not disable rollback (and also do not pass the flag)', async () => { + // WHEN + await deployStack({ + ...standardDeployStackArguments(), + }); + + // THEN + expect(cfnMocks.executeChangeSet).toHaveBeenCalledTimes(1); + expect(cfnMocks.executeChangeSet).not.toHaveBeenCalledWith(expect.objectContaining({ + DisableRollback: expect.anything(), + })); + }); + + test('rollback can be disabled by setting rollback: false', async () => { + // WHEN + await deployStack({ + ...standardDeployStackArguments(), + rollback: false, + }); + + // THEN + expect(cfnMocks.executeChangeSet).toHaveBeenCalledWith(expect.objectContaining({ + DisableRollback: true, + })); + }); + +}); + /** * Set up the mocks so that it looks like the stack exists to start with * diff --git a/yarn.lock b/yarn.lock index c553079426b7d..e9d7fe62947d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2319,6 +2319,21 @@ aws-sdk@^2.848.0, aws-sdk@^2.928.0: uuid "3.3.2" xml2js "0.4.19" +aws-sdk@^2.979.0: + version "2.979.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.979.0.tgz#d0104fec763cc3eafb123e709f94866790109da4" + integrity sha512-pKKhpYZwmihCvuH3757WHY8JQI9g2wvtF3s0aiyH2xCUmX/6uekhExz/utD4uqZP3m3PwKZPGQkQkH30DtHrPw== + dependencies: + buffer "4.9.2" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.3.2" + xml2js "0.4.19" + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" From cb1cf15e0a9a92713e71e80d0ec804c7642f1675 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Tue, 31 Aug 2021 13:44:31 +0100 Subject: [PATCH 44/91] chore: migrate some modules from nodeunit to jest (#16295) No changes have been made to the tests, pure migration. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/app-delivery/.gitignore | 3 +- packages/@aws-cdk/app-delivery/.npmignore | 3 +- packages/@aws-cdk/app-delivery/jest.config.js | 2 + packages/@aws-cdk/app-delivery/package.json | 6 +- ...s => pipeline-deploy-stack-action.test.ts} | 112 +++--- .../aws-applicationautoscaling/.gitignore | 3 +- .../aws-applicationautoscaling/.npmignore | 3 +- .../aws-applicationautoscaling/jest.config.js | 9 + .../aws-applicationautoscaling/package.json | 5 +- .../test/cron.test.ts | 55 +++ ...able-target.ts => scalable-target.test.ts} | 81 ++--- ...-policy.ts => step-scaling-policy.test.ts} | 82 ++--- ...et-tracking.ts => target-tracking.test.ts} | 37 +- .../test/test.cron.ts | 56 --- packages/@aws-cdk/aws-eks-legacy/.gitignore | 3 +- packages/@aws-cdk/aws-eks-legacy/.npmignore | 3 +- .../@aws-cdk/aws-eks-legacy/jest.config.js | 2 + packages/@aws-cdk/aws-eks-legacy/package.json | 7 +- .../test/{test.awsauth.ts => awsauth.test.ts} | 39 +- .../test/{test.cluster.ts => cluster.test.ts} | 297 ++++++++------- .../aws-eks-legacy/test/helm-chart.test.ts | 54 +++ .../{test.manifest.ts => manifest.test.ts} | 17 +- .../test/test.cluster-resource.ts | 69 ---- .../aws-eks-legacy/test/test.helm-chart.ts | 55 --- .../{test.user-data.ts => user-data.test.ts} | 85 +++-- packages/@aws-cdk/aws-events/.gitignore | 3 +- packages/@aws-cdk/aws-events/.npmignore | 3 +- packages/@aws-cdk/aws-events/jest.config.js | 2 + packages/@aws-cdk/aws-events/package.json | 4 +- .../test/{test.archive.ts => archive.test.ts} | 35 +- .../{test.event-bus.ts => event-bus.test.ts} | 223 ++++++------ .../test/{test.input.ts => input.test.ts} | 115 +++--- .../test/{test.rule.ts => rule.test.ts} | 341 +++++++++--------- .../@aws-cdk/aws-events/test/schedule.test.ts | 96 +++++ .../@aws-cdk/aws-events/test/test.schedule.ts | 97 ----- .../test/{test.util.ts => util.test.ts} | 75 ++-- 36 files changed, 1015 insertions(+), 1067 deletions(-) create mode 100644 packages/@aws-cdk/app-delivery/jest.config.js rename packages/@aws-cdk/app-delivery/test/{test.pipeline-deploy-stack-action.ts => pipeline-deploy-stack-action.test.ts} (87%) create mode 100644 packages/@aws-cdk/aws-applicationautoscaling/jest.config.js create mode 100644 packages/@aws-cdk/aws-applicationautoscaling/test/cron.test.ts rename packages/@aws-cdk/aws-applicationautoscaling/test/{test.scalable-target.ts => scalable-target.test.ts} (64%) rename packages/@aws-cdk/aws-applicationautoscaling/test/{test.step-scaling-policy.ts => step-scaling-policy.test.ts} (87%) rename packages/@aws-cdk/aws-applicationautoscaling/test/{test.target-tracking.ts => target-tracking.test.ts} (73%) delete mode 100644 packages/@aws-cdk/aws-applicationautoscaling/test/test.cron.ts create mode 100644 packages/@aws-cdk/aws-eks-legacy/jest.config.js rename packages/@aws-cdk/aws-eks-legacy/test/{test.awsauth.ts => awsauth.test.ts} (85%) rename packages/@aws-cdk/aws-eks-legacy/test/{test.cluster.ts => cluster.test.ts} (70%) create mode 100644 packages/@aws-cdk/aws-eks-legacy/test/helm-chart.test.ts rename packages/@aws-cdk/aws-eks-legacy/test/{test.manifest.ts => manifest.test.ts} (86%) delete mode 100644 packages/@aws-cdk/aws-eks-legacy/test/test.cluster-resource.ts delete mode 100644 packages/@aws-cdk/aws-eks-legacy/test/test.helm-chart.ts rename packages/@aws-cdk/aws-eks-legacy/test/{test.user-data.ts => user-data.test.ts} (73%) create mode 100644 packages/@aws-cdk/aws-events/jest.config.js rename packages/@aws-cdk/aws-events/test/{test.archive.ts => archive.test.ts} (69%) rename packages/@aws-cdk/aws-events/test/{test.event-bus.ts => event-bus.test.ts} (71%) rename packages/@aws-cdk/aws-events/test/{test.input.ts => input.test.ts} (78%) rename packages/@aws-cdk/aws-events/test/{test.rule.ts => rule.test.ts} (79%) create mode 100644 packages/@aws-cdk/aws-events/test/schedule.test.ts delete mode 100644 packages/@aws-cdk/aws-events/test/test.schedule.ts rename packages/@aws-cdk/aws-events/test/{test.util.ts => util.test.ts} (53%) diff --git a/packages/@aws-cdk/app-delivery/.gitignore b/packages/@aws-cdk/app-delivery/.gitignore index 1ed331de29bb6..8e88c5db7a762 100644 --- a/packages/@aws-cdk/app-delivery/.gitignore +++ b/packages/@aws-cdk/app-delivery/.gitignore @@ -11,4 +11,5 @@ tsconfig.json coverage !.eslintrc.js -junit.xml \ No newline at end of file +junit.xml +!jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/app-delivery/.npmignore b/packages/@aws-cdk/app-delivery/.npmignore index b0d9cb5dd2512..a11c52cc9c6cb 100644 --- a/packages/@aws-cdk/app-delivery/.npmignore +++ b/packages/@aws-cdk/app-delivery/.npmignore @@ -22,4 +22,5 @@ tsconfig.json **/cdk.out junit.xml test/ -!*.lit.ts \ No newline at end of file +!*.lit.ts +jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/app-delivery/jest.config.js b/packages/@aws-cdk/app-delivery/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/app-delivery/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/app-delivery/package.json b/packages/@aws-cdk/app-delivery/package.json index 45694d1f602c0..9b5bb232938dd 100644 --- a/packages/@aws-cdk/app-delivery/package.json +++ b/packages/@aws-cdk/app-delivery/package.json @@ -61,11 +61,12 @@ }, "devDependencies": { "@aws-cdk/aws-s3": "0.0.0", + "@types/jest": "^26.0.24", "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "fast-check": "^2.17.0", - "nodeunit": "^0.11.3", + "jest": "^26.6.3", "pkglint": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, @@ -81,6 +82,9 @@ "url": "https://aws.amazon.com", "organization": true }, + "cdk-build": { + "jest": true + }, "keywords": [ "aws", "cdk" diff --git a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts b/packages/@aws-cdk/app-delivery/test/pipeline-deploy-stack-action.test.ts similarity index 87% rename from packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts rename to packages/@aws-cdk/app-delivery/test/pipeline-deploy-stack-action.test.ts index e4611adf570f6..e065330c152d3 100644 --- a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts +++ b/packages/@aws-cdk/app-delivery/test/pipeline-deploy-stack-action.test.ts @@ -1,4 +1,5 @@ -import { expect, haveResource, haveResourceLike, isSuperObject } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { isSuperObject } from '@aws-cdk/assert-internal'; import * as cfn from '@aws-cdk/aws-cloudformation'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; @@ -10,7 +11,6 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cdk from '@aws-cdk/core'; import * as constructs from 'constructs'; import * as fc from 'fast-check'; -import * as nodeunit from 'nodeunit'; import { PipelineDeployStackAction } from '../lib/pipeline-deploy-stack-action'; interface SelfUpdatingPipeline { @@ -19,14 +19,14 @@ interface SelfUpdatingPipeline { } const accountId = fc.array(fc.integer(0, 9), 12, 12).map(arr => arr.join()); -export = nodeunit.testCase({ - 'rejects cross-environment deployment'(test: nodeunit.Test) { +describe('pipeline deploy stack action', () => { + test('rejects cross-environment deployment', () => { fc.assert( fc.property( accountId, accountId, (pipelineAccount, stackAccount) => { fc.pre(pipelineAccount !== stackAccount); - test.throws(() => { + expect(() => { const app = new cdk.App(); const stack = new cdk.Stack(app, 'Test', { env: { account: pipelineAccount } }); const pipeline = new codepipeline.Pipeline(stack, 'Pipeline'); @@ -43,20 +43,20 @@ export = nodeunit.testCase({ stack: new cdk.Stack(app, 'DeployedStack', { env: { account: stackAccount } }), adminPermissions: false, })); - }, 'Cross-environment deployment is not supported'); + }).toThrow('Cross-environment deployment is not supported'); }, ), ); - test.done(); - }, - 'rejects createRunOrder >= executeRunOrder'(test: nodeunit.Test) { + }); + + test('rejects createRunOrder >= executeRunOrder', () => { fc.assert( fc.property( fc.integer(1, 999), fc.integer(1, 999), (createRunOrder, executeRunOrder) => { fc.pre(createRunOrder >= executeRunOrder); - test.throws(() => { + expect(() => { const app = new cdk.App(); const stack = new cdk.Stack(app, 'Test'); const pipeline = new codepipeline.Pipeline(stack, 'Pipeline'); @@ -74,13 +74,13 @@ export = nodeunit.testCase({ stack: new cdk.Stack(app, 'DeployedStack'), adminPermissions: false, })); - }, 'createChangeSetRunOrder must be < executeChangeSetRunOrder'); + }).toThrow(/createChangeSetRunOrder .* must be < executeChangeSetRunOrder/); }, ), ); - test.done(); - }, - 'users can supply CloudFormation capabilities'(test: nodeunit.Test) { + + }); + test('users can supply CloudFormation capabilities', () => { const pipelineStack = getTestStack(); const stackWithNoCapability = new cdk.Stack(undefined, 'NoCapStack', { env: { account: '123456789012', region: 'us-east-1' } }); @@ -134,57 +134,57 @@ export = nodeunit.testCase({ capabilities: [cfn.CloudFormationCapabilities.ANONYMOUS_IAM, cfn.CloudFormationCapabilities.AUTO_EXPAND], adminPermissions: false, })); - expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + expect(pipelineStack).toHaveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ Configuration: { StackName: 'TestStack', ActionMode: 'CHANGE_SET_REPLACE', Capabilities: 'CAPABILITY_NAMED_IAM', }, - }))); - expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + })); + expect(pipelineStack).toHaveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ Configuration: { StackName: 'AnonymousIAM', ActionMode: 'CHANGE_SET_REPLACE', Capabilities: 'CAPABILITY_IAM', }, - }))); - expect(pipelineStack).notTo(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + })); + expect(pipelineStack).not.toHaveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ Configuration: { StackName: 'NoCapStack', ActionMode: 'CHANGE_SET_REPLACE', Capabilities: 'CAPABILITY_NAMED_IAM', }, - }))); - expect(pipelineStack).notTo(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + })); + expect(pipelineStack).not.toHaveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ Configuration: { StackName: 'NoCapStack', ActionMode: 'CHANGE_SET_REPLACE', Capabilities: 'CAPABILITY_IAM', }, - }))); - expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + })); + expect(pipelineStack).toHaveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ Configuration: { StackName: 'NoCapStack', ActionMode: 'CHANGE_SET_REPLACE', }, - }))); - expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + })); + expect(pipelineStack).toHaveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ Configuration: { StackName: 'AutoExpand', ActionMode: 'CHANGE_SET_REPLACE', Capabilities: 'CAPABILITY_AUTO_EXPAND', }, - }))); - expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + })); + expect(pipelineStack).toHaveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ Configuration: { StackName: 'AnonymousIAMAndAutoExpand', ActionMode: 'CHANGE_SET_REPLACE', Capabilities: 'CAPABILITY_IAM,CAPABILITY_AUTO_EXPAND', }, - }))); - test.done(); - }, - 'users can use admin permissions'(test: nodeunit.Test) { + })); + + }); + test('users can use admin permissions', () => { const pipelineStack = getTestStack(); const selfUpdatingStack = createSelfUpdatingStack(pipelineStack); @@ -195,7 +195,7 @@ export = nodeunit.testCase({ input: selfUpdatingStack.synthesizedApp, adminPermissions: true, })); - expect(pipelineStack).to(haveResource('AWS::IAM::Policy', { + expect(pipelineStack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -249,17 +249,17 @@ export = nodeunit.testCase({ }, ], }, - })); - expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + }); + expect(pipelineStack).toHaveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ Configuration: { StackName: 'TestStack', ActionMode: 'CHANGE_SET_REPLACE', Capabilities: 'CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND', }, - }))); - test.done(); - }, - 'users can supply a role for deploy action'(test: nodeunit.Test) { + })); + + }); + test('users can supply a role for deploy action', () => { const pipelineStack = getTestStack(); const selfUpdatingStack = createSelfUpdatingStack(pipelineStack); @@ -275,10 +275,10 @@ export = nodeunit.testCase({ role, }); selfUpdateStage.addAction(deployAction); - test.same(deployAction.deploymentRole, role); - test.done(); - }, - 'users can specify IAM permissions for the deploy action'(test: nodeunit.Test) { + expect(deployAction.deploymentRole).toEqual(role); + + }); + test('users can specify IAM permissions for the deploy action', () => { // GIVEN // const pipelineStack = getTestStack(); @@ -312,7 +312,7 @@ export = nodeunit.testCase({ })); // THEN // - expect(pipelineStack).to(haveResource('AWS::IAM::Policy', { + expect(pipelineStack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -379,10 +379,10 @@ export = nodeunit.testCase({ Ref: 'CodePipelineDeployChangeSetRoleF9F2B343', }, ], - })); - test.done(); - }, - 'rejects stacks with assets'(test: nodeunit.Test) { + }); + + }); + test('rejects stacks with assets', () => { fc.assert( fc.property( fc.integer(1, 5), @@ -394,21 +394,21 @@ export = nodeunit.testCase({ deployedStack.node.addMetadata(cxschema.ArtifactMetadataEntryType.ASSET, {}); } - test.throws(() => { + expect(() => { new PipelineDeployStackAction({ changeSetName: 'ChangeSet', input: new codepipeline.Artifact(), stack: deployedStack, adminPermissions: false, }); - }, /Cannot deploy the stack DeployedStack because it references/); + }).toThrow(/Cannot deploy the stack DeployedStack because it references/); }, ), ); - test.done(); - }, - 'allows overriding the ChangeSet and Execute action names'(test: nodeunit.Test) { + }); + + test('allows overriding the ChangeSet and Execute action names', () => { const stack = getTestStack(); const selfUpdatingPipeline = createSelfUpdatingStack(stack); selfUpdatingPipeline.pipeline.addStage({ @@ -424,7 +424,7 @@ export = nodeunit.testCase({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: [ {}, {}, @@ -440,10 +440,10 @@ export = nodeunit.testCase({ ], }, ], - })); + }); + - test.done(); - }, + }); }); class FakeAction implements codepipeline.IAction { diff --git a/packages/@aws-cdk/aws-applicationautoscaling/.gitignore b/packages/@aws-cdk/aws-applicationautoscaling/.gitignore index 018c65919d67c..7e6fdd4d423db 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/.gitignore +++ b/packages/@aws-cdk/aws-applicationautoscaling/.gitignore @@ -15,4 +15,5 @@ nyc.config.js *.snk !.eslintrc.js -junit.xml \ No newline at end of file +junit.xml +!jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-applicationautoscaling/.npmignore b/packages/@aws-cdk/aws-applicationautoscaling/.npmignore index 9a032ae80868c..e8acf10a468a1 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/.npmignore +++ b/packages/@aws-cdk/aws-applicationautoscaling/.npmignore @@ -24,4 +24,5 @@ tsconfig.json **/cdk.out junit.xml test/ -!*.lit.ts \ No newline at end of file +!*.lit.ts +jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-applicationautoscaling/jest.config.js b/packages/@aws-cdk/aws-applicationautoscaling/jest.config.js new file mode 100644 index 0000000000000..3eef7ba398c07 --- /dev/null +++ b/packages/@aws-cdk/aws-applicationautoscaling/jest.config.js @@ -0,0 +1,9 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + branches: 75, + }, + }, +}; diff --git a/packages/@aws-cdk/aws-applicationautoscaling/package.json b/packages/@aws-cdk/aws-applicationautoscaling/package.json index a48c9b098229f..5026578a5d6d0 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/package.json +++ b/packages/@aws-cdk/aws-applicationautoscaling/package.json @@ -55,6 +55,7 @@ }, "cdk-build": { "cloudformation": "AWS::ApplicationAutoScaling", + "jest": true, "env": { "AWSLINT_BASE_CONSTRUCT": true } @@ -72,11 +73,11 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.32", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "fast-check": "^2.17.0", - "nodeunit": "^0.11.3", + "jest": "^26.6.3", "pkglint": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-applicationautoscaling/test/cron.test.ts b/packages/@aws-cdk/aws-applicationautoscaling/test/cron.test.ts new file mode 100644 index 0000000000000..b3c337883146c --- /dev/null +++ b/packages/@aws-cdk/aws-applicationautoscaling/test/cron.test.ts @@ -0,0 +1,55 @@ +import { Duration, Stack, Lazy } from '@aws-cdk/core'; +import * as appscaling from '../lib'; + +describe('cron', () => { + test('test utc cron, hour only', () => { + expect(appscaling.Schedule.cron({ hour: '18', minute: '0' }).expressionString).toEqual('cron(0 18 * * ? *)'); + + }); + + test('test utc cron, hour and minute', () => { + expect(appscaling.Schedule.cron({ hour: '18', minute: '24' }).expressionString).toEqual('cron(24 18 * * ? *)'); + + }); + + test('rate must be whole number of minutes', () => { + expect(() => { + appscaling.Schedule.rate(Duration.minutes(0.13456)); + }).toThrow(/'0.13456 minutes' cannot be converted into a whole number of seconds/); + + }); + + test('rate can be in seconds', () => { + const duration = appscaling.Schedule.rate(Duration.seconds(120)); + expect('rate(2 minutes)').toEqual(duration.expressionString); + + }); + + test('rate must not be in seconds when specified as a token', () => { + expect(() => { + appscaling.Schedule.rate(Duration.seconds(Lazy.number({ produce: () => 5 }))); + }).toThrow(/Allowed units for scheduling/); + + }); + + test('rate cannot be 0', () => { + expect(() => { + appscaling.Schedule.rate(Duration.days(0)); + }).toThrow(/Duration cannot be 0/); + + }); + + test('rate can be token', () => { + const stack = new Stack(); + const lazyDuration = Duration.minutes(Lazy.number({ produce: () => 5 })); + const rate = appscaling.Schedule.rate(lazyDuration); + expect('rate(5 minutes)').toEqual(stack.resolve(rate).expressionString); + + }); + + test('rate can be in allowed type hours', () => { + expect('rate(1 hour)').toEqual(appscaling.Schedule.rate(Duration.hours(1)) + .expressionString); + + }); +}); diff --git a/packages/@aws-cdk/aws-applicationautoscaling/test/test.scalable-target.ts b/packages/@aws-cdk/aws-applicationautoscaling/test/scalable-target.test.ts similarity index 64% rename from packages/@aws-cdk/aws-applicationautoscaling/test/test.scalable-target.ts rename to packages/@aws-cdk/aws-applicationautoscaling/test/scalable-target.test.ts index 8c48f8b844a84..8628a8624468c 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/test/test.scalable-target.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/test/scalable-target.test.ts @@ -1,12 +1,11 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as appscaling from '../lib'; import { createScalableTarget } from './util'; -export = { - 'test scalable target creation'(test: Test) { +describe('scalable target', () => { + test('test scalable target creation', () => { // GIVEN const stack = new cdk.Stack(); @@ -20,18 +19,18 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { ServiceNamespace: 'dynamodb', ScalableDimension: 'test:TestCount', ResourceId: 'test:this/test', MinCapacity: 1, MaxCapacity: 20, - })); + }); + - test.done(); - }, + }); - 'validation does not fail when using Tokens'(test: Test) { + test('validation does not fail when using Tokens', () => { // GIVEN const stack = new cdk.Stack(); @@ -45,18 +44,18 @@ export = { }); // THEN: no exception - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { ServiceNamespace: 'dynamodb', ScalableDimension: 'test:TestCount', ResourceId: 'test:this/test', MinCapacity: 10, MaxCapacity: 1, - })); + }); - test.done(); - }, - 'add scheduled scaling'(test: Test) { + }); + + test('add scheduled scaling', () => { // GIVEN const stack = new cdk.Stack(); const target = createScalableTarget(stack); @@ -69,7 +68,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { ScheduledActions: [ { ScalableTargetAction: { @@ -80,12 +79,12 @@ export = { ScheduledActionName: 'ScaleUp', }, ], - })); + }); + - test.done(); - }, + }); - 'step scaling on MathExpression'(test: Test) { + test('step scaling on MathExpression', () => { // GIVEN const stack = new cdk.Stack(); const target = createScalableTarget(stack); @@ -110,11 +109,11 @@ export = { }); // THEN - expect(stack).notTo(haveResource('AWS::CloudWatch::Alarm', { + expect(stack).not.toHaveResource('AWS::CloudWatch::Alarm', { Period: 60, - })); + }); - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { ComparisonOperator: 'LessThanOrEqualToThreshold', EvaluationPeriods: 1, Metrics: [ @@ -136,22 +135,22 @@ export = { }, ], Threshold: 49, - })); - - test.done(); - }, - - 'test service namespace enum'(test: Test) { - test.equals(appscaling.ServiceNamespace.APPSTREAM, 'appstream'); - test.equals(appscaling.ServiceNamespace.COMPREHEND, 'comprehend'); - test.equals(appscaling.ServiceNamespace.CUSTOM_RESOURCE, 'custom-resource'); - test.equals(appscaling.ServiceNamespace.DYNAMODB, 'dynamodb'); - test.equals(appscaling.ServiceNamespace.EC2, 'ec2'); - test.equals(appscaling.ServiceNamespace.ECS, 'ecs'); - test.equals(appscaling.ServiceNamespace.ELASTIC_MAP_REDUCE, 'elasticmapreduce'); - test.equals(appscaling.ServiceNamespace.LAMBDA, 'lambda'); - test.equals(appscaling.ServiceNamespace.RDS, 'rds'); - test.equals(appscaling.ServiceNamespace.SAGEMAKER, 'sagemaker'); - test.done(); - }, -}; + }); + + + }); + + test('test service namespace enum', () => { + expect(appscaling.ServiceNamespace.APPSTREAM).toEqual( 'appstream'); + expect(appscaling.ServiceNamespace.COMPREHEND).toEqual( 'comprehend'); + expect(appscaling.ServiceNamespace.CUSTOM_RESOURCE).toEqual( 'custom-resource'); + expect(appscaling.ServiceNamespace.DYNAMODB).toEqual( 'dynamodb'); + expect(appscaling.ServiceNamespace.EC2).toEqual( 'ec2'); + expect(appscaling.ServiceNamespace.ECS).toEqual( 'ecs'); + expect(appscaling.ServiceNamespace.ELASTIC_MAP_REDUCE).toEqual( 'elasticmapreduce'); + expect(appscaling.ServiceNamespace.LAMBDA).toEqual( 'lambda'); + expect(appscaling.ServiceNamespace.RDS).toEqual( 'rds'); + expect(appscaling.ServiceNamespace.SAGEMAKER).toEqual( 'sagemaker'); + + }); +}); diff --git a/packages/@aws-cdk/aws-applicationautoscaling/test/test.step-scaling-policy.ts b/packages/@aws-cdk/aws-applicationautoscaling/test/step-scaling-policy.test.ts similarity index 87% rename from packages/@aws-cdk/aws-applicationautoscaling/test/test.step-scaling-policy.ts rename to packages/@aws-cdk/aws-applicationautoscaling/test/step-scaling-policy.test.ts index bb585daeb6eae..528a84b997306 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/test/test.step-scaling-policy.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/test/step-scaling-policy.test.ts @@ -1,13 +1,13 @@ -import { expect, haveResource, haveResourceLike, SynthUtils } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { SynthUtils } from '@aws-cdk/assert-internal'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as cdk from '@aws-cdk/core'; import * as fc from 'fast-check'; -import { Test } from 'nodeunit'; import * as appscaling from '../lib'; import { arbitrary_input_intervals, createScalableTarget } from './util'; -export = { - 'alarm thresholds are valid numbers'(test: Test) { +describe('step scaling policy', () => { + test('alarm thresholds are valid numbers', () => { fc.assert(fc.property( arbitrary_input_intervals(), (intervals) => { @@ -24,10 +24,10 @@ export = { }, )); - test.done(); - }, - 'generated step intervals are valid intervals'(test: Test) { + }); + + test('generated step intervals are valid intervals', () => { fc.assert(fc.property( arbitrary_input_intervals(), (intervals) => { @@ -40,10 +40,10 @@ export = { }, )); - test.done(); - }, - 'generated step intervals are nonoverlapping'(test: Test) { + }); + + test('generated step intervals are nonoverlapping', () => { fc.assert(fc.property( arbitrary_input_intervals(), (intervals) => { @@ -61,10 +61,10 @@ export = { }, ), { verbose: true }); - test.done(); - }, - 'all template intervals occur in input array'(test: Test) { + }); + + test('all template intervals occur in input array', () => { fc.assert(fc.property( arbitrary_input_intervals(), (intervals) => { @@ -83,10 +83,10 @@ export = { }, )); - test.done(); - }, - 'lower alarm uses lower policy'(test: Test) { + }); + + test('lower alarm uses lower policy', () => { fc.assert(fc.property( arbitrary_input_intervals(), (intervals) => { @@ -98,10 +98,10 @@ export = { }, )); - test.done(); - }, - 'upper alarm uses upper policy'(test: Test) { + }); + + test('upper alarm uses upper policy', () => { fc.assert(fc.property( arbitrary_input_intervals(), (intervals) => { @@ -113,10 +113,10 @@ export = { }, )); - test.done(); - }, - 'test step scaling on metric'(test: Test) { + }); + + test('test step scaling on metric', () => { // GIVEN const stack = new cdk.Stack(); const target = createScalableTarget(stack); @@ -132,7 +132,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'StepScaling', ScalingTargetId: { Ref: 'Target3191CF44', @@ -148,12 +148,12 @@ export = { ], }, - })); + }); + - test.done(); - }, + }); - 'step scaling from percentile metric'(test: Test) { + test('step scaling from percentile metric', () => { // GIVEN const stack = new cdk.Stack(); const target = createScalableTarget(stack); @@ -169,14 +169,14 @@ export = { }); // THEN - expect(stack).to(haveResourceLike('AWS::ApplicationAutoScaling::ScalingPolicy', { + expect(stack).toHaveResourceLike('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'StepScaling', StepScalingPolicyConfiguration: { AdjustmentType: 'ChangeInCapacity', MetricAggregationType: 'Average', }, - })); - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + }); + expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { ComparisonOperator: 'GreaterThanOrEqualToThreshold', EvaluationPeriods: 1, AlarmActions: [ @@ -186,12 +186,12 @@ export = { MetricName: 'Metric', Namespace: 'Test', Threshold: 100, - })); + }); - test.done(); - }, - 'step scaling with evaluation period configured'(test: Test) { + }); + + test('step scaling with evaluation period configured', () => { // GIVEN const stack = new cdk.Stack(); const target = createScalableTarget(stack); @@ -209,25 +209,25 @@ export = { }); // THEN - expect(stack).to(haveResourceLike('AWS::ApplicationAutoScaling::ScalingPolicy', { + expect(stack).toHaveResourceLike('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'StepScaling', StepScalingPolicyConfiguration: { AdjustmentType: 'ChangeInCapacity', MetricAggregationType: 'Maximum', }, - })); - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + }); + expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { ComparisonOperator: 'GreaterThanOrEqualToThreshold', EvaluationPeriods: 10, ExtendedStatistic: 'p99', MetricName: 'Metric', Namespace: 'Test', Threshold: 100, - })); + }); - test.done(); - }, -}; + + }); +}); /** * Synthesize the given step scaling setup to a template diff --git a/packages/@aws-cdk/aws-applicationautoscaling/test/test.target-tracking.ts b/packages/@aws-cdk/aws-applicationautoscaling/test/target-tracking.test.ts similarity index 73% rename from packages/@aws-cdk/aws-applicationautoscaling/test/test.target-tracking.ts rename to packages/@aws-cdk/aws-applicationautoscaling/test/target-tracking.test.ts index cbd67cbe9c330..2ec9f8dd07a26 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/test/test.target-tracking.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/test/target-tracking.test.ts @@ -1,12 +1,11 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as appscaling from '../lib'; import { createScalableTarget } from './util'; -export = { - 'test setup target tracking on predefined metric'(test: Test) { +describe('target tracking', () => { + test('test setup target tracking on predefined metric', () => { // GIVEN const stack = new cdk.Stack(); const target = createScalableTarget(stack); @@ -18,19 +17,19 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'EC2SpotFleetRequestAverageCPUUtilization' }, TargetValue: 30, }, - })); + }); + - test.done(); - }, + }); - 'test setup target tracking on predefined metric for lambda'(test: Test) { + test('test setup target tracking on predefined metric for lambda', () => { // GIVEN const stack = new cdk.Stack(); const target = createScalableTarget(stack); @@ -42,19 +41,19 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'LambdaProvisionedConcurrencyUtilization' }, TargetValue: 0.9, }, - })); + }); + - test.done(); - }, + }); - 'test setup target tracking on custom metric'(test: Test) { + test('test setup target tracking on custom metric', () => { // GIVEN const stack = new cdk.Stack(); const target = createScalableTarget(stack); @@ -66,7 +65,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { CustomizedMetricSpecification: { @@ -77,8 +76,8 @@ export = { TargetValue: 30, }, - })); + }); + - test.done(); - }, -}; + }); +}); diff --git a/packages/@aws-cdk/aws-applicationautoscaling/test/test.cron.ts b/packages/@aws-cdk/aws-applicationautoscaling/test/test.cron.ts deleted file mode 100644 index 324b2f8243c97..0000000000000 --- a/packages/@aws-cdk/aws-applicationautoscaling/test/test.cron.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Duration, Stack, Lazy } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import * as appscaling from '../lib'; - -export = { - 'test utc cron, hour only'(test: Test) { - test.equals(appscaling.Schedule.cron({ hour: '18', minute: '0' }).expressionString, 'cron(0 18 * * ? *)'); - test.done(); - }, - - 'test utc cron, hour and minute'(test: Test) { - test.equals(appscaling.Schedule.cron({ hour: '18', minute: '24' }).expressionString, 'cron(24 18 * * ? *)'); - test.done(); - }, - - 'rate must be whole number of minutes'(test: Test) { - test.throws(() => { - appscaling.Schedule.rate(Duration.minutes(0.13456)); - }, /'0.13456 minutes' cannot be converted into a whole number of seconds/); - test.done(); - }, - - 'rate can be in seconds'(test: Test) { - const duration = appscaling.Schedule.rate(Duration.seconds(120)); - test.equal('rate(2 minutes)', duration.expressionString); - test.done(); - }, - - 'rate must not be in seconds when specified as a token'(test: Test) { - test.throws(() => { - appscaling.Schedule.rate(Duration.seconds(Lazy.number({ produce: () => 5 }))); - }, /Allowed units for scheduling/); - test.done(); - }, - - 'rate cannot be 0'(test: Test) { - test.throws(() => { - appscaling.Schedule.rate(Duration.days(0)); - }, /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-eks-legacy/.gitignore b/packages/@aws-cdk/aws-eks-legacy/.gitignore index 752d096cc6b12..633c903ceefc7 100644 --- a/packages/@aws-cdk/aws-eks-legacy/.gitignore +++ b/packages/@aws-cdk/aws-eks-legacy/.gitignore @@ -16,4 +16,5 @@ coverage nyc.config.js !.eslintrc.js -junit.xml \ No newline at end of file +junit.xml +!jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks-legacy/.npmignore b/packages/@aws-cdk/aws-eks-legacy/.npmignore index 9a032ae80868c..e8acf10a468a1 100644 --- a/packages/@aws-cdk/aws-eks-legacy/.npmignore +++ b/packages/@aws-cdk/aws-eks-legacy/.npmignore @@ -24,4 +24,5 @@ tsconfig.json **/cdk.out junit.xml test/ -!*.lit.ts \ No newline at end of file +!*.lit.ts +jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks-legacy/jest.config.js b/packages/@aws-cdk/aws-eks-legacy/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-eks-legacy/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-eks-legacy/package.json b/packages/@aws-cdk/aws-eks-legacy/package.json index 862a69da0f289..1494c1d93020f 100644 --- a/packages/@aws-cdk/aws-eks-legacy/package.json +++ b/packages/@aws-cdk/aws-eks-legacy/package.json @@ -55,7 +55,8 @@ "build+test+extract": "yarn build+test && yarn rosetta:extract" }, "cdk-build": { - "cloudformation": "AWS::EKS" + "cloudformation": "AWS::EKS", + "jest": true }, "keywords": [ "aws", @@ -70,11 +71,11 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.32", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "nodeunit": "^0.11.3", + "jest": "^26.6.3", "pkglint": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-eks-legacy/test/test.awsauth.ts b/packages/@aws-cdk/aws-eks-legacy/test/awsauth.test.ts similarity index 85% rename from packages/@aws-cdk/aws-eks-legacy/test/test.awsauth.ts rename to packages/@aws-cdk/aws-eks-legacy/test/awsauth.test.ts index 2ebdcde32dbcf..d9eacb3f183d1 100644 --- a/packages/@aws-cdk/aws-eks-legacy/test/test.awsauth.ts +++ b/packages/@aws-cdk/aws-eks-legacy/test/awsauth.test.ts @@ -1,14 +1,13 @@ -import { countResources, expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as iam from '@aws-cdk/aws-iam'; -import { Test } from 'nodeunit'; import { Cluster, KubernetesResource } from '../lib'; import { AwsAuth } from '../lib/aws-auth'; import { testFixtureNoVpc } from './util'; /* eslint-disable max-len */ -export = { - 'empty aws-auth'(test: Test) { +describe('awsauth', () => { + test('empty aws-auth', () => { // GIVEN const { stack } = testFixtureNoVpc(); const cluster = new Cluster(stack, 'cluster'); @@ -17,18 +16,18 @@ export = { new AwsAuth(stack, 'AwsAuth', { cluster }); // THEN - expect(stack).to(haveResource(KubernetesResource.RESOURCE_TYPE, { + expect(stack).toHaveResource(KubernetesResource.RESOURCE_TYPE, { Manifest: JSON.stringify([{ apiVersion: 'v1', kind: 'ConfigMap', metadata: { name: 'aws-auth', namespace: 'kube-system' }, data: { mapRoles: '[]', mapUsers: '[]', mapAccounts: '[]' }, }]), - })); - test.done(); - }, + }); - 'addRoleMapping and addUserMapping can be used to define the aws-auth ConfigMap'(test: Test) { + }); + + test('addRoleMapping and addUserMapping can be used to define the aws-auth ConfigMap', () => { // GIVEN const { stack } = testFixtureNoVpc(); const cluster = new Cluster(stack, 'Cluster'); @@ -44,8 +43,8 @@ export = { cluster.awsAuth.addAccount('5566776655'); // THEN - expect(stack).to(countResources(KubernetesResource.RESOURCE_TYPE, 1)); - expect(stack).to(haveResource(KubernetesResource.RESOURCE_TYPE, { + expect(stack).toCountResources(KubernetesResource.RESOURCE_TYPE, 1); + expect(stack).toHaveResource(KubernetesResource.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -89,12 +88,12 @@ export = { ], ], }, - })); + }); + - test.done(); - }, + }); - 'imported users and roles can be also be used'(test: Test) { + test('imported users and roles can be also be used', () => { // GIVEN const { stack } = testFixtureNoVpc(); const cluster = new Cluster(stack, 'Cluster'); @@ -106,7 +105,7 @@ export = { cluster.awsAuth.addUserMapping(user, { groups: ['group2'] }); // THEN - expect(stack).to(haveResource(KubernetesResource.RESOURCE_TYPE, { + expect(stack).toHaveResource(KubernetesResource.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -130,8 +129,8 @@ export = { ], ], }, - })); + }); + - test.done(); - }, -}; + }); +}); diff --git a/packages/@aws-cdk/aws-eks-legacy/test/test.cluster.ts b/packages/@aws-cdk/aws-eks-legacy/test/cluster.test.ts similarity index 70% rename from packages/@aws-cdk/aws-eks-legacy/test/test.cluster.ts rename to packages/@aws-cdk/aws-eks-legacy/test/cluster.test.ts index cfcc21a8ce50e..f395348cc6f1e 100644 --- a/packages/@aws-cdk/aws-eks-legacy/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-eks-legacy/test/cluster.test.ts @@ -1,16 +1,15 @@ -import { expect, haveResource, haveResourceLike, not } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as eks from '../lib'; import { spotInterruptHandler } from '../lib/spot-interrupt-handler'; import { testFixture, testFixtureNoVpc } from './util'; /* eslint-disable max-len */ -export = { - 'a default cluster spans all subnets'(test: Test) { +describe('cluster', () => { + test('a default cluster spans all subnets', () => { // GIVEN const { stack, vpc } = testFixture(); @@ -18,7 +17,7 @@ export = { new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: false, defaultCapacity: 0 }); // THEN - expect(stack).to(haveResourceLike('AWS::EKS::Cluster', { + expect(stack).toHaveResourceLike('AWS::EKS::Cluster', { ResourcesVpcConfig: { SubnetIds: [ { Ref: 'VPCPublicSubnet1SubnetB4246D30' }, @@ -27,12 +26,12 @@ export = { { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, ], }, - })); + }); + - test.done(); - }, + }); - 'if "vpc" is not specified, vpc with default configuration will be created'(test: Test) { + test('if "vpc" is not specified, vpc with default configuration will be created', () => { // GIVEN const { stack } = testFixtureNoVpc(); @@ -40,13 +39,13 @@ export = { new eks.Cluster(stack, 'cluster'); // THEN - expect(stack).to(haveResource('AWS::EC2::VPC')); - test.done(); - }, + expect(stack).toHaveResource('AWS::EC2::VPC'); - 'default capacity': { + }); - 'x2 m5.large by default'(test: Test) { + describe('default capacity', () => { + + test('x2 m5.large by default', () => { // GIVEN const { stack } = testFixtureNoVpc(); @@ -54,13 +53,13 @@ export = { const cluster = new eks.Cluster(stack, 'cluster'); // THEN - test.ok(cluster.defaultCapacity); - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { DesiredCapacity: '2' })); - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 'm5.large' })); - test.done(); - }, + expect(cluster.defaultCapacity).toBeDefined(); + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { DesiredCapacity: '2' }); + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 'm5.large' }); + + }); - 'quantity and type can be customized'(test: Test) { + test('quantity and type can be customized', () => { // GIVEN const { stack } = testFixtureNoVpc(); @@ -71,13 +70,13 @@ export = { }); // THEN - test.ok(cluster.defaultCapacity); - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { DesiredCapacity: '10' })); - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 'm2.xlarge' })); - test.done(); - }, + expect(cluster.defaultCapacity).toBeDefined(); + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { DesiredCapacity: '10' }); + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 'm2.xlarge' }); + + }); - 'defaultCapacity=0 will not allocate at all'(test: Test) { + test('defaultCapacity=0 will not allocate at all', () => { // GIVEN const { stack } = testFixtureNoVpc(); @@ -85,14 +84,14 @@ export = { const cluster = new eks.Cluster(stack, 'cluster', { defaultCapacity: 0 }); // THEN - test.ok(!cluster.defaultCapacity); - expect(stack).notTo(haveResource('AWS::AutoScaling::AutoScalingGroup')); - expect(stack).notTo(haveResource('AWS::AutoScaling::LaunchConfiguration')); - test.done(); - }, - }, - - 'creating a cluster tags the private VPC subnets'(test: Test) { + expect(cluster.defaultCapacity).toBeUndefined(); + expect(stack).not.toHaveResource('AWS::AutoScaling::AutoScalingGroup'); + expect(stack).not.toHaveResource('AWS::AutoScaling::LaunchConfiguration'); + + }); + }); + + test('creating a cluster tags the private VPC subnets', () => { // GIVEN const { stack, vpc } = testFixture(); @@ -100,19 +99,19 @@ export = { new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: false, defaultCapacity: 0 }); // THEN - expect(stack).to(haveResource('AWS::EC2::Subnet', { + expect(stack).toHaveResource('AWS::EC2::Subnet', { Tags: [ { Key: 'aws-cdk:subnet-name', Value: 'Private' }, { Key: 'aws-cdk:subnet-type', Value: 'Private' }, { Key: 'kubernetes.io/role/internal-elb', Value: '1' }, { Key: 'Name', Value: 'Stack/VPC/PrivateSubnet1' }, ], - })); + }); + - test.done(); - }, + }); - 'creating a cluster tags the public VPC subnets'(test: Test) { + test('creating a cluster tags the public VPC subnets', () => { // GIVEN const { stack, vpc } = testFixture(); @@ -120,7 +119,7 @@ export = { new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: false, defaultCapacity: 0 }); // THEN - expect(stack).to(haveResource('AWS::EC2::Subnet', { + expect(stack).toHaveResource('AWS::EC2::Subnet', { MapPublicIpOnLaunch: true, Tags: [ { Key: 'aws-cdk:subnet-name', Value: 'Public' }, @@ -128,12 +127,12 @@ export = { { Key: 'kubernetes.io/role/elb', Value: '1' }, { Key: 'Name', Value: 'Stack/VPC/PublicSubnet1' }, ], - })); + }); + - test.done(); - }, + }); - 'adding capacity creates an ASG with tags'(test: Test) { + test('adding capacity creates an ASG with tags', () => { // GIVEN const { stack, vpc } = testFixture(); const cluster = new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: false, defaultCapacity: 0 }); @@ -144,7 +143,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { Tags: [ { Key: { 'Fn::Join': ['', ['kubernetes.io/cluster/', { Ref: 'ClusterEB0386A7' }]] }, @@ -157,12 +156,12 @@ export = { Value: 'Stack/Cluster/Default', }, ], - })); + }); - test.done(); - }, - 'exercise export/import'(test: Test) { + }); + + test('exercise export/import', () => { // GIVEN const { stack: stack1, vpc, app } = testFixture(); const stack2 = new cdk.Stack(app, 'stack2', { env: { region: 'us-east-1' } }); @@ -182,7 +181,7 @@ export = { new cdk.CfnOutput(stack2, 'ClusterARN', { value: imported.clusterArn }); // THEN - expect(stack2).toMatch({ + expect(stack2).toMatchTemplate({ Outputs: { ClusterARN: { Value: { @@ -191,23 +190,23 @@ export = { }, }, }); - test.done(); - }, - 'disabled features when kubectl is disabled'(test: Test) { + }); + + test('disabled features when kubectl is disabled', () => { // GIVEN const { stack, vpc } = testFixture(); const cluster = new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: false, defaultCapacity: 0 }); - test.throws(() => cluster.awsAuth, /Cannot define aws-auth mappings if kubectl is disabled/); - test.throws(() => cluster.addResource('foo', {}), /Cannot define a KubernetesManifest resource on a cluster with kubectl disabled/); - test.throws(() => cluster.addCapacity('boo', { instanceType: new ec2.InstanceType('r5d.24xlarge'), mapRole: true }), + expect(() => cluster.awsAuth).toThrow(/Cannot define aws-auth mappings if kubectl is disabled/); + expect(() => cluster.addResource('foo', {})).toThrow(/Cannot define a KubernetesManifest resource on a cluster with kubectl disabled/); + expect(() => cluster.addCapacity('boo', { instanceType: new ec2.InstanceType('r5d.24xlarge'), mapRole: true })).toThrow( /Cannot map instance IAM role to RBAC if kubectl is disabled for the cluster/); - test.throws(() => new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart' }), /Cannot define a Helm chart on a cluster with kubectl disabled/); - test.done(); - }, + expect(() => new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart' })).toThrow(/Cannot define a Helm chart on a cluster with kubectl disabled/); - 'mastersRole can be used to map an IAM role to "system:masters" (required kubectl)'(test: Test) { + }); + + test('mastersRole can be used to map an IAM role to "system:masters" (required kubectl)', () => { // GIVEN const { stack, vpc } = testFixture(); const role = new iam.Role(stack, 'role', { assumedBy: new iam.AnyPrincipal() }); @@ -216,7 +215,7 @@ export = { new eks.Cluster(stack, 'Cluster', { vpc, mastersRole: role, defaultCapacity: 0 }); // THEN - expect(stack).to(haveResource(eks.KubernetesResource.RESOURCE_TYPE, { + expect(stack).toHaveResource(eks.KubernetesResource.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -232,12 +231,12 @@ export = { ], ], }, - })); + }); - test.done(); - }, - 'addResource can be used to apply k8s manifests on this cluster'(test: Test) { + }); + + test('addResource can be used to apply k8s manifests on this cluster', () => { // GIVEN const { stack, vpc } = testFixture(); const cluster = new eks.Cluster(stack, 'Cluster', { vpc, defaultCapacity: 0 }); @@ -247,18 +246,18 @@ export = { cluster.addResource('manifest2', { bar: 123 }, { boor: [1, 2, 3] }); // THEN - expect(stack).to(haveResource(eks.KubernetesResource.RESOURCE_TYPE, { + expect(stack).toHaveResource(eks.KubernetesResource.RESOURCE_TYPE, { Manifest: '[{"foo":123}]', - })); + }); - expect(stack).to(haveResource(eks.KubernetesResource.RESOURCE_TYPE, { + expect(stack).toHaveResource(eks.KubernetesResource.RESOURCE_TYPE, { Manifest: '[{"bar":123},{"boor":[1,2,3]}]', - })); + }); - test.done(); - }, - 'when kubectl is enabled (default) adding capacity will automatically map its IAM role'(test: Test) { + }); + + test('when kubectl is enabled (default) adding capacity will automatically map its IAM role', () => { // GIVEN const { stack, vpc } = testFixture(); const cluster = new eks.Cluster(stack, 'Cluster', { vpc, defaultCapacity: 0 }); @@ -269,7 +268,7 @@ export = { }); // THEN - expect(stack).to(haveResource(eks.KubernetesResource.RESOURCE_TYPE, { + expect(stack).toHaveResource(eks.KubernetesResource.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -285,12 +284,12 @@ export = { ], ], }, - })); + }); - test.done(); - }, - 'addCapacity will *not* map the IAM role if mapRole is false'(test: Test) { + }); + + test('addCapacity will *not* map the IAM role if mapRole is false', () => { // GIVEN const { stack, vpc } = testFixture(); const cluster = new eks.Cluster(stack, 'Cluster', { vpc, defaultCapacity: 0 }); @@ -302,11 +301,11 @@ export = { }); // THEN - expect(stack).to(not(haveResource(eks.KubernetesResource.RESOURCE_TYPE))); - test.done(); - }, + expect(stack).not.toHaveResource(eks.KubernetesResource.RESOURCE_TYPE); + + }); - 'addCapacity will *not* map the IAM role if kubectl is disabled'(test: Test) { + test('addCapacity will *not* map the IAM role if kubectl is disabled', () => { // GIVEN const { stack, vpc } = testFixture(); const cluster = new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: false, defaultCapacity: 0 }); @@ -317,12 +316,12 @@ export = { }); // THEN - expect(stack).to(not(haveResource(eks.KubernetesResource.RESOURCE_TYPE))); - test.done(); - }, + expect(stack).not.toHaveResource(eks.KubernetesResource.RESOURCE_TYPE); - 'outputs': { - 'aws eks update-kubeconfig is the only output synthesized by default'(test: Test) { + }); + + describe('outputs', () => { + test('aws eks update-kubeconfig is the only output synthesized by default', () => { // GIVEN const { app, stack } = testFixtureNoVpc(); @@ -332,14 +331,14 @@ export = { // THEN const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - test.deepEqual(template.Outputs, { + expect(template.Outputs).toEqual({ ClusterConfigCommand43AAE40F: { Value: { 'Fn::Join': ['', ['aws eks update-kubeconfig --name ', { Ref: 'Cluster9EE0221C' }, ' --region us-east-1']] } }, ClusterGetTokenCommand06AE992E: { Value: { 'Fn::Join': ['', ['aws eks get-token --cluster-name ', { Ref: 'Cluster9EE0221C' }, ' --region us-east-1']] } }, }); - test.done(); - }, - 'if masters role is defined, it should be included in the config command'(test: Test) { + }); + + test('if masters role is defined, it should be included in the config command', () => { // GIVEN const { app, stack } = testFixtureNoVpc(); @@ -350,14 +349,14 @@ export = { // THEN const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - test.deepEqual(template.Outputs, { + expect(template.Outputs).toEqual({ ClusterConfigCommand43AAE40F: { Value: { 'Fn::Join': ['', ['aws eks update-kubeconfig --name ', { Ref: 'Cluster9EE0221C' }, ' --region us-east-1 --role-arn ', { 'Fn::GetAtt': ['masters0D04F23D', 'Arn'] }]] } }, ClusterGetTokenCommand06AE992E: { Value: { 'Fn::Join': ['', ['aws eks get-token --cluster-name ', { Ref: 'Cluster9EE0221C' }, ' --region us-east-1 --role-arn ', { 'Fn::GetAtt': ['masters0D04F23D', 'Arn'] }]] } }, }); - test.done(); - }, - 'if `outputConfigCommand=false` will disabled the output'(test: Test) { + }); + + test('if `outputConfigCommand=false` will disabled the output', () => { // GIVEN const { app, stack } = testFixtureNoVpc(); @@ -371,11 +370,11 @@ export = { // THEN const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - test.ok(!template.Outputs); // no outputs - test.done(); - }, + expect(template.Outputs).toBeUndefined(); // no outputs + + }); - '`outputClusterName` can be used to synthesize an output with the cluster name'(test: Test) { + test('`outputClusterName` can be used to synthesize an output with the cluster name', () => { // GIVEN const { app, stack } = testFixtureNoVpc(); @@ -388,13 +387,13 @@ export = { // THEN const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - test.deepEqual(template.Outputs, { + expect(template.Outputs).toEqual({ ClusterClusterNameEB26049E: { Value: { Ref: 'Cluster9EE0221C' } }, }); - test.done(); - }, - '`outputMastersRoleArn` can be used to synthesize an output with the arn of the masters role if defined'(test: Test) { + }); + + test('`outputMastersRoleArn` can be used to synthesize an output with the arn of the masters role if defined', () => { // GIVEN const { app, stack } = testFixtureNoVpc(); @@ -408,13 +407,13 @@ export = { // THEN const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - test.deepEqual(template.Outputs, { + expect(template.Outputs).toEqual({ ClusterMastersRoleArnB15964B1: { Value: { 'Fn::GetAtt': ['masters0D04F23D', 'Arn'] } }, }); - test.done(); - }, - 'when adding capacity, instance role ARN will not be outputed only if we do not auto-map aws-auth'(test: Test) { + }); + + test('when adding capacity, instance role ARN will not be outputed only if we do not auto-map aws-auth', () => { // GIVEN const { app, stack } = testFixtureNoVpc(); @@ -427,18 +426,18 @@ export = { // THEN const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - test.deepEqual(template.Outputs, { + expect(template.Outputs).toEqual({ ClusterDefaultCapacityInstanceRoleARN7DADF219: { Value: { 'Fn::GetAtt': ['ClusterDefaultCapacityInstanceRole3E209969', 'Arn'] }, }, }); - test.done(); - }, - }, - 'boostrap user-data': { + }); + }); + + describe('boostrap user-data', () => { - 'rendered by default for ASGs'(test: Test) { + test('rendered by default for ASGs', () => { // GIVEN const { app, stack } = testFixtureNoVpc(); const cluster = new eks.Cluster(stack, 'Cluster', { defaultCapacity: 0 }); @@ -449,11 +448,11 @@ export = { // THEN const template = app.synth().getStackByName(stack.stackName).template; const userData = template.Resources.ClusterMyCapcityLaunchConfig58583345.Properties.UserData; - test.deepEqual(userData, { 'Fn::Base64': { 'Fn::Join': ['', ['#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ', { Ref: 'Cluster9EE0221C' }, ' --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true\n/opt/aws/bin/cfn-signal --exit-code $? --stack Stack --resource ClusterMyCapcityASGD4CD8B97 --region us-east-1']] } }); - test.done(); - }, + expect(userData).toEqual({ 'Fn::Base64': { 'Fn::Join': ['', ['#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ', { Ref: 'Cluster9EE0221C' }, ' --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true\n/opt/aws/bin/cfn-signal --exit-code $? --stack Stack --resource ClusterMyCapcityASGD4CD8B97 --region us-east-1']] } }); - 'not rendered if bootstrap is disabled'(test: Test) { + }); + + test('not rendered if bootstrap is disabled', () => { // GIVEN const { app, stack } = testFixtureNoVpc(); const cluster = new eks.Cluster(stack, 'Cluster', { defaultCapacity: 0 }); @@ -467,12 +466,12 @@ export = { // THEN const template = app.synth().getStackByName(stack.stackName).template; const userData = template.Resources.ClusterMyCapcityLaunchConfig58583345.Properties.UserData; - test.deepEqual(userData, { 'Fn::Base64': '#!/bin/bash' }); - test.done(); - }, + expect(userData).toEqual({ 'Fn::Base64': '#!/bin/bash' }); + + }); // cursory test for options: see test.user-data.ts for full suite - 'bootstrap options'(test: Test) { + test('bootstrap options', () => { // GIVEN const { app, stack } = testFixtureNoVpc(); const cluster = new eks.Cluster(stack, 'Cluster', { defaultCapacity: 0 }); @@ -488,13 +487,13 @@ export = { // THEN const template = app.synth().getStackByName(stack.stackName).template; const userData = template.Resources.ClusterMyCapcityLaunchConfig58583345.Properties.UserData; - test.deepEqual(userData, { 'Fn::Base64': { 'Fn::Join': ['', ['#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ', { Ref: 'Cluster9EE0221C' }, ' --kubelet-extra-args "--node-labels lifecycle=OnDemand --node-labels FOO=42" --use-max-pods true\n/opt/aws/bin/cfn-signal --exit-code $? --stack Stack --resource ClusterMyCapcityASGD4CD8B97 --region us-east-1']] } }); - test.done(); - }, + expect(userData).toEqual({ 'Fn::Base64': { 'Fn::Join': ['', ['#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ', { Ref: 'Cluster9EE0221C' }, ' --kubelet-extra-args "--node-labels lifecycle=OnDemand --node-labels FOO=42" --use-max-pods true\n/opt/aws/bin/cfn-signal --exit-code $? --stack Stack --resource ClusterMyCapcityASGD4CD8B97 --region us-east-1']] } }); - 'spot instances': { + }); + + describe('spot instances', () => { - 'nodes labeled an tainted accordingly'(test: Test) { + test('nodes labeled an tainted accordingly', () => { // GIVEN const { app, stack } = testFixtureNoVpc(); const cluster = new eks.Cluster(stack, 'Cluster', { defaultCapacity: 0 }); @@ -508,11 +507,11 @@ export = { // THEN const template = app.synth().getStackByName(stack.stackName).template; const userData = template.Resources.ClusterMyCapcityLaunchConfig58583345.Properties.UserData; - test.deepEqual(userData, { 'Fn::Base64': { 'Fn::Join': ['', ['#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ', { Ref: 'Cluster9EE0221C' }, ' --kubelet-extra-args "--node-labels lifecycle=Ec2Spot --register-with-taints=spotInstance=true:PreferNoSchedule" --use-max-pods true\n/opt/aws/bin/cfn-signal --exit-code $? --stack Stack --resource ClusterMyCapcityASGD4CD8B97 --region us-east-1']] } }); - test.done(); - }, + expect(userData).toEqual({ 'Fn::Base64': { 'Fn::Join': ['', ['#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ', { Ref: 'Cluster9EE0221C' }, ' --kubelet-extra-args "--node-labels lifecycle=Ec2Spot --register-with-taints=spotInstance=true:PreferNoSchedule" --use-max-pods true\n/opt/aws/bin/cfn-signal --exit-code $? --stack Stack --resource ClusterMyCapcityASGD4CD8B97 --region us-east-1']] } }); - 'if kubectl is enabled, the interrupt handler is added'(test: Test) { + }); + + test('if kubectl is enabled, the interrupt handler is added', () => { // GIVEN const { stack } = testFixtureNoVpc(); const cluster = new eks.Cluster(stack, 'Cluster', { defaultCapacity: 0 }); @@ -524,11 +523,11 @@ export = { }); // THEN - expect(stack).to(haveResource(eks.KubernetesResource.RESOURCE_TYPE, { Manifest: JSON.stringify(spotInterruptHandler()) })); - test.done(); - }, + expect(stack).toHaveResource(eks.KubernetesResource.RESOURCE_TYPE, { Manifest: JSON.stringify(spotInterruptHandler()) }); + + }); - 'if kubectl is disabled, interrupt handler is not added'(test: Test) { + test('if kubectl is disabled, interrupt handler is not added', () => { // GIVEN const { stack } = testFixtureNoVpc(); const cluster = new eks.Cluster(stack, 'Cluster', { defaultCapacity: 0, kubectlEnabled: false }); @@ -540,29 +539,29 @@ export = { }); // THEN - expect(stack).notTo(haveResource(eks.KubernetesResource.RESOURCE_TYPE)); - test.done(); - }, + expect(stack).not.toHaveResource(eks.KubernetesResource.RESOURCE_TYPE); - }, + }); - }, + }); - 'if bootstrap is disabled cannot specify options'(test: Test) { + }); + + test('if bootstrap is disabled cannot specify options', () => { // GIVEN const { stack } = testFixtureNoVpc(); const cluster = new eks.Cluster(stack, 'Cluster', { defaultCapacity: 0 }); // THEN - test.throws(() => cluster.addCapacity('MyCapcity', { + expect(() => cluster.addCapacity('MyCapcity', { instanceType: new ec2.InstanceType('m3.xlargs'), bootstrapEnabled: false, bootstrapOptions: { awsApiRetryAttempts: 10 }, - }), /Cannot specify "bootstrapOptions" if "bootstrapEnabled" is false/); - test.done(); - }, + })).toThrow(/Cannot specify "bootstrapOptions" if "bootstrapEnabled" is false/); + + }); - 'EKS-Optimized AMI with GPU support'(test: Test) { + test('EKS-Optimized AMI with GPU support', () => { // GIVEN const { app, stack } = testFixtureNoVpc(); @@ -575,9 +574,9 @@ export = { // THEN const assembly = app.synth(); const parameters = assembly.getStackByName(stack.stackName).template.Parameters; - test.ok(Object.entries(parameters).some( + expect(Object.entries(parameters).some( ([k, v]) => k.startsWith('SsmParameterValueawsserviceeksoptimizedami') && (v as any).Default.includes('amazon-linux2-gpu'), - ), 'EKS AMI with GPU should be in ssm parameters'); - test.done(); - }, -}; + )).toEqual(true); + + }); +}); diff --git a/packages/@aws-cdk/aws-eks-legacy/test/helm-chart.test.ts b/packages/@aws-cdk/aws-eks-legacy/test/helm-chart.test.ts new file mode 100644 index 0000000000000..4101ce02168d9 --- /dev/null +++ b/packages/@aws-cdk/aws-eks-legacy/test/helm-chart.test.ts @@ -0,0 +1,54 @@ +import '@aws-cdk/assert-internal/jest'; +import * as eks from '../lib'; +import { testFixtureCluster } from './util'; + +/* eslint-disable max-len */ + +describe('helm chart', () => { + describe('add Helm chart', () => { + test('should have default namespace', () => { + // GIVEN + const { stack, cluster } = testFixtureCluster(); + + // WHEN + new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart' }); + + // THEN + expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Namespace: 'default' }); + + }); + test('should have a lowercase default release name', () => { + // GIVEN + const { stack, cluster } = testFixtureCluster(); + + // WHEN + new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart' }); + + // THEN + expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Release: 'stackmychartff398361' }); + + }); + test('should trim the last 63 of the default release name', () => { + // GIVEN + const { stack, cluster } = testFixtureCluster(); + + // WHEN + new eks.HelmChart(stack, 'MyChartNameWhichISMostProbablyLongerThenSixtyThreeCharacters', { cluster, chart: 'chart' }); + + // THEN + expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Release: 'rtnamewhichismostprobablylongerthensixtythreecharactersb800614d' }); + + }); + test('with values', () => { + // GIVEN + const { stack, cluster } = testFixtureCluster(); + + // WHEN + new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart', values: { foo: 123 } }); + + // THEN + expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Values: '{\"foo\":123}' }); + + }); + }); +}); diff --git a/packages/@aws-cdk/aws-eks-legacy/test/test.manifest.ts b/packages/@aws-cdk/aws-eks-legacy/test/manifest.test.ts similarity index 86% rename from packages/@aws-cdk/aws-eks-legacy/test/test.manifest.ts rename to packages/@aws-cdk/aws-eks-legacy/test/manifest.test.ts index 32e418352f808..962e0c0129821 100644 --- a/packages/@aws-cdk/aws-eks-legacy/test/test.manifest.ts +++ b/packages/@aws-cdk/aws-eks-legacy/test/manifest.test.ts @@ -1,12 +1,11 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; -import { Test } from 'nodeunit'; +import '@aws-cdk/assert-internal/jest'; import { Cluster, KubernetesResource } from '../lib'; import { testFixtureNoVpc } from './util'; /* eslint-disable max-len */ -export = { - 'basic usage'(test: Test) { +describe('manifest', () => { + test('basic usage', () => { // GIVEN const { stack } = testFixtureNoVpc(); const cluster = new Cluster(stack, 'cluster'); @@ -69,9 +68,9 @@ export = { manifest, }); - expect(stack).to(haveResource(KubernetesResource.RESOURCE_TYPE, { + expect(stack).toHaveResource(KubernetesResource.RESOURCE_TYPE, { Manifest: JSON.stringify(manifest), - })); - test.done(); - }, -}; \ No newline at end of file + }); + + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks-legacy/test/test.cluster-resource.ts b/packages/@aws-cdk/aws-eks-legacy/test/test.cluster-resource.ts deleted file mode 100644 index 90bddc141b05e..0000000000000 --- a/packages/@aws-cdk/aws-eks-legacy/test/test.cluster-resource.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Test } from 'nodeunit'; - -export = { - create: { - 'defaults'(test: Test) { - test.done(); - }, - 'with no specific version'(test: Test) { - test.done(); - }, - }, - - update: { - 'requires replacement': { - 'change of "name" creates a new cluster with the new name and deletes the old cluster'(test: Test) { - test.done(); - }, - 'change of "resourcesVpcConfig"'(test: Test) { - test.done(); - }, - 'change of "roleArn"'(test: Test) { - test.done(); - }, - 'change of "roleArn" and "version"'(test: Test) { - test.done(); - }, - }, - - 'in-place': { - 'version change': { - 'from undefined to a specific value'(test: Test) { - test.done(); - }, - - 'from a specific value to another value'(test: Test) { - test.done(); - }, - - 'fails from specific value to undefined'(test: Test) { - test.done(); - }, - }, - }, - - 'update failure returns the previous physical name': { - - 'for "version" updates'(test: Test) { - test.done(); - }, - - 'for "name" updates'(test: Test) { - test.done(); - }, - - 'for "roleArn" updates'(test: Test) { - test.done(); - }, - - }, - }, - - delete: { - 'delete failure': { - 'returns correct physical name'(test: Test) { - test.done(); - }, - }, - }, -}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks-legacy/test/test.helm-chart.ts b/packages/@aws-cdk/aws-eks-legacy/test/test.helm-chart.ts deleted file mode 100644 index e595e3d00a5ac..0000000000000 --- a/packages/@aws-cdk/aws-eks-legacy/test/test.helm-chart.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; -import { Test } from 'nodeunit'; -import * as eks from '../lib'; -import { testFixtureCluster } from './util'; - -/* eslint-disable max-len */ - -export = { - 'add Helm chart': { - 'should have default namespace'(test: Test) { - // GIVEN - const { stack, cluster } = testFixtureCluster(); - - // WHEN - new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart' }); - - // THEN - expect(stack).to(haveResource(eks.HelmChart.RESOURCE_TYPE, { Namespace: 'default' })); - test.done(); - }, - 'should have a lowercase default release name'(test: Test) { - // GIVEN - const { stack, cluster } = testFixtureCluster(); - - // WHEN - new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart' }); - - // THEN - expect(stack).to(haveResource(eks.HelmChart.RESOURCE_TYPE, { Release: 'stackmychartff398361' })); - test.done(); - }, - 'should trim the last 63 of the default release name'(test: Test) { - // GIVEN - const { stack, cluster } = testFixtureCluster(); - - // WHEN - new eks.HelmChart(stack, 'MyChartNameWhichISMostProbablyLongerThenSixtyThreeCharacters', { cluster, chart: 'chart' }); - - // THEN - expect(stack).to(haveResource(eks.HelmChart.RESOURCE_TYPE, { Release: 'rtnamewhichismostprobablylongerthensixtythreecharactersb800614d' })); - test.done(); - }, - 'with values'(test: Test) { - // GIVEN - const { stack, cluster } = testFixtureCluster(); - - // WHEN - new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart', values: { foo: 123 } }); - - // THEN - expect(stack).to(haveResource(eks.HelmChart.RESOURCE_TYPE, { Values: '{\"foo\":123}' })); - test.done(); - }, - }, -}; diff --git a/packages/@aws-cdk/aws-eks-legacy/test/test.user-data.ts b/packages/@aws-cdk/aws-eks-legacy/test/user-data.test.ts similarity index 73% rename from packages/@aws-cdk/aws-eks-legacy/test/test.user-data.ts rename to packages/@aws-cdk/aws-eks-legacy/test/user-data.test.ts index be8341bf6834b..4189e720d2f97 100644 --- a/packages/@aws-cdk/aws-eks-legacy/test/test.user-data.ts +++ b/packages/@aws-cdk/aws-eks-legacy/test/user-data.test.ts @@ -1,13 +1,12 @@ import * as autoscaling from '@aws-cdk/aws-autoscaling'; import * as ec2 from '@aws-cdk/aws-ec2'; import { App, Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import { renderUserData } from '../lib/user-data'; /* eslint-disable max-len */ -export = { - 'default user data'(test: Test) { +describe('user data', () => { + test('default user data', () => { // GIVEN const { asg, stack } = newFixtures(); @@ -15,16 +14,16 @@ export = { const userData = stack.resolve(renderUserData('my-cluster-name', asg)); // THEN - test.deepEqual(userData, [ + expect(userData).toEqual([ 'set -o xtrace', '/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true', '/opt/aws/bin/cfn-signal --exit-code $? --stack my-stack --resource ASG46ED3070 --region us-west-33', ]); - test.done(); - }, - '--use-max-pods=true'(test: Test) { + }); + + test('--use-max-pods=true', () => { // GIVEN const { asg, stack } = newFixtures(); @@ -34,11 +33,11 @@ export = { })); // THEN - test.deepEqual(userData[1], '/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true'); - test.done(); - }, + expect(userData[1]).toEqual('/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true'); - '--use-max-pods=false'(test: Test) { + }); + + test('--use-max-pods=false', () => { // GIVEN const { asg, stack } = newFixtures(); @@ -48,11 +47,11 @@ export = { })); // THEN - test.deepEqual(userData[1], '/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods false'); - test.done(); - }, + expect(userData[1]).toEqual('/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods false'); + + }); - '--aws-api-retry-attempts'(test: Test) { + test('--aws-api-retry-attempts', () => { // GIVEN const { asg, stack } = newFixtures(); @@ -62,11 +61,11 @@ export = { })); // THEN - test.deepEqual(userData[1], '/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true --aws-api-retry-attempts 123'); - test.done(); - }, + expect(userData[1]).toEqual('/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true --aws-api-retry-attempts 123'); + + }); - '--docker-config-json'(test: Test) { + test('--docker-config-json', () => { // GIVEN const { asg } = newFixtures(); @@ -76,11 +75,11 @@ export = { }); // THEN - test.deepEqual(userData[1], '/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true --docker-config-json \'{"docker":123}\''); - test.done(); - }, + expect(userData[1]).toEqual('/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true --docker-config-json \'{"docker":123}\''); + + }); - '--enable-docker-bridge=true'(test: Test) { + test('--enable-docker-bridge=true', () => { // GIVEN const { asg, stack } = newFixtures(); @@ -90,11 +89,11 @@ export = { })); // THEN - test.deepEqual(userData[1], '/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true --enable-docker-bridge'); - test.done(); - }, + expect(userData[1]).toEqual('/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true --enable-docker-bridge'); - '--enable-docker-bridge=false'(test: Test) { + }); + + test('--enable-docker-bridge=false', () => { // GIVEN const { asg, stack } = newFixtures(); @@ -104,11 +103,11 @@ export = { })); // THEN - test.deepEqual(userData[1], '/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true'); - test.done(); - }, + expect(userData[1]).toEqual('/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true'); - '--kubelet-extra-args'(test: Test) { + }); + + test('--kubelet-extra-args', () => { // GIVEN const { asg, stack } = newFixtures(); @@ -118,11 +117,11 @@ export = { })); // THEN - test.deepEqual(userData[1], '/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand --extra-args-for --kubelet" --use-max-pods true'); - test.done(); - }, + expect(userData[1]).toEqual('/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand --extra-args-for --kubelet" --use-max-pods true'); + + }); - 'arbitrary additional bootstrap arguments can be passed through "additionalArgs"'(test: Test) { + test('arbitrary additional bootstrap arguments can be passed through "additionalArgs"', () => { // GIVEN const { asg, stack } = newFixtures(); @@ -132,11 +131,11 @@ export = { })); // THEN - test.deepEqual(userData[1], '/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true --apiserver-endpoint 1111 --foo-bar'); - test.done(); - }, + expect(userData[1]).toEqual('/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true --apiserver-endpoint 1111 --foo-bar'); + + }); - 'if asg has spot instances, the correct label and taint is used'(test: Test) { + test('if asg has spot instances, the correct label and taint is used', () => { // GIVEN const { asg, stack } = newFixtures(true); @@ -146,10 +145,10 @@ export = { })); // THEN - test.deepEqual(userData[1], '/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=Ec2Spot --register-with-taints=spotInstance=true:PreferNoSchedule --node-labels X=y" --use-max-pods true'); - test.done(); - }, -}; + expect(userData[1]).toEqual('/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=Ec2Spot --register-with-taints=spotInstance=true:PreferNoSchedule --node-labels X=y" --use-max-pods true'); + + }); +}); function newFixtures(spot = false) { const app = new App(); diff --git a/packages/@aws-cdk/aws-events/.gitignore b/packages/@aws-cdk/aws-events/.gitignore index dcc1dc41e477f..17a41566f0002 100644 --- a/packages/@aws-cdk/aws-events/.gitignore +++ b/packages/@aws-cdk/aws-events/.gitignore @@ -15,4 +15,5 @@ nyc.config.js *.snk !.eslintrc.js -junit.xml \ No newline at end of file +junit.xml +!jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events/.npmignore b/packages/@aws-cdk/aws-events/.npmignore index 9a032ae80868c..e8acf10a468a1 100644 --- a/packages/@aws-cdk/aws-events/.npmignore +++ b/packages/@aws-cdk/aws-events/.npmignore @@ -24,4 +24,5 @@ tsconfig.json **/cdk.out junit.xml test/ -!*.lit.ts \ No newline at end of file +!*.lit.ts +jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events/jest.config.js b/packages/@aws-cdk/aws-events/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-events/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-events/package.json b/packages/@aws-cdk/aws-events/package.json index 2d962235e0227..cf480c87c1dfe 100644 --- a/packages/@aws-cdk/aws-events/package.json +++ b/packages/@aws-cdk/aws-events/package.json @@ -55,6 +55,7 @@ }, "cdk-build": { "cloudformation": "AWS::Events", + "jest": true, "env": { "AWSLINT_BASE_CONSTRUCT": "true" } @@ -74,10 +75,11 @@ }, "license": "Apache-2.0", "devDependencies": { + "@types/jest": "^26.0.24", "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", - "nodeunit": "^0.11.3", + "jest": "^26.6.3", "pkglint": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-events/test/test.archive.ts b/packages/@aws-cdk/aws-events/test/archive.test.ts similarity index 69% rename from packages/@aws-cdk/aws-events/test/test.archive.ts rename to packages/@aws-cdk/aws-events/test/archive.test.ts index 84c9d12222e42..0c37a0ec4ae39 100644 --- a/packages/@aws-cdk/aws-events/test/test.archive.ts +++ b/packages/@aws-cdk/aws-events/test/archive.test.ts @@ -1,11 +1,10 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import { Duration, Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import { EventBus } from '../lib'; import { Archive } from '../lib/archive'; -export = { - 'creates an archive for an EventBus'(test: Test) { +describe('archive', () => { + test('creates an archive for an EventBus', () => { // GIVEN const stack = new Stack(); @@ -21,11 +20,11 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::Events::EventBus', { + expect(stack).toHaveResource('AWS::Events::EventBus', { Name: 'Bus', - })); + }); - expect(stack).to(haveResource('AWS::Events::Archive', { + expect(stack).toHaveResource('AWS::Events::Archive', { EventPattern: { account: [{ Ref: 'AWS::AccountId', @@ -38,11 +37,11 @@ export = { 'Arn', ], }, - })); + }); + - test.done(); - }, - 'creates an archive for an EventBus with a pattern including a detailType property'(test: Test) { + }); + test('creates an archive for an EventBus with a pattern including a detailType property', () => { // GIVEN const stack = new Stack(); @@ -59,11 +58,11 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::Events::EventBus', { + expect(stack).toHaveResource('AWS::Events::EventBus', { Name: 'Bus', - })); + }); - expect(stack).to(haveResource('AWS::Events::Archive', { + expect(stack).toHaveResource('AWS::Events::Archive', { EventPattern: { 'account': [{ Ref: 'AWS::AccountId', @@ -77,8 +76,8 @@ export = { 'Arn', ], }, - })); + }); + - test.done(); - }, -} + }); +}); diff --git a/packages/@aws-cdk/aws-events/test/test.event-bus.ts b/packages/@aws-cdk/aws-events/test/event-bus.test.ts similarity index 71% rename from packages/@aws-cdk/aws-events/test/test.event-bus.ts rename to packages/@aws-cdk/aws-events/test/event-bus.test.ts index 9dea3faab34f6..b4384255ea7b4 100644 --- a/packages/@aws-cdk/aws-events/test/test.event-bus.ts +++ b/packages/@aws-cdk/aws-events/test/event-bus.test.ts @@ -1,11 +1,10 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as iam from '@aws-cdk/aws-iam'; import { Aws, CfnResource, Stack, Arn } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import { EventBus } from '../lib'; -export = { - 'default event bus'(test: Test) { +describe('event bus', () => { + test('default event bus', () => { // GIVEN const stack = new Stack(); @@ -13,14 +12,14 @@ export = { new EventBus(stack, 'Bus'); // THEN - expect(stack).to(haveResource('AWS::Events::EventBus', { + expect(stack).toHaveResource('AWS::Events::EventBus', { Name: 'Bus', - })); + }); + - test.done(); - }, + }); - 'named event bus'(test: Test) { + test('named event bus', () => { // GIVEN const stack = new Stack(); @@ -30,14 +29,14 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::Events::EventBus', { + expect(stack).toHaveResource('AWS::Events::EventBus', { Name: 'myEventBus', - })); + }); - test.done(); - }, - 'partner event bus'(test: Test) { + }); + + test('partner event bus', () => { // GIVEN const stack = new Stack(); @@ -47,15 +46,15 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::Events::EventBus', { + expect(stack).toHaveResource('AWS::Events::EventBus', { Name: 'aws.partner/PartnerName/acct1/repo1', EventSourceName: 'aws.partner/PartnerName/acct1/repo1', - })); + }); - test.done(); - }, - 'imported event bus'(test: Test) { + }); + + test('imported event bus', () => { const stack = new Stack(); const eventBus = new EventBus(stack, 'Bus'); @@ -71,15 +70,15 @@ export = { }, }); - expect(stack).to(haveResource('Test::Resource', { + expect(stack).toHaveResource('Test::Resource', { EventBusArn1: { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }, EventBusArn2: { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }, - })); + }); - test.done(); - }, - 'imported event bus from name'(test: Test) { + }); + + test('imported event bus from name', () => { const stack = new Stack(); const eventBus = new EventBus(stack, 'Bus', { eventBusName: 'test-bus-to-import-by-name' }); @@ -87,12 +86,12 @@ export = { const importEB = EventBus.fromEventBusName(stack, 'ImportBus', eventBus.eventBusName); // WHEN - test.deepEqual(stack.resolve(eventBus.eventBusName), stack.resolve(importEB.eventBusName)); + expect(stack.resolve(eventBus.eventBusName)).toEqual(stack.resolve(importEB.eventBusName)); + - test.done(); - }, + }); - 'same account imported event bus has right resource env'(test: Test) { + test('same account imported event bus has right resource env', () => { const stack = new Stack(); const eventBus = new EventBus(stack, 'Bus'); @@ -100,13 +99,13 @@ export = { 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'] }] }] }); + expect(stack.resolve(importEB.env.account)).toEqual({ 'Fn::Select': [4, { 'Fn::Split': [':', { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }] }] }); + expect(stack.resolve(importEB.env.region)).toEqual({ 'Fn::Select': [3, { 'Fn::Split': [':', { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }] }] }); - test.done(); - }, - 'cross account imported event bus has right resource env'(test: Test) { + }); + + test('cross account imported event bus has right resource env', () => { const stack = new Stack(); const arnParts = { @@ -121,13 +120,13 @@ export = { const importEB = EventBus.fromEventBusArn(stack, 'ImportBus', arn); // WHEN - test.deepEqual(importEB.env.account, arnParts.account); - test.deepEqual(importEB.env.region, arnParts.region); + expect(importEB.env.account).toEqual(arnParts.account); + expect(importEB.env.region).toEqual(arnParts.region); + - test.done(); - }, + }); - 'can get bus name'(test: Test) { + test('can get bus name', () => { // GIVEN const stack = new Stack(); const bus = new EventBus(stack, 'Bus', { @@ -143,14 +142,14 @@ export = { }); // THEN - expect(stack).to(haveResource('Test::Resource', { + expect(stack).toHaveResource('Test::Resource', { EventBusName: { Ref: 'BusEA82B648' }, - })); + }); + - test.done(); - }, + }); - 'can get bus arn'(test: Test) { + test('can get bus arn', () => { // GIVEN const stack = new Stack(); const bus = new EventBus(stack, 'Bus', { @@ -166,14 +165,14 @@ export = { }); // THEN - expect(stack).to(haveResource('Test::Resource', { + expect(stack).toHaveResource('Test::Resource', { EventBusArn: { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }, - })); + }); + - test.done(); - }, + }); - 'event bus name cannot be default'(test: Test) { + test('event bus name cannot be default', () => { // GIVEN const stack = new Stack(); @@ -183,14 +182,14 @@ export = { }); // THEN - test.throws(() => { + expect(() => { createInvalidBus(); - }, /'eventBusName' must not be 'default'/); + }).toThrow(/'eventBusName' must not be 'default'/); - test.done(); - }, - 'event bus name cannot contain slash'(test: Test) { + }); + + test('event bus name cannot contain slash', () => { // GIVEN const stack = new Stack(); @@ -200,14 +199,14 @@ export = { }); // THEN - test.throws(() => { + expect(() => { createInvalidBus(); - }, /'eventBusName' must not contain '\/'/); + }).toThrow(/'eventBusName' must not contain '\/'/); + - test.done(); - }, + }); - 'event bus cannot have name and source name'(test: Test) { + test('event bus cannot have name and source name', () => { // GIVEN const stack = new Stack(); @@ -218,14 +217,14 @@ export = { }); // THEN - test.throws(() => { + expect(() => { createInvalidBus(); - }, /'eventBusName' and 'eventSourceName' cannot both be provided/); + }).toThrow(/'eventBusName' and 'eventSourceName' cannot both be provided/); - test.done(); - }, - 'event bus name cannot be empty string'(test: Test) { + }); + + test('event bus name cannot be empty string', () => { // GIVEN const stack = new Stack(); @@ -235,26 +234,26 @@ export = { }); // THEN - test.throws(() => { + expect(() => { createInvalidBus(); - }, /'eventBusName' must satisfy: /); + }).toThrow(/'eventBusName' must satisfy: /); + - test.done(); - }, + }); - 'does not throw if eventBusName is a token'(test: Test) { + test('does not throw if eventBusName is a token', () => { // GIVEN const stack = new Stack(); // WHEN / THEN - test.doesNotThrow(() => new EventBus(stack, 'EventBus', { + expect(() => new EventBus(stack, 'EventBus', { eventBusName: Aws.STACK_NAME, - })); + })).not.toThrow(); + - test.done(); - }, + }); - 'event bus source name must follow pattern'(test: Test) { + test('event bus source name must follow pattern', () => { // GIVEN const stack = new Stack(); @@ -264,14 +263,14 @@ export = { }); // THEN - test.throws(() => { + expect(() => { createInvalidBus(); - }, /'eventSourceName' must satisfy: \/\^aws/); + }).toThrow(/'eventSourceName' must satisfy: \/\^aws/); - test.done(); - }, - 'event bus source name cannot be empty string'(test: Test) { + }); + + test('event bus source name cannot be empty string', () => { // GIVEN const stack = new Stack(); @@ -281,14 +280,14 @@ export = { }); // THEN - test.throws(() => { + expect(() => { createInvalidBus(); - }, /'eventSourceName' must satisfy: /); + }).toThrow(/'eventSourceName' must satisfy: /); + - test.done(); - }, + }); - 'can grant PutEvents'(test: Test) { + test('can grant PutEvents', () => { // GIVEN const stack = new Stack(); const role = new iam.Role(stack, 'Role', { @@ -299,7 +298,7 @@ export = { EventBus.grantPutEvents(role); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -315,12 +314,12 @@ export = { Ref: 'Role1ABCC5F0', }, ], - })); + }); + - test.done(); - }, + }); - 'can grant PutEvents using grantAllPutEvents'(test: Test) { + test('can grant PutEvents using grantAllPutEvents', () => { // GIVEN const stack = new Stack(); const role = new iam.Role(stack, 'Role', { @@ -331,7 +330,7 @@ export = { EventBus.grantAllPutEvents(role); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -347,11 +346,11 @@ export = { Ref: 'Role1ABCC5F0', }, ], - })); + }); + - test.done(); - }, - 'can grant PutEvents to a specific event bus'(test: Test) { + }); + test('can grant PutEvents to a specific event bus', () => { // GIVEN const stack = new Stack(); const role = new iam.Role(stack, 'Role', { @@ -364,7 +363,7 @@ export = { eventBus.grantPutEventsTo(role); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -385,11 +384,11 @@ export = { Ref: 'Role1ABCC5F0', }, ], - })); + }); + - test.done(); - }, - 'can archive events'(test: Test) { + }); + test('can archive events', () => { // GIVEN const stack = new Stack(); @@ -404,11 +403,11 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::Events::EventBus', { + expect(stack).toHaveResource('AWS::Events::EventBus', { Name: 'Bus', - })); + }); - expect(stack).to(haveResource('AWS::Events::Archive', { + expect(stack).toHaveResource('AWS::Events::Archive', { SourceArn: { 'Fn::GetAtt': [ 'BusEA82B648', @@ -436,11 +435,11 @@ export = { }, RetentionDays: 0, ArchiveName: 'MyArchive', - })); + }); - test.done(); - }, - 'can archive events from an imported EventBus'(test: Test) { + + }); + test('can archive events from an imported EventBus', () => { // GIVEN const stack = new Stack(); @@ -457,11 +456,11 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::Events::EventBus', { + expect(stack).toHaveResource('AWS::Events::EventBus', { Name: 'Bus', - })); + }); - expect(stack).to(haveResource('AWS::Events::Archive', { + expect(stack).toHaveResource('AWS::Events::Archive', { SourceArn: { 'Fn::GetAtt': [ 'BusEA82B648', @@ -512,8 +511,8 @@ export = { }, RetentionDays: 0, ArchiveName: 'MyArchive', - })); + }); + - test.done(); - }, -}; + }); +}); diff --git a/packages/@aws-cdk/aws-events/test/test.input.ts b/packages/@aws-cdk/aws-events/test/input.test.ts similarity index 78% rename from packages/@aws-cdk/aws-events/test/test.input.ts rename to packages/@aws-cdk/aws-events/test/input.test.ts index 43c763ef35f74..76ec2cd2d2bd9 100644 --- a/packages/@aws-cdk/aws-events/test/test.input.ts +++ b/packages/@aws-cdk/aws-events/test/input.test.ts @@ -1,13 +1,12 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import { User } from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import { EventField, IRuleTarget, RuleTargetInput, Schedule } from '../lib'; import { Rule } from '../lib/rule'; -export = { - 'json template': { - 'can just be a JSON object'(test: Test) { +describe('input', () => { + describe('json template', () => { + test('can just be a JSON object', () => { // GIVEN const stack = new cdk.Stack(); const rule = new Rule(stack, 'Rule', { @@ -18,17 +17,17 @@ export = { rule.addTarget(new SomeTarget(RuleTargetInput.fromObject({ SomeObject: 'withAValue' }))); // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { Input: '{"SomeObject":"withAValue"}', }, ], - })); - test.done(); - }, + }); + + }); - 'can use joined JSON containing refs in JSON object'(test: Test) { + test('can use joined JSON containing refs in JSON object', () => { // GIVEN const stack = new cdk.Stack(); const rule = new Rule(stack, 'Rule', { @@ -42,7 +41,7 @@ export = { }))); // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { InputTransformer: { @@ -62,12 +61,12 @@ export = { }, }, ], - })); + }); - test.done(); - }, - 'can use joined JSON containing refs in JSON object with tricky inputs'(test: Test) { + }); + + test('can use joined JSON containing refs in JSON object with tricky inputs', () => { // GIVEN const stack = new cdk.Stack(); const rule = new Rule(stack, 'Rule', { @@ -81,7 +80,7 @@ export = { }))); // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { InputTransformer: { @@ -101,12 +100,12 @@ export = { }, }, ], - })); + }); - test.done(); - }, - 'can use joined JSON containing refs in JSON object and concat'(test: Test) { + }); + + test('can use joined JSON containing refs in JSON object and concat', () => { // GIVEN const stack = new cdk.Stack(); const rule = new Rule(stack, 'Rule', { @@ -120,7 +119,7 @@ export = { }))); // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { InputTransformer: { @@ -140,12 +139,12 @@ export = { }, }, ], - })); + }); + - test.done(); - }, + }); - 'can use joined JSON containing refs in JSON object and quotes'(test: Test) { + test('can use joined JSON containing refs in JSON object and quotes', () => { // GIVEN const stack = new cdk.Stack(); const rule = new Rule(stack, 'Rule', { @@ -159,7 +158,7 @@ export = { }))); // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { InputTransformer: { @@ -179,12 +178,12 @@ export = { }, }, ], - })); + }); + - test.done(); - }, + }); - 'can use joined JSON containing refs in JSON object and multiple keys'(test: Test) { + test('can use joined JSON containing refs in JSON object and multiple keys', () => { // GIVEN const stack = new cdk.Stack(); const rule = new Rule(stack, 'Rule', { @@ -198,7 +197,7 @@ export = { }))); // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { InputTransformer: { @@ -218,12 +217,12 @@ export = { }, }, ], - })); + }); + - test.done(); - }, + }); - 'can use token'(test: Test) { + test('can use token', () => { // GIVEN const stack = new cdk.Stack(); const rule = new Rule(stack, 'Rule', { @@ -235,7 +234,7 @@ export = { rule.addTarget(new SomeTarget(RuleTargetInput.fromObject({ userArn: user.userArn }))); // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { Input: { @@ -255,13 +254,13 @@ export = { }, }, ], - })); - test.done(); - }, - }, + }); - 'text templates': { - 'strings with newlines are serialized to a newline-delimited list of JSON strings'(test: Test) { + }); + }); + + describe('text templates', () => { + test('strings with newlines are serialized to a newline-delimited list of JSON strings', () => { // GIVEN const stack = new cdk.Stack(); const rule = new Rule(stack, 'Rule', { @@ -272,18 +271,18 @@ export = { rule.addTarget(new SomeTarget(RuleTargetInput.fromMultilineText('I have\nmultiple lines'))); // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { Input: '"I have"\n"multiple lines"', }, ], - })); + }); - test.done(); - }, - 'escaped newlines are not interpreted as newlines'(test: Test) { + }); + + test('escaped newlines are not interpreted as newlines', () => { // GIVEN const stack = new cdk.Stack(); const rule = new Rule(stack, 'Rule', { @@ -294,18 +293,18 @@ export = { rule.addTarget(new SomeTarget(RuleTargetInput.fromMultilineText('this is not\\na real newline'))), // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { Input: '"this is not\\\\na real newline"', }, ], - })); + }); + - test.done(); - }, + }); - 'can use Tokens in text templates'(test: Test) { + test('can use Tokens in text templates', () => { // GIVEN const stack = new cdk.Stack(); const rule = new Rule(stack, 'Rule', { @@ -318,18 +317,18 @@ export = { rule.addTarget(new SomeTarget(RuleTargetInput.fromText(`hello ${world}`))); // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { Input: '"hello world"', }, ], - })); + }); + - test.done(); - }, - }, -}; + }); + }); +}); class SomeTarget implements IRuleTarget { public constructor(private readonly input: RuleTargetInput) { diff --git a/packages/@aws-cdk/aws-events/test/test.rule.ts b/packages/@aws-cdk/aws-events/test/rule.test.ts similarity index 79% rename from packages/@aws-cdk/aws-events/test/test.rule.ts rename to packages/@aws-cdk/aws-events/test/rule.test.ts index b6ff7b62d86d4..5cc3b6c5f03e1 100644 --- a/packages/@aws-cdk/aws-events/test/test.rule.ts +++ b/packages/@aws-cdk/aws-events/test/rule.test.ts @@ -1,22 +1,21 @@ /* eslint-disable object-curly-newline */ -import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import { EventBus, EventField, IRule, IRuleTarget, RuleTargetConfig, RuleTargetInput, Schedule } from '../lib'; import { Rule } from '../lib/rule'; /* eslint-disable quote-props */ -export = { - 'default rule'(test: Test) { +describe('rule', () => { + test('default rule', () => { const stack = new cdk.Stack(); new Rule(stack, 'MyRule', { schedule: Schedule.rate(cdk.Duration.minutes(10)), }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyRuleA44AB831': { 'Type': 'AWS::Events::Rule', @@ -27,10 +26,10 @@ export = { }, }, }); - test.done(); - }, - 'can get rule name'(test: Test) { + }); + + test('can get rule name', () => { const stack = new cdk.Stack(); const rule = new Rule(stack, 'MyRule', { schedule: Schedule.rate(cdk.Duration.minutes(10)), @@ -43,14 +42,14 @@ export = { }, }); - expect(stack).to(haveResource('Test::Resource', { + expect(stack).toHaveResource('Test::Resource', { RuleName: { Ref: 'MyRuleA44AB831' }, - })); + }); - test.done(); - }, - 'get rate as token'(test: Test) { + }); + + test('get rate as token', () => { const app = new cdk.App(); const stack = new cdk.Stack(app, 'MyScheduledStack'); const lazyDuration = cdk.Duration.minutes(cdk.Lazy.number({ produce: () => 5 })); @@ -61,29 +60,29 @@ export = { }); // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { 'Name': 'rateInMinutes', 'ScheduleExpression': 'rate(5 minutes)', - })); + }); - test.done(); - }, - 'Seconds is not an allowed value for Schedule rate'(test: Test) { + }); + + test('Seconds is not an allowed value for Schedule rate', () => { const lazyDuration = cdk.Duration.seconds(cdk.Lazy.number({ produce: () => 5 })); - test.throws(() => Schedule.rate(lazyDuration), /Allowed units for scheduling/i); - test.done(); - }, + expect(() => Schedule.rate(lazyDuration)).toThrow(/Allowed units for scheduling/i); + + }); - 'Millis is not an allowed value for Schedule rate'(test: Test) { + test('Millis is not an allowed value for Schedule rate', () => { const lazyDuration = cdk.Duration.millis(cdk.Lazy.number({ produce: () => 5 })); // THEN - test.throws(() => Schedule.rate(lazyDuration), /Allowed units for scheduling/i); - test.done(); - }, + expect(() => Schedule.rate(lazyDuration)).toThrow(/Allowed units for scheduling/i); - 'rule with physical name'(test: Test) { + }); + + test('rule with physical name', () => { // GIVEN const stack = new cdk.Stack(); @@ -94,14 +93,14 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::Events::Rule', { + expect(stack).toHaveResource('AWS::Events::Rule', { Name: 'PhysicalName', - })); + }); - test.done(); - }, - 'eventPattern is rendered properly'(test: Test) { + }); + + test('eventPattern is rendered properly', () => { const stack = new cdk.Stack(); new Rule(stack, 'MyRule', { @@ -120,7 +119,7 @@ export = { }, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyRuleA44AB831': { 'Type': 'AWS::Events::Rule', @@ -142,18 +141,18 @@ export = { }, }); - test.done(); - }, - 'fails synthesis if neither eventPattern nor scheudleExpression are specified'(test: Test) { + }); + + test('fails synthesis if neither eventPattern nor scheudleExpression are specified', () => { const app = new cdk.App(); const stack = new cdk.Stack(app, 'MyStack'); new Rule(stack, 'Rule'); - test.throws(() => app.synth(), /Either 'eventPattern' or 'schedule' must be defined/); - test.done(); - }, + expect(() => app.synth()).toThrow(/Either 'eventPattern' or 'schedule' must be defined/); - 'addEventPattern can be used to add filters'(test: Test) { + }); + + test('addEventPattern can be used to add filters', () => { const stack = new cdk.Stack(); const rule = new Rule(stack, 'MyRule'); @@ -174,7 +173,7 @@ export = { }, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyRuleA44AB831': { 'Type': 'AWS::Events::Rule', @@ -203,10 +202,10 @@ export = { }, }, }); - test.done(); - }, - 'addEventPattern can de-duplicate filters and keep the order'(test: Test) { + }); + + test('addEventPattern can de-duplicate filters and keep the order', () => { const stack = new cdk.Stack(); const rule = new Rule(stack, 'MyRule'); @@ -218,7 +217,7 @@ export = { detailType: ['EC2 Instance State-change Notification', 'AWS API Call via CloudTrail'], }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyRuleA44AB831': { 'Type': 'AWS::Events::Rule', @@ -234,10 +233,10 @@ export = { }, }, }); - test.done(); - }, - 'targets can be added via props or addTarget with input transformer'(test: Test) { + }); + + test('targets can be added via props or addTarget with input transformer', () => { const stack = new cdk.Stack(); const t1: IRuleTarget = { bind: () => ({ @@ -262,7 +261,7 @@ export = { rule.addTarget(t2); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'EventRule5A491D2C': { 'Type': 'AWS::Events::Rule', @@ -292,10 +291,10 @@ export = { }, }, }); - test.done(); - }, - 'input template can contain tokens'(test: Test) { + }); + + test('input template can contain tokens', () => { const stack = new cdk.Stack(); const rule = new Rule(stack, 'EventRule', { @@ -338,7 +337,7 @@ export = { }), }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'EventRule5A491D2C': { 'Type': 'AWS::Events::Rule', @@ -380,10 +379,10 @@ export = { }, }); - test.done(); - }, - 'target can declare role which will be used'(test: Test) { + }); + + test('target can declare role which will be used', () => { // GIVEN const stack = new cdk.Stack(); @@ -405,7 +404,7 @@ export = { }); // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { 'Targets': [ { 'Arn': 'ARN2', @@ -413,12 +412,12 @@ export = { 'RoleArn': { 'Fn::GetAtt': ['SomeRole6DDC54DD', 'Arn'] }, }, ], - })); + }); - test.done(); - }, - 'in cross-account scenario, target role is only used in target account'(test: Test) { + }); + + test('in cross-account scenario, target role is only used in target account', () => { // GIVEN const app = new cdk.App(); const ruleStack = new cdk.Stack(app, 'RuleStack', { env: { account: '1234', region: 'us-east-1' } }); @@ -443,7 +442,7 @@ export = { }); // THEN - expect(ruleStack).to(haveResourceLike('AWS::Events::Rule', { + expect(ruleStack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { Arn: { 'Fn::Join': ['', [ @@ -453,8 +452,8 @@ export = { ]] }, }, ], - })); - expect(targetStack).to(haveResourceLike('AWS::Events::Rule', { + }); + expect(targetStack).toHaveResourceLike('AWS::Events::Rule', { 'Targets': [ { 'Arn': 'ARN2', @@ -462,12 +461,12 @@ export = { 'RoleArn': { 'Fn::GetAtt': ['SomeRole6DDC54DD', 'Arn'] }, }, ], - })); + }); + - test.done(); - }, + }); - 'asEventRuleTarget can use the ruleArn and a uniqueId of the rule'(test: Test) { + test('asEventRuleTarget can use the ruleArn and a uniqueId of the rule', () => { const stack = new cdk.Stack(); let receivedRuleArn = 'FAIL'; @@ -489,12 +488,12 @@ export = { const rule = new Rule(stack, 'EventRule'); rule.addTarget(t1); - test.deepEqual(stack.resolve(receivedRuleArn), stack.resolve(rule.ruleArn)); - test.deepEqual(receivedRuleId, cdk.Names.uniqueId(rule)); - test.done(); - }, + expect(stack.resolve(receivedRuleArn)).toEqual(stack.resolve(rule.ruleArn)); + expect(receivedRuleId).toEqual(cdk.Names.uniqueId(rule)); - 'fromEventRuleArn'(test: Test) { + }); + + test('fromEventRuleArn', () => { // GIVEN const stack = new cdk.Stack(); @@ -502,12 +501,12 @@ export = { const importedRule = Rule.fromEventRuleArn(stack, 'ImportedRule', 'arn:aws:events:us-east-2:123456789012:rule/example'); // THEN - test.deepEqual(importedRule.ruleArn, 'arn:aws:events:us-east-2:123456789012:rule/example'); - test.deepEqual(importedRule.ruleName, 'example'); - test.done(); - }, + expect(importedRule.ruleArn).toEqual('arn:aws:events:us-east-2:123456789012:rule/example'); + expect(importedRule.ruleName).toEqual('example'); + + }); - 'rule can be disabled'(test: Test) { + test('rule can be disabled', () => { // GIVEN const stack = new cdk.Stack(); @@ -518,14 +517,14 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::Events::Rule', { + expect(stack).toHaveResource('AWS::Events::Rule', { 'State': 'DISABLED', - })); + }); + - test.done(); - }, + }); - 'can add multiple targets with the same id'(test: Test) { + test('can add multiple targets with the same id', () => { // GIVEN const stack = new cdk.Stack(); const rule = new Rule(stack, 'Rule', { @@ -536,7 +535,7 @@ export = { rule.addTarget(new SomeTarget()); // THEN - expect(stack).to(haveResource('AWS::Events::Rule', { + expect(stack).toHaveResource('AWS::Events::Rule', { Targets: [ { 'Arn': 'ARN1', @@ -553,12 +552,12 @@ export = { }, }, ], - })); + }); + - test.done(); - }, + }); - 'sqsParameters are generated when they are specified in target props'(test: Test) { + test('sqsParameters are generated when they are specified in target props', () => { const stack = new cdk.Stack(); const t1: IRuleTarget = { bind: () => ({ @@ -573,7 +572,7 @@ export = { targets: [t1], }); - expect(stack).to(haveResource('AWS::Events::Rule', { + expect(stack).toHaveResource('AWS::Events::Rule', { Targets: [ { 'Arn': 'ARN1', @@ -583,11 +582,11 @@ export = { }, }, ], - })); - test.done(); - }, + }); + + }); - 'associate rule with event bus'(test: Test) { + test('associate rule with event bus', () => { // GIVEN const stack = new cdk.Stack(); const eventBus = new EventBus(stack, 'EventBus'); @@ -601,30 +600,30 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::Events::Rule', { + expect(stack).toHaveResource('AWS::Events::Rule', { EventBusName: { Ref: 'EventBus7B8748AA', }, - })); + }); + - test.done(); - }, + }); - 'throws with eventBus and schedule'(test: Test) { + test('throws with eventBus and schedule', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'MyStack'); const eventBus = new EventBus(stack, 'EventBus'); // THEN - test.throws(() => new Rule(stack, 'MyRule', { + expect(() => new Rule(stack, 'MyRule', { schedule: Schedule.rate(cdk.Duration.minutes(10)), eventBus, - }), /Cannot associate rule with 'eventBus' when using 'schedule'/); - test.done(); - }, + })).toThrow(/Cannot associate rule with 'eventBus' when using 'schedule'/); - 'allow an imported target if is in the same account and region'(test: Test) { + }); + + test('allow an imported target if is in the same account and region', () => { const app = new cdk.App(); const sourceAccount = '123456789012'; @@ -640,7 +639,7 @@ export = { rule.addTarget(new SomeTarget('T', resource)); - expect(sourceStack).to(haveResource('AWS::Events::Rule', { + expect(sourceStack).toHaveResource('AWS::Events::Rule', { Targets: [ { 'Arn': 'ARN1', @@ -650,13 +649,13 @@ export = { }, }, ], - })); + }); - test.done(); - }, - 'for cross-account and/or cross-region targets': { - 'requires that the source stack specify a concrete account'(test: Test) { + }); + + describe('for cross-account and/or cross-region targets', () => { + test('requires that the source stack specify a concrete account', () => { const app = new cdk.App(); const sourceStack = new cdk.Stack(app, 'SourceStack'); @@ -666,14 +665,14 @@ export = { const targetStack = new cdk.Stack(app, 'TargetStack', { env: { account: targetAccount } }); const resource = new cdk.Construct(targetStack, 'Resource'); - test.throws(() => { + expect(() => { rule.addTarget(new SomeTarget('T', resource)); - }, /You need to provide a concrete region/); + }).toThrow(/You need to provide a concrete region/); + - test.done(); - }, + }); - 'requires that the target stack specify a concrete account'(test: Test) { + test('requires that the target stack specify a concrete account', () => { const app = new cdk.App(); const sourceAccount = '123456789012'; @@ -683,14 +682,14 @@ export = { const targetStack = new cdk.Stack(app, 'TargetStack'); const resource = new cdk.Construct(targetStack, 'Resource'); - test.throws(() => { + expect(() => { rule.addTarget(new SomeTarget('T', resource)); - }, /You need to provide a concrete account for the target stack when using cross-account or cross-region events/); + }).toThrow(/You need to provide a concrete account for the target stack when using cross-account or cross-region events/); + - test.done(); - }, + }); - 'requires that the target stack specify a concrete region'(test: Test) { + test('requires that the target stack specify a concrete region', () => { const app = new cdk.App(); const sourceAccount = '123456789012'; @@ -701,14 +700,14 @@ export = { const targetStack = new cdk.Stack(app, 'TargetStack', { env: { account: targetAccount } }); const resource = new cdk.Construct(targetStack, 'Resource'); - test.throws(() => { + expect(() => { rule.addTarget(new SomeTarget('T', resource)); - }, /You need to provide a concrete region for the target stack when using cross-account or cross-region events/); + }).toThrow(/You need to provide a concrete region for the target stack when using cross-account or cross-region events/); + - test.done(); - }, + }); - 'creates cross-account targets if in the same region'(test: Test) { + test('creates cross-account targets if in the same region', () => { const app = new cdk.App(); const sourceAccount = '123456789012'; @@ -727,7 +726,7 @@ export = { rule.addTarget(new SomeTarget('T', resource)); - expect(sourceStack).to(haveResourceLike('AWS::Events::Rule', { + expect(sourceStack).toHaveResourceLike('AWS::Events::Rule', { 'State': 'ENABLED', 'Targets': [ { @@ -744,9 +743,9 @@ export = { }, }, ], - })); + }); - expect(targetStack).to(haveResource('AWS::Events::Rule', { + expect(targetStack).toHaveResource('AWS::Events::Rule', { Targets: [ { 'Arn': 'ARN1', @@ -756,12 +755,12 @@ export = { }, }, ], - })); + }); + - test.done(); - }, + }); - 'creates cross-region targets'(test: Test) { + test('creates cross-region targets', () => { const app = new cdk.App(); const sourceAccount = '123456789012'; @@ -780,7 +779,7 @@ export = { rule.addTarget(new SomeTarget('T', resource)); - expect(sourceStack).to(haveResourceLike('AWS::Events::Rule', { + expect(sourceStack).toHaveResourceLike('AWS::Events::Rule', { 'State': 'ENABLED', 'Targets': [ { @@ -797,9 +796,9 @@ export = { }, }, ], - })); + }); - expect(targetStack).to(haveResource('AWS::Events::Rule', { + expect(targetStack).toHaveResource('AWS::Events::Rule', { Targets: [ { 'Arn': 'ARN1', @@ -809,12 +808,12 @@ export = { }, }, ], - })); + }); + - test.done(); - }, + }); - 'do not create duplicated targets'(test: Test) { + test('do not create duplicated targets', () => { const app = new cdk.App(); const sourceAccount = '123456789012'; @@ -835,7 +834,7 @@ export = { // same target should be skipped rule.addTarget(new SomeTarget('T1', resource)); - expect(sourceStack).to(haveResourceLike('AWS::Events::Rule', { + expect(sourceStack).toHaveResourceLike('AWS::Events::Rule', { 'State': 'ENABLED', 'Targets': [ { @@ -852,9 +851,9 @@ export = { }, }, ], - })); + }); - expect(sourceStack).notTo(haveResourceLike('AWS::Events::Rule', { + expect(sourceStack).not.toHaveResourceLike('AWS::Events::Rule', { 'State': 'ENABLED', 'Targets': [ { @@ -871,12 +870,12 @@ export = { }, }, ], - })); + }); + - test.done(); - }, + }); - 'requires that the target is not imported'(test: Test) { + test('requires that the target is not imported', () => { const app = new cdk.App(); const sourceAccount = '123456789012'; @@ -891,14 +890,14 @@ export = { const targetAccount = '123456789012'; const targetRegion = 'us-west-1'; const resource = EventBus.fromEventBusArn(sourceStack, 'TargetEventBus', `arn:aws:events:${targetRegion}:${targetAccount}:event-bus/default`); - test.throws(() => { + expect(() => { rule.addTarget(new SomeTarget('T', resource)); - }, /Cannot create a cross-account or cross-region rule for an imported resource/); + }).toThrow(/Cannot create a cross-account or cross-region rule for an imported resource/); + - test.done(); - }, + }); - 'requires that the source and target stacks be part of the same App'(test: Test) { + test('requires that the source and target stacks be part of the same App', () => { const sourceApp = new cdk.App(); const sourceAccount = '123456789012'; const sourceStack = new cdk.Stack(sourceApp, 'SourceStack', { env: { account: sourceAccount, region: 'us-west-2' } }); @@ -909,14 +908,14 @@ export = { const targetStack = new cdk.Stack(targetApp, 'TargetStack', { env: { account: targetAccount, region: 'us-west-2' } }); const resource = new cdk.Construct(targetStack, 'Resource'); - test.throws(() => { + expect(() => { rule.addTarget(new SomeTarget('T', resource)); - }, /Event stack and target stack must belong to the same CDK app/); + }).toThrow(/Event stack and target stack must belong to the same CDK app/); + - test.done(); - }, + }); - 'generates the correct rules in the source and target stacks when eventPattern is passed in the constructor'(test: Test) { + test('generates the correct rules in the source and target stacks when eventPattern is passed in the constructor', () => { const app = new cdk.App(); const sourceAccount = '123456789012'; @@ -945,7 +944,7 @@ export = { rule.addTarget(new SomeTarget('T1', resource1)); rule.addTarget(new SomeTarget('T2', resource2)); - expect(sourceStack).to(haveResourceLike('AWS::Events::Rule', { + expect(sourceStack).toHaveResourceLike('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'some-event', @@ -967,9 +966,9 @@ export = { }, }, ], - })); + }); - expect(targetStack).to(haveResourceLike('AWS::Events::Rule', { + expect(targetStack).toHaveResourceLike('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'some-event', @@ -982,8 +981,8 @@ export = { 'Arn': 'ARN1', }, ], - })); - expect(targetStack).to(haveResourceLike('AWS::Events::Rule', { + }); + expect(targetStack).toHaveResourceLike('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'some-event', @@ -996,19 +995,19 @@ export = { 'Arn': 'ARN1', }, ], - })); + }); const eventBusPolicyStack = app.node.findChild(`EventBusPolicy-${sourceAccount}-us-west-2-${targetAccount}`) as cdk.Stack; - expect(eventBusPolicyStack).to(haveResourceLike('AWS::Events::EventBusPolicy', { + expect(eventBusPolicyStack).toHaveResourceLike('AWS::Events::EventBusPolicy', { 'Action': 'events:PutEvents', 'StatementId': `Allow-account-${sourceAccount}`, 'Principal': sourceAccount, - })); + }); + - test.done(); - }, + }); - 'generates the correct rule in the target stack when addEventPattern in the source rule is used'(test: Test) { + test('generates the correct rule in the target stack when addEventPattern in the source rule is used', () => { const app = new cdk.App(); const sourceAccount = '123456789012'; @@ -1035,7 +1034,7 @@ export = { source: ['some-event'], }); - expect(targetStack).to(haveResourceLike('AWS::Events::Rule', { + expect(targetStack).toHaveResourceLike('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'some-event', @@ -1048,12 +1047,12 @@ export = { 'Arn': 'ARN1', }, ], - })); + }); - test.done(); - }, - }, -}; + + }); + }); +}); class SomeTarget implements IRuleTarget { // eslint-disable-next-line cdk/no-core-construct diff --git a/packages/@aws-cdk/aws-events/test/schedule.test.ts b/packages/@aws-cdk/aws-events/test/schedule.test.ts new file mode 100644 index 0000000000000..d853da9ba6c30 --- /dev/null +++ b/packages/@aws-cdk/aws-events/test/schedule.test.ts @@ -0,0 +1,96 @@ +import { Duration, Stack, Lazy } from '@aws-cdk/core'; +import * as events from '../lib'; + +describe('schedule', () => { + test('cron expressions day and dow are mutex: given weekday', () => { + // Run every 10 minutes Monday through Friday + expect('cron(0/10 * ? * MON-FRI *)').toEqual(events.Schedule.cron({ + minute: '0/10', + weekDay: 'MON-FRI', + }).expressionString); + + }); + + test('cron expressions day and dow are mutex: given month day', () => { + // Run at 8:00 am (UTC) every 1st day of the month + expect('cron(0 8 1 * ? *)').toEqual(events.Schedule.cron({ + minute: '0', + hour: '8', + day: '1', + }).expressionString); + + }); + + test('cron expressions day and dow are mutex: given neither', () => { + // Run at 10:00 am (UTC) every day + expect('cron(0 10 * * ? *)').toEqual(events.Schedule.cron({ + minute: '0', + hour: '10', + }).expressionString); + + }); + + test('rate must be whole number of minutes', () => { + expect(() => { + events.Schedule.rate(Duration.minutes(0.13456)); + }).toThrow(/'0.13456 minutes' cannot be converted into a whole number of seconds/); + + }); + + test('rate must be whole number', () => { + expect(() => { + events.Schedule.rate(Duration.minutes(1/8)); + }).toThrow(/'0.125 minutes' cannot be converted into a whole number of seconds/); + + }); + + test('rate cannot be 0', () => { + expect(() => { + events.Schedule.rate(Duration.days(0)); + }).toThrow(/Duration cannot be 0/); + + }); + + test('rate can be from a token', () => { + const stack = new Stack(); + const lazyDuration = Duration.minutes(Lazy.number({ produce: () => 5 })); + const rate = events.Schedule.rate(lazyDuration); + expect('rate(5 minutes)').toEqual(stack.resolve(rate).expressionString); + + }); + + test('rate can be in minutes', () => { + expect('rate(10 minutes)').toEqual( + events.Schedule.rate(Duration.minutes(10)) + .expressionString); + + }); + + test('rate can be in days', () => { + expect('rate(10 days)').toEqual( + events.Schedule.rate(Duration.days(10)) + .expressionString); + + }); + + test('rate can be in hours', () => { + expect('rate(10 hours)').toEqual( + events.Schedule.rate(Duration.hours(10)) + .expressionString); + + }); + + test('rate can be in seconds', () => { + expect('rate(2 minutes)').toEqual( + events.Schedule.rate(Duration.seconds(120)) + .expressionString); + + }); + + test('rate must not be in seconds when specified as a token', () => { + expect(() => { + events.Schedule.rate(Duration.seconds(Lazy.number({ produce: () => 5 }))); + }).toThrow(/Allowed units for scheduling/); + + }); +}); diff --git a/packages/@aws-cdk/aws-events/test/test.schedule.ts b/packages/@aws-cdk/aws-events/test/test.schedule.ts deleted file mode 100644 index 2d9728e743202..0000000000000 --- a/packages/@aws-cdk/aws-events/test/test.schedule.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Duration, Stack, Lazy } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import * as events from '../lib'; - -export = { - 'cron expressions day and dow are mutex: given weekday'(test: Test) { - // Run every 10 minutes Monday through Friday - test.equal('cron(0/10 * ? * MON-FRI *)', events.Schedule.cron({ - minute: '0/10', - weekDay: 'MON-FRI', - }).expressionString); - test.done(); - }, - - 'cron expressions day and dow are mutex: given month day'(test: Test) { - // Run at 8:00 am (UTC) every 1st day of the month - test.equal('cron(0 8 1 * ? *)', events.Schedule.cron({ - minute: '0', - hour: '8', - day: '1', - }).expressionString); - test.done(); - }, - - 'cron expressions day and dow are mutex: given neither'(test: Test) { - // Run at 10:00 am (UTC) every day - test.equal('cron(0 10 * * ? *)', events.Schedule.cron({ - minute: '0', - hour: '10', - }).expressionString); - test.done(); - }, - - 'rate must be whole number of minutes'(test: Test) { - test.throws(() => { - 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(); - }, - - 'rate cannot be 0'(test: Test) { - test.throws(() => { - events.Schedule.rate(Duration.days(0)); - }, /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(); - }, - - 'rate can be in seconds'(test: Test) { - test.equal('rate(2 minutes)', - events.Schedule.rate(Duration.seconds(120)) - .expressionString); - test.done(); - }, - - 'rate must not be in seconds when specified as a token'(test: Test) { - test.throws(() => { - events.Schedule.rate(Duration.seconds(Lazy.number({ produce: () => 5 }))); - }, /Allowed units for scheduling/); - test.done(); - }, -}; diff --git a/packages/@aws-cdk/aws-events/test/test.util.ts b/packages/@aws-cdk/aws-events/test/util.test.ts similarity index 53% rename from packages/@aws-cdk/aws-events/test/test.util.ts rename to packages/@aws-cdk/aws-events/test/util.test.ts index 80cbe36ddbba4..eb354ca9c48a0 100644 --- a/packages/@aws-cdk/aws-events/test/test.util.ts +++ b/packages/@aws-cdk/aws-events/test/util.test.ts @@ -1,10 +1,9 @@ -import { Test } from 'nodeunit'; import { mergeEventPattern } from '../lib/util'; -export = { - mergeEventPattern: { - 'happy case'(test: Test) { - test.deepEqual(mergeEventPattern({ +describe('util', () => { + describe('mergeEventPattern', () => { + test('happy case', () => { + expect(mergeEventPattern({ bar: [1, 2], hey: ['happy'], hello: { @@ -16,7 +15,7 @@ export = { hello: { world: ['you'], }, - }), { + })).toEqual({ bar: [1, 2], hey: ['happy', 'day', 'today'], hello: { @@ -24,25 +23,25 @@ export = { case: [1], }, }); - test.done(); - }, - 'merge into an empty destination'(test: Test) { - test.deepEqual(mergeEventPattern(undefined, { foo: ['123'] }), { foo: [123] }); - test.deepEqual(mergeEventPattern(undefined, { foo: { bar: ['123'] } }), { foo: { bar: [123] } }); - test.deepEqual(mergeEventPattern({ }, { foo: { bar: ['123'] } }), { foo: { bar: [123] } }); - test.done(); - }, + }); - 'fails if a field is not an array'(test: Test) { - test.throws(() => mergeEventPattern(undefined, 123), /Invalid event pattern '123', expecting an object or an array/); - test.throws(() => mergeEventPattern(undefined, 'Hello'), /Invalid event pattern '"Hello"', expecting an object or an array/); - test.throws(() => mergeEventPattern(undefined, { foo: '123' }), /Invalid event pattern field { foo: "123" }. All fields must be arrays/); - test.done(); - }, + test('merge into an empty destination', () => { + expect(mergeEventPattern(undefined, { foo: ['123'] })).toEqual({ foo: ['123'] }); + expect(mergeEventPattern(undefined, { foo: { bar: ['123'] } })).toEqual({ foo: { bar: ['123'] } }); + expect(mergeEventPattern({ }, { foo: { bar: ['123'] } })).toEqual({ foo: { bar: ['123'] } }); - 'fails if mismatch between dest and src'(test: Test) { - test.throws(() => mergeEventPattern({ + }); + + test('fails if a field is not an array', () => { + expect(() => mergeEventPattern(undefined, 123)).toThrow(/Invalid event pattern '123', expecting an object or an array/); + expect(() => mergeEventPattern(undefined, 'Hello')).toThrow(/Invalid event pattern '"Hello"', expecting an object or an array/); + expect(() => mergeEventPattern(undefined, { foo: '123' })).toThrow(/Invalid event pattern field { foo: "123" }. All fields must be arrays/); + + }); + + test('fails if mismatch between dest and src', () => { + expect(() => mergeEventPattern({ obj: { array: [1], }, @@ -52,46 +51,46 @@ export = { value: ['hello'], }, }, - }), /Invalid event pattern field array. Type mismatch between existing pattern \[1\] and added pattern \{"value":\["hello"\]\}/); - test.done(); - }, + })).toThrow(/Invalid event pattern field array. Type mismatch between existing pattern \[1\] and added pattern \{"value":\["hello"\]\}/); - 'deduplicate match values in pattern array'(test: Test) { - test.deepEqual(mergeEventPattern({ + }); + + test('deduplicate match values in pattern array', () => { + expect(mergeEventPattern({ 'detail-type': ['AWS API Call via CloudTrail'], }, { 'detail-type': ['AWS API Call via CloudTrail'], - }), { + })).toEqual({ 'detail-type': ['AWS API Call via CloudTrail'], }); - test.deepEqual(mergeEventPattern({ + expect(mergeEventPattern({ time: [{ prefix: '2017-10-02' }], }, { time: [{ prefix: '2017-10-02' }, { prefix: '2017-10-03' }], - }), { + })).toEqual({ time: [{ prefix: '2017-10-02' }, { prefix: '2017-10-03' }], }); - test.deepEqual(mergeEventPattern({ + expect(mergeEventPattern({ 'detail-type': ['AWS API Call via CloudTrail'], 'time': [{ prefix: '2017-10-02' }], }, { 'detail-type': ['AWS API Call via CloudTrail'], 'time': [{ prefix: '2017-10-02' }, { prefix: '2017-10-03' }], - }), { + })).toEqual({ 'detail-type': ['AWS API Call via CloudTrail'], 'time': [{ prefix: '2017-10-02' }, { prefix: '2017-10-03' }], }); - test.deepEqual(mergeEventPattern({ + expect(mergeEventPattern({ 'detail-type': ['AWS API Call via CloudTrail', 'AWS API Call via CloudTrail'], 'time': [{ prefix: '2017-10-02' }], }, { 'detail-type': ['AWS API Call via CloudTrail', 'AWS API Call via CloudTrail'], 'time': [{ prefix: '2017-10-02' }, { prefix: '2017-10-03' }, { prefix: '2017-10-02' }], - }), { + })).toEqual({ 'detail-type': ['AWS API Call via CloudTrail'], 'time': [{ prefix: '2017-10-02' }, { prefix: '2017-10-03' }], }); - test.done(); - }, - }, -}; + + }); + }); +}); From b60367526f0b962ed0466237a6e9db2b0931a50f Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Tue, 31 Aug 2021 16:53:33 +0100 Subject: [PATCH 45/91] chore(cdk-release): support separate alpha Changelog (#16182) This is part of the continued work to release the experimental modules as independent alpha modules. This change introduces a new mode for handling changes in the changelog: in this new mode, all changes from alpha modules are still stripped from the main changelog (as they are currently for v2); those alpha module changes are now also written to a dedicated changelog (e.g., `CHANGELOG.v2.alpha.md`). related #15591 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- scripts/bump.js | 12 +- scripts/resolve-version-lib.js | 11 +- scripts/script-tests/resolve-version.test.js | 5 + tools/cdk-release/lib/conventional-commits.ts | 58 +++---- tools/cdk-release/lib/index.ts | 39 +++-- tools/cdk-release/lib/lifecycles/changelog.ts | 81 +++++++--- tools/cdk-release/lib/types.ts | 23 ++- tools/cdk-release/test/changelog.test.ts | 146 ++++++++++++++++-- .../test/conventional-commits.test.ts | 88 +++++++---- 9 files changed, 357 insertions(+), 106 deletions(-) diff --git a/scripts/bump.js b/scripts/bump.js index b7e364bc0ab16..6078e04460a95 100755 --- a/scripts/bump.js +++ b/scripts/bump.js @@ -29,11 +29,6 @@ async function main() { } }; - const majorVersion = semver.major(ver.version); - if (majorVersion > 1) { - opts.stripExperimentalChanges = true; - } - const forTesting = process.env.BUMP_CANDIDATE || false; if (forTesting) { opts.skip.commit = true; @@ -46,6 +41,8 @@ async function main() { console.error(`BUMP_CANDIDATE is set, so bumping version for testing (with the "${opts.prerelease}" prerelease tag)`); } + const majorVersion = semver.major(ver.version); + const useLegacyBump = process.env.LEGACY_BUMP || false; if (useLegacyBump) { console.error("ℹ️ Using the third-party 'standard-version' package to perform the bump"); @@ -67,9 +64,14 @@ async function main() { // this is incredible, but passing this option to standard-version actually makes it crash! // good thing we're getting rid of it... opts.verbose = !!process.env.VERBOSE; + if (majorVersion > 1) { + // NOTE - Once we start publishing alpha modules independently, this needs to be flipped to 'separate' + opts.experimentalChangesTreatment = 'strip'; + } // Rename some options to match cdk-release inputs (replaces bumpFiles, packageFiles, and infile) opts.versionFile = ver.versionFile; opts.changelogFile = ver.changelogFile; + opts.alphaChangelogFile = ver.alphaChangelogFile; console.error("🎉 Calling our 'cdk-release' package to make the bump"); console.error("ℹ️ Set the LEGACY_BUMP env variable to use the old 'standard-version' bump instead"); const cdkRelease = require('cdk-release'); diff --git a/scripts/resolve-version-lib.js b/scripts/resolve-version-lib.js index 21a13c0eb4ab2..ec1f7b1b83822 100755 --- a/scripts/resolve-version-lib.js +++ b/scripts/resolve-version-lib.js @@ -51,21 +51,26 @@ function resolveVersion(rootdir) { } // - // determine changelog file name + // determine changelog file names // const changelogFile = majorVersion === 1 ? 'CHANGELOG.md' : `CHANGELOG.v${majorVersion}.md`; + const alphaChangelogFile = majorVersion === 1 + ? undefined + : `CHANGELOG.v${majorVersion}.alpha.md`; + // // export all of it // return { version: currentVersion, - versionFile: versionFile, - changelogFile: changelogFile, + versionFile, + changelogFile, + alphaChangelogFile, prerelease: releaseType !== 'stable' ? releaseType : undefined, marker: '0.0.0', }; diff --git a/scripts/script-tests/resolve-version.test.js b/scripts/script-tests/resolve-version.test.js index 442e97e45b3cf..693e29234f023 100644 --- a/scripts/script-tests/resolve-version.test.js +++ b/scripts/script-tests/resolve-version.test.js @@ -13,6 +13,7 @@ happy({ }, expected: { changelogFile: 'CHANGELOG.v2.md', + alphaChangelogFile: 'CHANGELOG.v2.alpha.md', marker: '0.0.0', prerelease: undefined, version: '2.1.0', @@ -28,6 +29,7 @@ happy({ }, expected: { changelogFile: 'CHANGELOG.v2.md', + alphaChangelogFile: 'CHANGELOG.v2.alpha.md', marker: '0.0.0', prerelease: 'alpha', version: '2.1.0-alpha.0', @@ -43,6 +45,7 @@ happy({ }, expected: { changelogFile: 'CHANGELOG.v2.md', + alphaChangelogFile: 'CHANGELOG.v2.alpha.md', marker: '0.0.0', prerelease: 'rc', version: '2.1.0-rc.0', @@ -58,6 +61,7 @@ happy({ }, expected: { changelogFile: 'CHANGELOG.md', + alphaChangelogFile: undefined, marker: '0.0.0', prerelease: undefined, version: '1.72.0', @@ -73,6 +77,7 @@ happy({ }, expected: { changelogFile: 'CHANGELOG.v2.md', + alphaChangelogFile: 'CHANGELOG.v2.alpha.md', marker: '0.0.0', prerelease: undefined, version: '2.0.0-rc.0', diff --git a/tools/cdk-release/lib/conventional-commits.ts b/tools/cdk-release/lib/conventional-commits.ts index ed42884bc8b08..56355edc7ac62 100644 --- a/tools/cdk-release/lib/conventional-commits.ts +++ b/tools/cdk-release/lib/conventional-commits.ts @@ -1,8 +1,5 @@ -import * as fs from 'fs-extra'; import { ReleaseOptions } from './types'; // eslint-disable-next-line @typescript-eslint/no-require-imports -const lerna_project = require('@lerna/project'); -// eslint-disable-next-line @typescript-eslint/no-require-imports const conventionalCommitsParser = require('conventional-commits-parser'); // eslint-disable-next-line @typescript-eslint/no-require-imports const gitRawCommits = require('git-raw-commits'); @@ -95,31 +92,42 @@ export async function getConventionalCommitsFromGitHistory(args: ReleaseOptions, } /** - * Filters commits based on the criteria in `args` - * (right now, the only criteria is whether to remove commits that relate to experimental packages). + * Options for filterCommits + */ +export interface FilterCommitsOptions { + /** + * Scopes matching these package names (and variants) will be excluded from the commits returned. + * @default - No packages are excluded. + **/ + excludePackages?: string[]; + + /** + * If provided, scopes matching these package names (and variants) will be the *only commits* considered. + * @default - All packages are included. + **/ + includePackages?: string[]; +} + +/** + * Filters commits based on package scopes and inclusion/exclusion criteria. + * If `opts.includePackages` is provided, commits without scopes will not be included. * - * @param args configuration * @param commits the array of Conventional Commits to filter - * @returns an array of ConventionalCommit objects which is a subset of `commits` - * (possibly exactly equal to `commits`) + * @param opts filtering options; if none are provided, all commits are returned. */ -export function filterCommits(args: ReleaseOptions, commits: ConventionalCommit[]): ConventionalCommit[] { - if (!args.stripExperimentalChanges) { - return commits; - } +export function filterCommits(commits: ConventionalCommit[], opts: FilterCommitsOptions = {}): ConventionalCommit[] { + const excludeScopes = createScopeVariations(opts.excludePackages ?? []); + const includeScopes = createScopeVariations(opts.includePackages ?? []); + + return commits + .filter(commit => includeScopes.length === 0 || (commit.scope && includeScopes.includes(commit.scope))) + .filter(commit => excludeScopes.length === 0 || !commit.scope || !excludeScopes.includes(commit.scope)); +} + +function createScopeVariations(names: string[]) { + const simplifiedNames = names.map(n => n.replace(/^@aws-cdk\//, '')); - // a get a list of packages from our monorepo - const project = new lerna_project.Project(); - const packages = project.getPackagesSync(); - const experimentalPackageNames: string[] = packages - .filter((pkg: any) => { - const pkgJson = fs.readJsonSync(pkg.manifestLocation); - return pkgJson.name.startsWith('@aws-cdk/') - && (pkgJson.maturity === 'experimental' || pkgJson.maturity === 'developer-preview'); - }) - .map((pkg: any) => pkg.name.substr('@aws-cdk/'.length)); - - const experimentalScopes = flatMap(experimentalPackageNames, (pkgName) => [ + return flatMap(simplifiedNames, (pkgName) => [ pkgName, ...(pkgName.startsWith('aws-') ? [ @@ -133,8 +141,6 @@ export function filterCommits(args: ReleaseOptions, commits: ConventionalCommit[ : [] ), ]); - - return commits.filter(commit => !commit.scope || !experimentalScopes.includes(commit.scope)); } function flatMap(xs: T[], fn: (x: T) => U[]): U[] { diff --git a/tools/cdk-release/lib/index.ts b/tools/cdk-release/lib/index.ts index 04159170ffd81..b908e4cccdc7d 100644 --- a/tools/cdk-release/lib/index.ts +++ b/tools/cdk-release/lib/index.ts @@ -1,12 +1,14 @@ -import * as fs from 'fs'; import * as path from 'path'; -import { filterCommits, getConventionalCommitsFromGitHistory } from './conventional-commits'; +import * as fs from 'fs-extra'; +import { getConventionalCommitsFromGitHistory } from './conventional-commits'; import { defaults } from './defaults'; import { bump } from './lifecycles/bump'; -import { changelog } from './lifecycles/changelog'; +import { writeChangelogs } from './lifecycles/changelog'; import { commit } from './lifecycles/commit'; import { debug, debugObject } from './private/print'; -import { ReleaseOptions, Versions } from './types'; +import { PackageInfo, ReleaseOptions, Versions } from './types'; +// eslint-disable-next-line @typescript-eslint/no-require-imports +const lerna_project = require('@lerna/project'); module.exports = async function main(opts: ReleaseOptions): Promise { // handle the default options @@ -19,21 +21,20 @@ module.exports = async function main(opts: ReleaseOptions): Promise { const currentVersion = readVersion(args.versionFile); debugObject(args, 'Current version info', currentVersion); - const commits = await getConventionalCommitsFromGitHistory(args, `v${currentVersion.stableVersion}`); - const filteredCommits = filterCommits(args, commits); - debugObject(args, 'Found and filtered commits', filteredCommits); - const newVersion = await bump(args, currentVersion); debugObject(args, 'New version is', newVersion); + debug(args, 'Reading Git commits'); + const commits = await getConventionalCommitsFromGitHistory(args, `v${currentVersion.stableVersion}`); + debug(args, 'Writing Changelog'); - await changelog(args, currentVersion.stableVersion, newVersion.stableVersion, filteredCommits); + const changelogResults = await writeChangelogs({ ...args, currentVersion, newVersion, commits, packages: getProjectPackageInfos() }); debug(args, 'Committing result'); - await commit(args, newVersion.stableVersion, [args.versionFile, args.changelogFile]); + await commit(args, newVersion.stableVersion, [args.versionFile, ...changelogResults.map(r => r.filePath)]); }; -export function readVersion(versionFile: string): Versions { +function readVersion(versionFile: string): Versions { const versionPath = path.resolve(process.cwd(), versionFile); const contents = JSON.parse(fs.readFileSync(versionPath, { encoding: 'utf-8' })); return { @@ -41,3 +42,19 @@ export function readVersion(versionFile: string): Versions { alphaVersion: contents.alphaVersion, }; } + +function getProjectPackageInfos(): PackageInfo[] { + const packages = lerna_project.Project.getPackagesSync(); + + return packages.map((pkg: any) => { + const maturity = pkg.get('maturity'); + const alpha = pkg.name.startsWith('@aws-cdk/') + && (maturity === 'experimental' || maturity === 'developer-preview'); + + return { + name: pkg.name, + location: pkg.location, + alpha, + }; + }); +} diff --git a/tools/cdk-release/lib/lifecycles/changelog.ts b/tools/cdk-release/lib/lifecycles/changelog.ts index 7e1052068ffd9..3da1b8e10de3a 100644 --- a/tools/cdk-release/lib/lifecycles/changelog.ts +++ b/tools/cdk-release/lib/lifecycles/changelog.ts @@ -1,9 +1,9 @@ import * as stream from 'stream'; import * as fs from 'fs-extra'; -import { ConventionalCommit } from '../conventional-commits'; +import { ConventionalCommit, filterCommits } from '../conventional-commits'; import { writeFile } from '../private/files'; -import { notify, debug, debugObject } from '../private/print'; -import { LifecyclesSkip } from '../types'; +import { notify, debug } from '../private/print'; +import { ExperimentalChangesTreatment, LifecyclesSkip, PackageInfo, Versions } from '../types'; // eslint-disable-next-line @typescript-eslint/no-require-imports const conventionalChangelogPresetLoader = require('conventional-changelog-preset-loader'); // eslint-disable-next-line @typescript-eslint/no-require-imports @@ -11,8 +11,18 @@ const conventionalChangelogWriter = require('conventional-changelog-writer'); const START_OF_LAST_RELEASE_PATTERN = /(^#+ \[?[0-9]+\.[0-9]+\.[0-9]+| { + if (opts.skip?.changelog) { + return []; + } + + const experimentalChangesTreatment = opts.experimentalChangesTreatment ?? ExperimentalChangesTreatment.INCLUDE; + const alphaPackages = opts.packages.filter(p => p.alpha); + const stableCommits = filterCommits(opts.commits, { excludePackages: alphaPackages.map(p => p.name) }); + + switch (experimentalChangesTreatment) { + case ExperimentalChangesTreatment.INCLUDE: + const allContents = await changelog(opts, opts.currentVersion.stableVersion, opts.newVersion.stableVersion, opts.commits); + return [{ filePath: opts.changelogFile, fileContents: allContents }]; + + case ExperimentalChangesTreatment.STRIP: + const strippedContents = await changelog(opts, opts.currentVersion.stableVersion, opts.newVersion.stableVersion, stableCommits); + return [{ filePath: opts.changelogFile, fileContents: strippedContents }]; + + case ExperimentalChangesTreatment.SEPARATE: + if (!opts.currentVersion.alphaVersion || !opts.newVersion.alphaVersion) { + throw new Error('unable to create separate alpha Changelog without alpha package versions'); + } + if (!opts.alphaChangelogFile) { + throw new Error('alphaChangelogFile must be specified if experimentalChangesTreatment is SEPARATE'); + } + + const changelogResults: ChangelogResult[] = []; + const contents = await changelog(opts, opts.currentVersion.stableVersion, opts.newVersion.stableVersion, stableCommits); + changelogResults.push({ filePath: opts.changelogFile, fileContents: contents }); + + const alphaCommits = filterCommits(opts.commits, { includePackages: alphaPackages.map(p => p.name) }); + const alphaContents = await changelog( + { ...opts, changelogFile: opts.alphaChangelogFile }, + opts.currentVersion.alphaVersion, opts.newVersion.alphaVersion, alphaCommits); + changelogResults.push({ filePath: opts.alphaChangelogFile, fileContents: alphaContents }); + + return changelogResults; + + default: + throw new Error(`unsupported experimentalChanges type: ${opts.experimentalChangesTreatment}`); + } } export async function changelog( args: ChangelogOptions, currentVersion: string, newVersion: string, commits: ConventionalCommit[], -): Promise { - if (args.skip?.changelog) { - return { - contents: '', - changedFiles: [], - }; - } +): Promise { + createChangelogIfMissing(args); // find the position of the last release and remove header @@ -50,7 +97,6 @@ export async function changelog( const presetConfig = await conventionalChangelogPresetLoader({ name: 'conventional-changelog-conventionalcommits', }); - debugObject(args, 'conventionalChangelogPresetLoader returned', presetConfig); return new Promise((resolve, reject) => { // convert an array of commits into a Stream, @@ -118,10 +164,7 @@ export async function changelog( } else { writeFile(args, args.changelogFile, args.changeLogHeader + '\n' + (content + oldContent).replace(/\n+$/, '\n')); } - return resolve({ - contents: content, - changedFiles: [args.changelogFile], - }); + return resolve(content); }); }); } diff --git a/tools/cdk-release/lib/types.ts b/tools/cdk-release/lib/types.ts index 822a5beba84d2..7dcbb12956f51 100644 --- a/tools/cdk-release/lib/types.ts +++ b/tools/cdk-release/lib/types.ts @@ -20,22 +20,41 @@ export interface Versions { export type ReleaseType = 'major' | 'minor' | 'patch'; -/* ****** main options ******** */ +/** How to handle experimental changes in the changelog. */ +export enum ExperimentalChangesTreatment { + /** Experimental changes are included in the main changelog (this is the default) */ + INCLUDE = 'include', + /** Remove all experimental changes from the changelog */ + STRIP = 'strip', + /** Write experimental changes to a separate changelog */ + SEPARATE = 'separate' +}; export interface ReleaseOptions { releaseAs: ReleaseType; skip?: LifecyclesSkip; versionFile: string; changelogFile: string; + alphaChangelogFile?: string; prerelease?: string; scripts?: Lifecycles; dryRun?: boolean; verbose?: boolean; silent?: boolean; sign?: boolean; - stripExperimentalChanges?: boolean; + /** + * How to handle experimental changes in the changelog. + * @default ExperimentalChangesTreatment.INCLUDE + */ + experimentalChangesTreatment?: ExperimentalChangesTreatment; changeLogHeader?: string; includeDateInChangelog?: boolean; releaseCommitMessageFormat?: string; } + +export interface PackageInfo { + name: string; + location: string; + alpha: boolean; +} diff --git a/tools/cdk-release/test/changelog.test.ts b/tools/cdk-release/test/changelog.test.ts index 67e01477e4ced..6e618d00a1389 100644 --- a/tools/cdk-release/test/changelog.test.ts +++ b/tools/cdk-release/test/changelog.test.ts @@ -1,14 +1,140 @@ import { ConventionalCommit } from '../lib/conventional-commits'; -import { changelog, ChangelogOptions } from '../lib/lifecycles/changelog'; - -describe('Changelog generation', () => { - const args: ChangelogOptions = { - changelogFile: 'CHANGELOG.md', - dryRun: true, - silent: true, - includeDateInChangelog: false, +import { changelog, ChangelogOptions, writeChangelogs } from '../lib/lifecycles/changelog'; +import { ExperimentalChangesTreatment, PackageInfo, Versions } from '../lib/types'; + +const args: ChangelogOptions = { + changelogFile: 'CHANGELOG.md', + dryRun: true, + silent: true, + includeDateInChangelog: false, +}; + +describe('writeChangelogs', () => { + + const currentVersion: Versions = { stableVersion: '1.23.0' }; + const newVersion: Versions = { stableVersion: '1.24.0' }; + + const commits: ConventionalCommit[] = [ + buildCommit({ type: 'feat', scope: 'aws-stable', subject: 'new stable feat' }), + buildCommit({ type: 'feat', scope: 'aws-experimental', subject: 'new experimental feat' }), + ]; + const packages: PackageInfo[] = [ + { name: 'aws-stable', alpha: false, location: 'aws-stable' }, + { name: 'aws-experimental', alpha: true, location: 'aws-experimental' }, + ]; + + const defaultWriteChangelogOpts = { + ...args, + currentVersion, + newVersion, + commits, + packages, }; + test('does nothing if skip.changelog is set', async () => { + const changelogResult = await writeChangelogs({ ...defaultWriteChangelogOpts, skip: { changelog: true } }); + + expect(changelogResult).toEqual([]); + }); + + test('defaults experimentalChangesTreatment to "include"', async () => { + const changelogResultDefault = await writeChangelogs({ + ...defaultWriteChangelogOpts, experimentalChangesTreatment: undefined, + }); + const changelogResultInclude = await writeChangelogs({ + ...defaultWriteChangelogOpts, experimentalChangesTreatment: ExperimentalChangesTreatment.INCLUDE, + }); + + expect(changelogResultDefault).toEqual(changelogResultInclude); + }); + + test('if experimentalChangesTreatment is "include", includes experimental changes', async () => { + const changelogResult = await writeChangelogs({ + ...defaultWriteChangelogOpts, experimentalChangesTreatment: ExperimentalChangesTreatment.INCLUDE, + }); + + expect(changelogResult.length).toEqual(1); + expect(changelogResult[0].filePath).toEqual('CHANGELOG.md'); + expect(changelogResult[0].fileContents.trim()).toBe( + `## [1.24.0](https://github.com/aws/aws-cdk/compare/v1.23.0...v1.24.0) + +### Features + +* **aws-experimental:** new experimental feat +* **aws-stable:** new stable feat`); + }); + + test('if changelogExperimentalChanges is "strip", excludes experimental changes', async () => { + const changelogResult = await writeChangelogs({ + ...defaultWriteChangelogOpts, experimentalChangesTreatment: ExperimentalChangesTreatment.STRIP, + }); + + expect(changelogResult.length).toEqual(1); + expect(changelogResult[0].filePath).toEqual('CHANGELOG.md'); + expect(changelogResult[0].fileContents.trim()).toBe( + `## [1.24.0](https://github.com/aws/aws-cdk/compare/v1.23.0...v1.24.0) + +### Features + +* **aws-stable:** new stable feat`); + }); + + describe('experimentalChangesTreatment is SEPARATE', () => { + + const defaultSeparateChangelogOpts = { + ...defaultWriteChangelogOpts, + experimentalChangesTreatment: ExperimentalChangesTreatment.SEPARATE, + currentVersion: { stableVersion: '1.23.0', alphaVersion: '1.23.0-alpha.0' }, + newVersion: { stableVersion: '1.24.0', alphaVersion: '1.24.0-alpha.0' }, + alphaChangelogFile: 'CHANGELOG.alpha.md', + }; + + test('throws if alpha versions are not present', async () => { + await expect(writeChangelogs({ + ...defaultSeparateChangelogOpts, + currentVersion: { stableVersion: '1.23.0' }, + newVersion: { stableVersion: '1.24.0' }, + })) + .rejects + .toThrow(/without alpha package versions/); + + await expect(writeChangelogs({ ...defaultSeparateChangelogOpts, newVersion: { stableVersion: '1.24.0' } })) + .rejects + .toThrow(/without alpha package versions/); + + await expect(writeChangelogs({ ...defaultSeparateChangelogOpts, currentVersion: { stableVersion: '1.23.0' } })) + .rejects + .toThrow(/without alpha package versions/); + }); + + test('throws if alpha changelog file is not present', async () => { + await expect(writeChangelogs({ ...defaultSeparateChangelogOpts, alphaChangelogFile: undefined })) + .rejects + .toThrow(/alphaChangelogFile must be specified/); + }); + + test('excludes experimental changes and writes to the alpha changelog', async () => { + const changelogResult = await writeChangelogs(defaultSeparateChangelogOpts); + + const mainResult = changelogResult.find(r => r.filePath === 'CHANGELOG.md'); + const alphaResult = changelogResult.find(r => r.filePath === 'CHANGELOG.alpha.md'); + expect(mainResult?.fileContents.trim()).toBe( + `## [1.24.0](https://github.com/aws/aws-cdk/compare/v1.23.0...v1.24.0) + +### Features + +* **aws-stable:** new stable feat`); + expect(alphaResult?.fileContents.trim()).toBe( + `## [1.24.0-alpha.0](https://github.com/aws/aws-cdk/compare/v1.23.0-alpha.0...v1.24.0-alpha.0) + +### Features + +* **aws-experimental:** new experimental feat`); + }); + }); +}); + +describe('changelog', () => { test("correctly handles 'BREAKING CHANGES'", async () => { const commits: ConventionalCommit[] = [ buildCommit({ @@ -68,6 +194,6 @@ function buildCommit(commit: PartialCommit): ConventionalCommit { }; } -async function invokeChangelogFrom1_23_0to1_24_0(args: ChangelogOptions, commits: ConventionalCommit[]): Promise { - return (await changelog(args, '1.23.0', '1.24.0', commits)).contents; +async function invokeChangelogFrom1_23_0to1_24_0(changelogArgs: ChangelogOptions, commits: ConventionalCommit[]): Promise { + return changelog(changelogArgs, '1.23.0', '1.24.0', commits); } diff --git a/tools/cdk-release/test/conventional-commits.test.ts b/tools/cdk-release/test/conventional-commits.test.ts index 773f229849e5d..000031f142074 100644 --- a/tools/cdk-release/test/conventional-commits.test.ts +++ b/tools/cdk-release/test/conventional-commits.test.ts @@ -39,38 +39,69 @@ describe('getConventionalCommitsFromGitHistory', () => { }); }); -// NOTE - These test currently use real package.json data to determine package's stability. describe('filterCommits', () => { const commits: ConventionalCommit[] = [ - buildCommit({ - type: 'feat', - scope: 'scope', - subject: 'super important feature', - }), - buildCommit({ - type: 'fix', - scope: 'example-construct-library', // really hope we don't stabilize this one - subject: 'hairy bugfix', - notes: [ - { - title: 'BREAKING CHANGE', - text: 'this is a breaking change', - }, - ], - }), + commitWithScope('aws-stable'), + commitWithScope('aws-experimental'), + commitWithScope(), ]; - test('if stripExperimental is not set, returns original commits', async () => { - const filteredCommits = filterCommits(args, commits); + test('if no options are provided, returns all commits', () => { + const filteredCommits = filterCommits(commits); + + expect(filteredCommits).toEqual(commits); + }); + + test('excludePackages removes commits matching scope', () => { + const filteredCommits = filterCommits(commits, { excludePackages: ['@aws-cdk/aws-experimental'] }); expect(filteredCommits.length).toEqual(2); + expect(filteredCommits.map(c => c.scope)).not.toContain('aws-experimental'); + }); + + test('excludePackages removes commits matching specific variants of the scope', () => { + const experimentalCommits = [ + commitWithScope('aws-experimental'), + commitWithScope('awsexperimental'), + commitWithScope('experimental'), + commitWithScope('aws.experimental'), + ]; + + const filteredCommits = filterCommits(experimentalCommits, { excludePackages: ['@aws-cdk/aws-experimental'] }); + + expect(filteredCommits.length).toEqual(1); + expect(filteredCommits[0].scope).toEqual('aws.experimental'); + }); + + test('includePackages only includes commits matching scope', () => { + const filteredCommits = filterCommits(commits, { includePackages: ['@aws-cdk/aws-stable'] }); + + expect(filteredCommits.length).toEqual(1); + expect(filteredCommits[0].scope).toEqual('aws-stable'); }); - test("skips experimental modules if requested, even with 'BREAKING CHANGES'", async () => { - const filteredCommits = filterCommits({ ...args, stripExperimentalChanges: true }, commits); + test('includePackages includes commits matching variants of the scope', () => { + const stableCommits = [ + commitWithScope('aws-stable'), + commitWithScope('awsstable'), + commitWithScope('stable'), + commitWithScope('notstable'), + ]; + + const filteredCommits = filterCommits(stableCommits, { includePackages: ['@aws-cdk/aws-stable'] }); + + expect(filteredCommits.length).toEqual(3); + expect(filteredCommits.map(c => c.scope)).not.toContain('notstable'); + }); + + test('excludes criteria are run after includes', () => { + const filteredCommits = filterCommits(commits, { + includePackages: ['@aws-cdk/aws-stable', '@aws-cdk/aws-experimental'], + excludePackages: ['@aws-cdk/aws-experimental'], + }); expect(filteredCommits.length).toEqual(1); - expect(filteredCommits[0].subject).toEqual('super important feature'); + expect(filteredCommits[0].scope).toEqual('aws-stable'); }); }); @@ -85,16 +116,13 @@ function mockGitCommits(messages: string[]) { return rStream; } -interface PartialCommit extends Partial { - readonly type: string; - readonly subject: string; -} - -function buildCommit(commit: PartialCommit): ConventionalCommit { +function commitWithScope(scope?: string): ConventionalCommit { return { notes: [], references: [], - header: `${commit.type}${commit.scope ? '(' + commit.scope + ')' : ''}: ${commit.subject}`, - ...commit, + header: `feat${scope ? '(' + scope + ')' : ''}: some commit message`, + type: 'feat', + subject: 'some commit message', + scope, }; } From ff6082caf7e534989fb8ee6b4c63c0c02e9a5ec0 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Wed, 1 Sep 2021 00:36:00 +0300 Subject: [PATCH 46/91] fix(core): inconsistent analytics string across operating systems (#16300) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gzip header includes an “OS” byte that indicates which operating system was used to perform the operation. This means that the CDK Metadata analytics string would be different for the same CDK app in two different operating systems. To address this, we explicitly set the [gzip OS flag] to 255 (unknown). Fixes #15322 [gzip OS flag]: https://datatracker.ietf.org/doc/html/rfc1952 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../core/lib/private/metadata-resource.ts | 51 ++++++++++++++++++- .../core/test/metadata-resource.test.ts | 7 +++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/core/lib/private/metadata-resource.ts b/packages/@aws-cdk/core/lib/private/metadata-resource.ts index 1cccc4f24ff3d..b74fe41e17a2e 100644 --- a/packages/@aws-cdk/core/lib/private/metadata-resource.ts +++ b/packages/@aws-cdk/core/lib/private/metadata-resource.ts @@ -74,8 +74,13 @@ export function formatAnalytics(infos: ConstructInfo[]) { infos.forEach(info => insertFqnInTrie(`${info.version}!${info.fqn}`, trie)); const plaintextEncodedConstructs = prefixEncodeTrie(trie); - const compressedConstructs = zlib.gzipSync(Buffer.from(plaintextEncodedConstructs)).toString('base64'); + const compressedConstructsBuffer = zlib.gzipSync(Buffer.from(plaintextEncodedConstructs)); + // set OS flag to "unknown" in order to ensure we get consistent results across operating systems + // see https://github.com/aws/aws-cdk/issues/15322 + setGzipOperatingSystemToUnknown(compressedConstructsBuffer); + + const compressedConstructs = compressedConstructsBuffer.toString('base64'); return `v2:deflate64:${compressedConstructs}`; } @@ -125,3 +130,47 @@ function prefixEncodeTrie(trie: Trie) { }); return prefixEncoded; } + +/** + * Sets the OS flag to "unknown" in order to ensure we get consistent results across operating systems. + * + * @see https://datatracker.ietf.org/doc/html/rfc1952#page-5 + * + * +---+---+---+---+---+---+---+---+---+---+ + * |ID1|ID2|CM |FLG| MTIME |XFL|OS | + * +---+---+---+---+---+---+---+---+---+---+ + * | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | + * +---+---+---+---+---+---+---+---+---+---+ + * + * OS (Operating System) + * ===================== + * This identifies the type of file system on which compression + * took place. This may be useful in determining end-of-line + * convention for text files. The currently defined values are + * as follows: + * 0 - FAT filesystem (MS-DOS, OS/2, NT/Win32) + * 1 - Amiga + * 2 - VMS (or OpenVMS) + * 3 - Unix + * 4 - VM/CMS + * 5 - Atari TOS + * 6 - HPFS filesystem (OS/2, NT) + * 7 - Macintosh + * 8 - Z-System + * 9 - CP/M + * 10 - TOPS-20 + * 11 - NTFS filesystem (NT) + * 12 - QDOS + * 13 - Acorn RISCOS + * 255 - unknown + * + * @param gzipBuffer A gzip buffer + */ +function setGzipOperatingSystemToUnknown(gzipBuffer: Buffer) { + // check that this is indeed a gzip buffer (https://datatracker.ietf.org/doc/html/rfc1952#page-6) + if (gzipBuffer[0] !== 0x1f || gzipBuffer[1] !== 0x8b) { + throw new Error('Expecting a gzip buffer (must start with 0x1f8b)'); + } + + gzipBuffer[9] = 255; +} \ No newline at end of file diff --git a/packages/@aws-cdk/core/test/metadata-resource.test.ts b/packages/@aws-cdk/core/test/metadata-resource.test.ts index ca87ae9d252f6..a1fed889d9894 100644 --- a/packages/@aws-cdk/core/test/metadata-resource.test.ts +++ b/packages/@aws-cdk/core/test/metadata-resource.test.ts @@ -125,6 +125,13 @@ describe('formatAnalytics', () => { expectAnalytics(constructInfo, '1.2.3!aws-cdk-lib.{Construct,CfnResource,Stack},0.1.2!aws-cdk-lib.{CoolResource,OtherResource}'); }); + test('ensure gzip is encoded with "unknown" operating system to maintain consistent output across systems', () => { + const constructInfo = [{ fqn: 'aws-cdk-lib.Construct', version: '1.2.3' }]; + const analytics = formatAnalytics(constructInfo); + const gzip = Buffer.from(analytics.split(':')[2], 'base64'); + expect(gzip[9]).toBe(255); + }); + // Compares the output of formatAnalytics with an expected (plaintext) output. // For ease of testing, the plaintext versions are compared rather than the encoded versions. function expectAnalytics(constructs: ConstructInfo[], expectedPlaintext: string) { From 5a5da573149d45bf6e29bf7155715fa926804871 Mon Sep 17 00:00:00 2001 From: Johs Kristoffersen Date: Wed, 1 Sep 2021 00:51:09 +0200 Subject: [PATCH 47/91] feat(stepfunctions-tasks): support allocation strategies in EMR CreateCluster (#16296) fixes #16252 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/emr/emr-create-cluster.ts | 25 +++- .../lib/emr/private/cluster-utils.ts | 1 + .../test/emr/emr-create-cluster.test.ts | 117 ++++++++++++++++++ 3 files changed, 141 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-create-cluster.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-create-cluster.ts index 5e1e1d708d2d2..bfb3d94e28b5c 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-create-cluster.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-create-cluster.ts @@ -544,7 +544,7 @@ export namespace EmrCreateCluster { * */ export enum SpotTimeoutAction { - /**\ + /** * SWITCH_TO_ON_DEMAND */ SWITCH_TO_ON_DEMAND = 'SWITCH_TO_ON_DEMAND', @@ -554,6 +554,21 @@ export namespace EmrCreateCluster { TERMINATE_CLUSTER = 'TERMINATE_CLUSTER', } + /** + * Spot Allocation Strategies + * + * Specifies the strategy to use in launching Spot Instance fleets. For example, "capacity-optimized" launches instances from Spot Instance pools with optimal capacity for the number of instances that are launching. + * + * @see https://docs.aws.amazon.com/emr/latest/APIReference/API_SpotProvisioningSpecification.html + * + */ + export enum SpotAllocationStrategy { + /** + * Capacity-optimized, which launches instances from Spot Instance pools with optimal capacity for the number of instances that are launching. + */ + CAPACITY_OPTIMIZED = 'capacity-optimized', + } + /** * The launch specification for Spot instances in the instance fleet, which determines the defined duration and provisioning timeout behavior. * @@ -561,10 +576,16 @@ export namespace EmrCreateCluster { * */ export interface SpotProvisioningSpecificationProperty { + /** + * Specifies the strategy to use in launching Spot Instance fleets. + * + * @default - No allocation strategy, i.e. spot instance type will be chosen based on current price only + */ + readonly allocationStrategy?: SpotAllocationStrategy; /** * The defined duration for Spot instances (also known as Spot blocks) in minutes. * - * @default No blockDurationMinutes + * @default - No blockDurationMinutes */ readonly blockDurationMinutes?: number; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/private/cluster-utils.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/private/cluster-utils.ts index 37cf474c83ea0..c8ae8a50a360c 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/private/cluster-utils.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/private/cluster-utils.ts @@ -123,6 +123,7 @@ export function InstanceTypeConfigPropertyToJson(property: EmrCreateCluster.Inst export function InstanceFleetProvisioningSpecificationsPropertyToJson(property: EmrCreateCluster.InstanceFleetProvisioningSpecificationsProperty) { return { SpotSpecification: { + AllocationStrategy: cdk.stringToCloudFormation(property.spotSpecification.allocationStrategy), BlockDurationMinutes: cdk.numberToCloudFormation(property.spotSpecification.blockDurationMinutes), TimeoutAction: cdk.stringToCloudFormation(property.spotSpecification.timeoutAction?.valueOf()), TimeoutDurationMinutes: cdk.numberToCloudFormation(property.spotSpecification.timeoutDurationMinutes), diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-create-cluster.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-create-cluster.test.ts index 5f40b28be29ac..273a580a6a38e 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-create-cluster.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-create-cluster.test.ts @@ -641,6 +641,123 @@ test('Create Cluster with Instances configuration', () => { }); }); +test('Create Cluster with InstanceFleet with allocation strategy=capacity-optimized', () => { + // WHEN + const task = new EmrCreateCluster(stack, 'Task', { + instances: { + instanceFleets: [{ + instanceFleetType: EmrCreateCluster.InstanceRoleType.MASTER, + instanceTypeConfigs: [{ + bidPrice: '1', + bidPriceAsPercentageOfOnDemandPrice: 1, + configurations: [{ + classification: 'Classification', + properties: { + Key: 'Value', + }, + }], + ebsConfiguration: { + ebsBlockDeviceConfigs: [{ + volumeSpecification: { + iops: 1, + volumeSize: cdk.Size.gibibytes(1), + volumeType: EmrCreateCluster.EbsBlockDeviceVolumeType.STANDARD, + }, + volumesPerInstance: 1, + }], + ebsOptimized: true, + }, + instanceType: 'm5.xlarge', + weightedCapacity: 1, + }], + launchSpecifications: { + spotSpecification: { + allocationStrategy: EmrCreateCluster.SpotAllocationStrategy.CAPACITY_OPTIMIZED, + blockDurationMinutes: 1, + timeoutAction: EmrCreateCluster.SpotTimeoutAction.TERMINATE_CLUSTER, + timeoutDurationMinutes: 1, + }, + }, + name: 'Master', + targetOnDemandCapacity: 1, + targetSpotCapacity: 1, + }], + }, + clusterRole, + name: 'Cluster', + serviceRole, + integrationPattern: sfn.IntegrationPattern.REQUEST_RESPONSE, + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::elasticmapreduce:createCluster', + ], + ], + }, + End: true, + Parameters: { + Name: 'Cluster', + Instances: { + KeepJobFlowAliveWhenNoSteps: true, + InstanceFleets: [{ + InstanceFleetType: 'MASTER', + InstanceTypeConfigs: [{ + BidPrice: '1', + BidPriceAsPercentageOfOnDemandPrice: 1, + Configurations: [{ + Classification: 'Classification', + Properties: { + Key: 'Value', + }, + }], + EbsConfiguration: { + EbsBlockDeviceConfigs: [{ + VolumeSpecification: { + Iops: 1, + SizeInGB: 1, + VolumeType: 'standard', + }, + VolumesPerInstance: 1, + }], + EbsOptimized: true, + }, + InstanceType: 'm5.xlarge', + WeightedCapacity: 1, + }], + LaunchSpecifications: { + SpotSpecification: { + AllocationStrategy: 'capacity-optimized', + BlockDurationMinutes: 1, + TimeoutAction: 'TERMINATE_CLUSTER', + TimeoutDurationMinutes: 1, + }, + }, + Name: 'Master', + TargetOnDemandCapacity: 1, + TargetSpotCapacity: 1, + }], + }, + VisibleToAllUsers: true, + JobFlowRole: { + Ref: 'ClusterRoleD9CA7471', + }, + ServiceRole: { + Ref: 'ServiceRole4288B192', + }, + }, + }); +}); + test('Create Cluster with InstanceFleet', () => { // WHEN const task = new EmrCreateCluster(stack, 'Task', { From 97b3b979c0a3cfc301acd772ba1ca7253a3f624d Mon Sep 17 00:00:00 2001 From: AWS CDK Team Date: Wed, 1 Sep 2021 07:59:20 +0000 Subject: [PATCH 48/91] chore(release): 1.121.0 --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ version.v1.json | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd16881355b70..22a49e715b1dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,33 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.121.0](https://github.com/aws/aws-cdk/compare/v1.120.0...v1.121.0) (2021-09-01) + + +### Features + +* **assertions:** 'not' matcher ([#16240](https://github.com/aws/aws-cdk/issues/16240)) ([b838f95](https://github.com/aws/aws-cdk/commit/b838f95f0905316fe706779381c93bedaa9ad504)), closes [#15868](https://github.com/aws/aws-cdk/issues/15868) +* **aws-cloudfront-origins:** add custom headers to S3Origin ([#16161](https://github.com/aws/aws-cdk/issues/16161)) ([f42b233](https://github.com/aws/aws-cdk/commit/f42b233a76ae810634fa43a25604dbc65bdd63b9)), closes [#16160](https://github.com/aws/aws-cdk/issues/16160) +* **cfnspec:** cloudformation spec v40.1.0 ([#16254](https://github.com/aws/aws-cdk/issues/16254)) ([fe81be7](https://github.com/aws/aws-cdk/commit/fe81be78322e3f1c23d2b02e59b56faa3b06e554)) +* **cli:** support `--no-rollback` flag ([#16293](https://github.com/aws/aws-cdk/issues/16293)) ([d763d90](https://github.com/aws/aws-cdk/commit/d763d9092289d0b28b2695b8474b44ed7d0bce54)), closes [#16289](https://github.com/aws/aws-cdk/issues/16289) +* **core:** normalize line endings in asset hash calculation ([#16276](https://github.com/aws/aws-cdk/issues/16276)) ([01bf6e2](https://github.com/aws/aws-cdk/commit/01bf6e2922994e7d41c8c6b171aa1693835f2b53)) +* **ec2:** add m6i instances ([#16081](https://github.com/aws/aws-cdk/issues/16081)) ([a42a1ea](https://github.com/aws/aws-cdk/commit/a42a1ea5a122f864936cdb0113b16fe92cc7205e)) +* **ecs:** add support for Fargate PV1.4 ephemeral storage ([#15440](https://github.com/aws/aws-cdk/issues/15440)) ([f1bf935](https://github.com/aws/aws-cdk/commit/f1bf935c47006096b33fb7bf0c847ffab9230870)), closes [#14570](https://github.com/aws/aws-cdk/issues/14570) +* **ecs-patterns:** add capacity provider strategies to queue processing service pattern ([#15684](https://github.com/aws/aws-cdk/issues/15684)) ([f40e8d6](https://github.com/aws/aws-cdk/commit/f40e8d6a502dd42e0a52d81f72abecaa2cdd920a)), closes [#14781](https://github.com/aws/aws-cdk/issues/14781) +* **ecs-patterns:** Allow configuration of SSL policy for listeners created by ECS patterns ([#15210](https://github.com/aws/aws-cdk/issues/15210)) ([2c3d21e](https://github.com/aws/aws-cdk/commit/2c3d21e2f1117a54510ba92748588ee95ab3631c)), closes [#11841](https://github.com/aws/aws-cdk/issues/11841) [#8816](https://github.com/aws/aws-cdk/issues/8816) +* **route53resolver:** DNS Firewall ([#15031](https://github.com/aws/aws-cdk/issues/15031)) ([ffdcd94](https://github.com/aws/aws-cdk/commit/ffdcd94405c160763e396a191d5af793ac8db998)) +* **stepfunctions-tasks:** support allocation strategies in EMR CreateCluster ([#16296](https://github.com/aws/aws-cdk/issues/16296)) ([5a5da57](https://github.com/aws/aws-cdk/commit/5a5da573149d45bf6e29bf7155715fa926804871)), closes [#16252](https://github.com/aws/aws-cdk/issues/16252) +* **synthetics:** add Python runtime and latest Nodejs runtime ([#16069](https://github.com/aws/aws-cdk/issues/16069)) ([de218ba](https://github.com/aws/aws-cdk/commit/de218ba3a294b5b98f93fc75a04ce42294e95008)), closes [#15138](https://github.com/aws/aws-cdk/issues/15138) [#16177](https://github.com/aws/aws-cdk/issues/16177) + + +### Bug Fixes + +* **apigatewayv2:** api mapping key with two hyphens is disallowed ([#16204](https://github.com/aws/aws-cdk/issues/16204)) ([0889564](https://github.com/aws/aws-cdk/commit/0889564a0c1b04d33909dd3fdb42147f23d67cbd)), closes [#15948](https://github.com/aws/aws-cdk/issues/15948) +* **aws-rds:** fromDatabaseInstanceAttributes incorrectly stringifies ports with tokens ([#16286](https://github.com/aws/aws-cdk/issues/16286)) ([41b831a](https://github.com/aws/aws-cdk/commit/41b831a6698ee6c7a3c8968bff8273a0c7f35448)), closes [#11813](https://github.com/aws/aws-cdk/issues/11813) +* **core:** inconsistent analytics string across operating systems ([#16300](https://github.com/aws/aws-cdk/issues/16300)) ([ff6082c](https://github.com/aws/aws-cdk/commit/ff6082caf7e534989fb8ee6b4c63c0c02e9a5ec0)), closes [#15322](https://github.com/aws/aws-cdk/issues/15322) +* **docs:** unnecessary log group in Step Functions state machine x-ray example ([#16159](https://github.com/aws/aws-cdk/issues/16159)) ([04d4547](https://github.com/aws/aws-cdk/commit/04d45474d80d3687a3fdf27f4d76dd1c8521eff0)) +* **elasticloadbalancingv2:** target group health check does not validate interval versus timeout ([#16107](https://github.com/aws/aws-cdk/issues/16107)) ([a85ad39](https://github.com/aws/aws-cdk/commit/a85ad392459c815d5c8e645dd3e8240d059024e6)), closes [#3703](https://github.com/aws/aws-cdk/issues/3703) + ## [1.120.0](https://github.com/aws/aws-cdk/compare/v1.119.0...v1.120.0) (2021-08-26) diff --git a/version.v1.json b/version.v1.json index 7ff9a577d56ba..be934684cecce 100644 --- a/version.v1.json +++ b/version.v1.json @@ -1,3 +1,3 @@ { - "version": "1.120.0" + "version": "1.121.0" } \ No newline at end of file From 1cf068dbf068d03ece47ed7617008ea7a55e0cdd Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Wed, 1 Sep 2021 11:08:06 +0300 Subject: [PATCH 49/91] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22a49e715b1dc..638fe5a43e6e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ All notable changes to this project will be documented in this file. See [standa ### Features * **assertions:** 'not' matcher ([#16240](https://github.com/aws/aws-cdk/issues/16240)) ([b838f95](https://github.com/aws/aws-cdk/commit/b838f95f0905316fe706779381c93bedaa9ad504)), closes [#15868](https://github.com/aws/aws-cdk/issues/15868) -* **aws-cloudfront-origins:** add custom headers to S3Origin ([#16161](https://github.com/aws/aws-cdk/issues/16161)) ([f42b233](https://github.com/aws/aws-cdk/commit/f42b233a76ae810634fa43a25604dbc65bdd63b9)), closes [#16160](https://github.com/aws/aws-cdk/issues/16160) +* **cloudfront-origins:** add custom headers to S3Origin ([#16161](https://github.com/aws/aws-cdk/issues/16161)) ([f42b233](https://github.com/aws/aws-cdk/commit/f42b233a76ae810634fa43a25604dbc65bdd63b9)), closes [#16160](https://github.com/aws/aws-cdk/issues/16160) * **cfnspec:** cloudformation spec v40.1.0 ([#16254](https://github.com/aws/aws-cdk/issues/16254)) ([fe81be7](https://github.com/aws/aws-cdk/commit/fe81be78322e3f1c23d2b02e59b56faa3b06e554)) * **cli:** support `--no-rollback` flag ([#16293](https://github.com/aws/aws-cdk/issues/16293)) ([d763d90](https://github.com/aws/aws-cdk/commit/d763d9092289d0b28b2695b8474b44ed7d0bce54)), closes [#16289](https://github.com/aws/aws-cdk/issues/16289) * **core:** normalize line endings in asset hash calculation ([#16276](https://github.com/aws/aws-cdk/issues/16276)) ([01bf6e2](https://github.com/aws/aws-cdk/commit/01bf6e2922994e7d41c8c6b171aa1693835f2b53)) From 78b4660db79ce4dd3974b074d828414054bb9179 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Wed, 1 Sep 2021 11:08:39 +0300 Subject: [PATCH 50/91] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 638fe5a43e6e2..e50659a3c7123 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ All notable changes to this project will be documented in this file. See [standa ### Bug Fixes * **apigatewayv2:** api mapping key with two hyphens is disallowed ([#16204](https://github.com/aws/aws-cdk/issues/16204)) ([0889564](https://github.com/aws/aws-cdk/commit/0889564a0c1b04d33909dd3fdb42147f23d67cbd)), closes [#15948](https://github.com/aws/aws-cdk/issues/15948) -* **aws-rds:** fromDatabaseInstanceAttributes incorrectly stringifies ports with tokens ([#16286](https://github.com/aws/aws-cdk/issues/16286)) ([41b831a](https://github.com/aws/aws-cdk/commit/41b831a6698ee6c7a3c8968bff8273a0c7f35448)), closes [#11813](https://github.com/aws/aws-cdk/issues/11813) +* **rds:** fromDatabaseInstanceAttributes incorrectly stringifies ports with tokens ([#16286](https://github.com/aws/aws-cdk/issues/16286)) ([41b831a](https://github.com/aws/aws-cdk/commit/41b831a6698ee6c7a3c8968bff8273a0c7f35448)), closes [#11813](https://github.com/aws/aws-cdk/issues/11813) * **core:** inconsistent analytics string across operating systems ([#16300](https://github.com/aws/aws-cdk/issues/16300)) ([ff6082c](https://github.com/aws/aws-cdk/commit/ff6082caf7e534989fb8ee6b4c63c0c02e9a5ec0)), closes [#15322](https://github.com/aws/aws-cdk/issues/15322) * **docs:** unnecessary log group in Step Functions state machine x-ray example ([#16159](https://github.com/aws/aws-cdk/issues/16159)) ([04d4547](https://github.com/aws/aws-cdk/commit/04d45474d80d3687a3fdf27f4d76dd1c8521eff0)) * **elasticloadbalancingv2:** target group health check does not validate interval versus timeout ([#16107](https://github.com/aws/aws-cdk/issues/16107)) ([a85ad39](https://github.com/aws/aws-cdk/commit/a85ad392459c815d5c8e645dd3e8240d059024e6)), closes [#3703](https://github.com/aws/aws-cdk/issues/3703) From 74af7afc6a741061a40d28892d6164a7d39f8a92 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Wed, 1 Sep 2021 10:00:13 +0100 Subject: [PATCH 51/91] Apply suggestions from code review --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e50659a3c7123..0c4b63bd89989 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,9 +24,8 @@ All notable changes to this project will be documented in this file. See [standa ### Bug Fixes * **apigatewayv2:** api mapping key with two hyphens is disallowed ([#16204](https://github.com/aws/aws-cdk/issues/16204)) ([0889564](https://github.com/aws/aws-cdk/commit/0889564a0c1b04d33909dd3fdb42147f23d67cbd)), closes [#15948](https://github.com/aws/aws-cdk/issues/15948) -* **rds:** fromDatabaseInstanceAttributes incorrectly stringifies ports with tokens ([#16286](https://github.com/aws/aws-cdk/issues/16286)) ([41b831a](https://github.com/aws/aws-cdk/commit/41b831a6698ee6c7a3c8968bff8273a0c7f35448)), closes [#11813](https://github.com/aws/aws-cdk/issues/11813) +* **rds:** `fromDatabaseInstanceAttributes()` incorrectly stringifies ports with tokens ([#16286](https://github.com/aws/aws-cdk/issues/16286)) ([41b831a](https://github.com/aws/aws-cdk/commit/41b831a6698ee6c7a3c8968bff8273a0c7f35448)), closes [#11813](https://github.com/aws/aws-cdk/issues/11813) * **core:** inconsistent analytics string across operating systems ([#16300](https://github.com/aws/aws-cdk/issues/16300)) ([ff6082c](https://github.com/aws/aws-cdk/commit/ff6082caf7e534989fb8ee6b4c63c0c02e9a5ec0)), closes [#15322](https://github.com/aws/aws-cdk/issues/15322) -* **docs:** unnecessary log group in Step Functions state machine x-ray example ([#16159](https://github.com/aws/aws-cdk/issues/16159)) ([04d4547](https://github.com/aws/aws-cdk/commit/04d45474d80d3687a3fdf27f4d76dd1c8521eff0)) * **elasticloadbalancingv2:** target group health check does not validate interval versus timeout ([#16107](https://github.com/aws/aws-cdk/issues/16107)) ([a85ad39](https://github.com/aws/aws-cdk/commit/a85ad392459c815d5c8e645dd3e8240d059024e6)), closes [#3703](https://github.com/aws/aws-cdk/issues/3703) ## [1.120.0](https://github.com/aws/aws-cdk/compare/v1.119.0...v1.120.0) (2021-08-26) From 61c251c21887ca15184618d7ab858b6a7907f96d Mon Sep 17 00:00:00 2001 From: Madeline Kusters <80541297+madeline-k@users.noreply.github.com> Date: Wed, 1 Sep 2021 10:26:00 -0700 Subject: [PATCH 52/91] chore(resolve-version): export the alphaVersion (#16321) This will be used by a change in `tools/individual-pkg-gen/copy-files-removing-deps.ts` on the `v2-main` branch, so that the `transform` step can correctly set the `version` key and the version for each dependency in each alpha module's `package.json` file. See https://github.com/aws/aws-cdk/pull/16322 for usage. Part of #15591 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- scripts/resolve-version-lib.js | 4 +- scripts/script-tests/resolve-version.test.js | 39 ++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/scripts/resolve-version-lib.js b/scripts/resolve-version-lib.js index ec1f7b1b83822..5885d96b6afd9 100755 --- a/scripts/resolve-version-lib.js +++ b/scripts/resolve-version-lib.js @@ -37,7 +37,8 @@ function resolveVersion(rootdir) { // validate that current version matches the requirements // - const currentVersion = require(versionFilePath).version; + const versions = require(versionFilePath); + const currentVersion = versions.version; if (!currentVersion.startsWith(`${majorVersion}.`)) { throw new Error(`current version "${currentVersion}" does not use the expected major version ${majorVersion}`); } @@ -68,6 +69,7 @@ function resolveVersion(rootdir) { return { version: currentVersion, + alphaVersion: versions.alphaVersion, versionFile, changelogFile, alphaChangelogFile, diff --git a/scripts/script-tests/resolve-version.test.js b/scripts/script-tests/resolve-version.test.js index 693e29234f023..1cabbb2acd795 100644 --- a/scripts/script-tests/resolve-version.test.js +++ b/scripts/script-tests/resolve-version.test.js @@ -17,6 +17,24 @@ happy({ marker: '0.0.0', prerelease: undefined, version: '2.1.0', + alphaVersion: undefined, + versionFile: 'version.v2.json' + } +}); + +happy({ + name: 'stable release with alpha modules', + inputs: { + 'release.json': { majorVersion: 2, releaseType: 'stable' }, + 'version.v2.json': { version: '2.1.0', alphaVersion: '2.1.0-alpha' }, + }, + expected: { + changelogFile: 'CHANGELOG.v2.md', + alphaChangelogFile: 'CHANGELOG.v2.alpha.md', + marker: '0.0.0', + prerelease: undefined, + version: '2.1.0', + alphaVersion: '2.1.0-alpha', versionFile: 'version.v2.json' } }); @@ -33,6 +51,7 @@ happy({ marker: '0.0.0', prerelease: 'alpha', version: '2.1.0-alpha.0', + alphaVersion: undefined, versionFile: 'version.v2.json' } }); @@ -49,6 +68,24 @@ happy({ marker: '0.0.0', prerelease: 'rc', version: '2.1.0-rc.0', + alphaVersion: undefined, + versionFile: 'version.v2.json' + } +}); + +happy({ + name: 'rc release with alpha modules', + inputs: { + 'release.json': { majorVersion: 2, releaseType: 'rc' }, + 'version.v2.json': { version: '2.0.0-rc.0', alphaVersion: '2.0.0-alpha.0' }, + }, + expected: { + changelogFile: 'CHANGELOG.v2.md', + alphaChangelogFile: 'CHANGELOG.v2.alpha.md', + marker: '0.0.0', + prerelease: 'rc', + version: '2.0.0-rc.0', + alphaVersion: '2.0.0-alpha.0', versionFile: 'version.v2.json' } }); @@ -65,6 +102,7 @@ happy({ marker: '0.0.0', prerelease: undefined, version: '1.72.0', + alphaVersion: undefined, versionFile: 'version.v1.json' } }); @@ -81,6 +119,7 @@ happy({ marker: '0.0.0', prerelease: undefined, version: '2.0.0-rc.0', + alphaVersion: undefined, versionFile: 'version.v2.json' } }); From 46ea434e4dd3aa1f2d6d35217315c0464781d914 Mon Sep 17 00:00:00 2001 From: kaizen3031593 Date: Wed, 1 Sep 2021 13:53:21 -0400 Subject: [PATCH 53/91] initial implementation of adding outputname to hasoutput --- .../assertions/lib/private/outputs.ts | 15 ++++-- packages/@aws-cdk/assertions/lib/template.ts | 5 +- .../@aws-cdk/assertions/test/template.test.ts | 50 +++++++++++++++++-- 3 files changed, 60 insertions(+), 10 deletions(-) diff --git a/packages/@aws-cdk/assertions/lib/private/outputs.ts b/packages/@aws-cdk/assertions/lib/private/outputs.ts index 46e5a6cb1d52b..0b328ffda7fcb 100644 --- a/packages/@aws-cdk/assertions/lib/private/outputs.ts +++ b/packages/@aws-cdk/assertions/lib/private/outputs.ts @@ -12,20 +12,25 @@ export function findOutputs(inspector: StackInspector, props: any = {}): { [key: return result.matches; } -export function hasOutput(inspector: StackInspector, props: any): string | void { +export function hasOutput(inspector: StackInspector, outputName: string, props: any): string | void { const section: { [key: string]: {} } = inspector.value.Outputs; - const result = matchSection(section, props); - + const result = matchSection(filterName(section, outputName), props); if (result.match) { return; } if (result.closestResult === undefined) { - return 'No outputs found in the template'; + return `No outputs named ${outputName} found in the template.`; } return [ - `Template has ${result.analyzedCount} outputs, but none match as expected.`, + `Template has ${result.analyzedCount} outputs named ${outputName}, but none match as expected.`, formatFailure(result.closestResult), ].join('\n'); +} + +function filterName(section: { [key: string]: {} }, outputName: string): { [key: string]: {} } { + return Object.entries(section ?? {}) + .filter(([k, _]) => k === outputName) + .reduce((agg, [k, v]) => { return { ...agg, [k]: v }; }, {}); } \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/template.ts b/packages/@aws-cdk/assertions/lib/template.ts index 848c46bcc295a..2f9f84c54cabb 100644 --- a/packages/@aws-cdk/assertions/lib/template.ts +++ b/packages/@aws-cdk/assertions/lib/template.ts @@ -109,10 +109,11 @@ export class Template { * Assert that an Output with the given properties exists in the CloudFormation template. * By default, performs partial matching on the resource, via the `Match.objectLike()`. * To configure different behavour, use other matchers in the `Match` class. + * @param outputName the name of the output. * @param props the output as should be expected in the template. */ - public hasOutput(props: any): void { - const matchError = hasOutput(this.inspector, props); + public hasOutput(outputName: string, props: any): void { + const matchError = hasOutput(this.inspector, outputName, props); if (matchError) { throw new Error(matchError); } diff --git a/packages/@aws-cdk/assertions/test/template.test.ts b/packages/@aws-cdk/assertions/test/template.test.ts index 50fb60a1a27f7..fc3c34deecd57 100644 --- a/packages/@aws-cdk/assertions/test/template.test.ts +++ b/packages/@aws-cdk/assertions/test/template.test.ts @@ -342,7 +342,7 @@ describe('Template', () => { }); const inspect = Template.fromStack(stack); - expect(() => inspect.hasOutput({ Value: 'Bar' })).not.toThrow(); + expect(() => inspect.hasOutput('Foo', { Value: 'Bar' })).not.toThrow(); }); test('not matching', (done) => { @@ -357,18 +357,62 @@ describe('Template', () => { const inspect = Template.fromStack(stack); expectToThrow( - () => inspect.hasOutput({ + () => inspect.hasOutput('Foo', { Value: 'Bar', Export: { Name: 'ExportBaz' }, }), [ - /2 outputs/, + /1 outputs named Foo/, /Expected ExportBaz but received ExportBar/, ], done, ); done(); }); + + test('outputName not matching', (done) => { + const stack = new Stack(); + new CfnOutput(stack, 'Foo', { + value: 'Bar', + }); + new CfnOutput(stack, 'Fred', { + value: 'Baz', + }); + + const inspect = Template.fromStack(stack); + expectToThrow( + () => inspect.hasOutput('Fred', { + Value: 'Bar', + }), + [ + /1 outputs named Fred/, + /Expected Bar but received Baz/, + ], + done, + ); + done(); + }); + }); + + test('name not matching', (done) => { + const stack = new Stack(); + new CfnOutput(stack, 'Foo', { + value: 'Bar', + exportName: 'ExportBar', + }); + + const inspect = Template.fromStack(stack); + expectToThrow( + () => inspect.hasOutput('Fred', { + Value: 'Bar', + Export: { Name: 'ExportBar' }, + }), + [ + /No outputs named Fred found in the template./, + ], + done, + ); + done(); }); describe('findOutputs', () => { From 839fdb90f4d68bf0f49fe6b8358af2870acb891d Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Wed, 1 Sep 2021 19:20:28 +0100 Subject: [PATCH 54/91] chore(yarn-cling): rename test packages to real package names (#16325) The tests for `yarn-cling` currently reference what were imaginary package names as placeholders; those package names have now been registered. While this has no practical impact, switching the package names to be ones we own just so there's no ambiguity. An `npm install` is never done on these directories, so the packages chosen really doesn't matter. Just picked two of our favorites at random. :) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- tools/yarn-cling/.gitignore | 4 +-- tools/yarn-cling/package.json | 2 +- tools/yarn-cling/test/cling.test.ts | 32 +++++++++---------- .../{package2 => cdk}/package.json | 2 +- .../node_modules/aws-cdk-lib/package.json | 4 +++ .../jsii/node_modules/aws-cdk/package.json | 7 ++++ .../test/test-fixture/jsii/node_modules/cdk | 1 + .../test/test-fixture/jsii/package.json | 8 +++++ .../package1/node_modules/package2 | 1 - .../registrydependency1/package.json | 7 ---- .../registrydependency2/package.json | 4 --- .../test/test-fixture/package1/package.json | 8 ----- tools/yarn-cling/test/test-fixture/yarn.lock | 4 +-- 13 files changed, 42 insertions(+), 42 deletions(-) rename tools/yarn-cling/test/test-fixture/{package2 => cdk}/package.json (52%) create mode 100644 tools/yarn-cling/test/test-fixture/jsii/node_modules/aws-cdk-lib/package.json create mode 100644 tools/yarn-cling/test/test-fixture/jsii/node_modules/aws-cdk/package.json create mode 120000 tools/yarn-cling/test/test-fixture/jsii/node_modules/cdk create mode 100644 tools/yarn-cling/test/test-fixture/jsii/package.json delete mode 120000 tools/yarn-cling/test/test-fixture/package1/node_modules/package2 delete mode 100644 tools/yarn-cling/test/test-fixture/package1/node_modules/registrydependency1/package.json delete mode 100644 tools/yarn-cling/test/test-fixture/package1/node_modules/registrydependency2/package.json delete mode 100644 tools/yarn-cling/test/test-fixture/package1/package.json diff --git a/tools/yarn-cling/.gitignore b/tools/yarn-cling/.gitignore index 884f5381e01ab..bef31def37928 100644 --- a/tools/yarn-cling/.gitignore +++ b/tools/yarn-cling/.gitignore @@ -11,5 +11,5 @@ dist coverage nyc.config.js !.eslintrc.js -!test/test-fixture/package1/node_modules/ -junit.xml \ No newline at end of file +!test/test-fixture/jsii/node_modules/ +junit.xml diff --git a/tools/yarn-cling/package.json b/tools/yarn-cling/package.json index 52501bf50f83e..9d86cc671b4d4 100644 --- a/tools/yarn-cling/package.json +++ b/tools/yarn-cling/package.json @@ -16,7 +16,7 @@ "build": "tsc", "watch": "tsc -w", "pkglint": "pkglint -f", - "test": "ln -sf ../../package2 test/test-fixture/package1/node_modules/ && jest", + "test": "ln -sf ../../cdk test/test-fixture/jsii/node_modules/ && jest", "build+test+package": "yarn build+test", "build+test": "yarn build && yarn test", "build+extract": "yarn build", diff --git a/tools/yarn-cling/test/cling.test.ts b/tools/yarn-cling/test/cling.test.ts index 1628d2415a1ae..f56e98f535f91 100644 --- a/tools/yarn-cling/test/cling.test.ts +++ b/tools/yarn-cling/test/cling.test.ts @@ -3,22 +3,22 @@ import { checkRequiredVersions, generateShrinkwrap } from '../lib'; test('generate lock for fixture directory', async () => { const lockFile = await generateShrinkwrap({ - packageJsonFile: path.join(__dirname, 'test-fixture', 'package1', 'package.json'), + packageJsonFile: path.join(__dirname, 'test-fixture', 'jsii', 'package.json'), hoist: false, }); expect(lockFile).toEqual({ lockfileVersion: 1, - name: 'package1', + name: 'jsii', requires: true, version: '1.1.1', dependencies: { - package2: { + 'cdk': { version: '2.2.2', }, - registrydependency1: { + 'aws-cdk': { dependencies: { - registrydependency2: { + 'aws-cdk-lib': { integrity: 'sha512-pineapple', resolved: 'https://registry.bla.com/stuff', version: '2.3.999', @@ -26,7 +26,7 @@ test('generate lock for fixture directory', async () => { }, integrity: 'sha512-banana', requires: { - registrydependency2: '^2.3.4', + 'aws-cdk-lib': '^2.3.4', }, resolved: 'https://registry.bla.com/stuff', version: '1.2.999', @@ -37,28 +37,28 @@ test('generate lock for fixture directory', async () => { test('generate hoisted lock for fixture directory', async () => { const lockFile = await generateShrinkwrap({ - packageJsonFile: path.join(__dirname, 'test-fixture', 'package1', 'package.json'), + packageJsonFile: path.join(__dirname, 'test-fixture', 'jsii', 'package.json'), hoist: true, }); expect(lockFile).toEqual({ lockfileVersion: 1, - name: 'package1', + name: 'jsii', requires: true, version: '1.1.1', dependencies: { - package2: { + 'cdk': { version: '2.2.2', }, - registrydependency1: { + 'aws-cdk': { integrity: 'sha512-banana', requires: { - registrydependency2: '^2.3.4', + 'aws-cdk-lib': '^2.3.4', }, resolved: 'https://registry.bla.com/stuff', version: '1.2.999', }, - registrydependency2: { + 'aws-cdk-lib': { integrity: 'sha512-pineapple', resolved: 'https://registry.bla.com/stuff', version: '2.3.999', @@ -70,17 +70,17 @@ test('generate hoisted lock for fixture directory', async () => { test('fail when requires cannot be satisfied', async () => { const lockFile = { lockfileVersion: 1, - name: 'package1', + name: 'jsii', requires: true, version: '1.1.1', dependencies: { - package1: { + jsii: { version: '2.2.2', requires: { - package2: '^3.3.3', // <- this needs to be adjusted + cdk: '^3.3.3', // <- this needs to be adjusted }, }, - package2: { + cdk: { version: '4.4.4', }, }, diff --git a/tools/yarn-cling/test/test-fixture/package2/package.json b/tools/yarn-cling/test/test-fixture/cdk/package.json similarity index 52% rename from tools/yarn-cling/test/test-fixture/package2/package.json rename to tools/yarn-cling/test/test-fixture/cdk/package.json index d10f139d13944..8594e124f71b7 100644 --- a/tools/yarn-cling/test/test-fixture/package2/package.json +++ b/tools/yarn-cling/test/test-fixture/cdk/package.json @@ -1,4 +1,4 @@ { - "name": "package2", + "name": "cdk", "version": "2.2.2" } diff --git a/tools/yarn-cling/test/test-fixture/jsii/node_modules/aws-cdk-lib/package.json b/tools/yarn-cling/test/test-fixture/jsii/node_modules/aws-cdk-lib/package.json new file mode 100644 index 0000000000000..2721d93a758df --- /dev/null +++ b/tools/yarn-cling/test/test-fixture/jsii/node_modules/aws-cdk-lib/package.json @@ -0,0 +1,4 @@ +{ + "name": "aws-cdk-lib", + "version": "2.3.999" +} diff --git a/tools/yarn-cling/test/test-fixture/jsii/node_modules/aws-cdk/package.json b/tools/yarn-cling/test/test-fixture/jsii/node_modules/aws-cdk/package.json new file mode 100644 index 0000000000000..f074216bccb3b --- /dev/null +++ b/tools/yarn-cling/test/test-fixture/jsii/node_modules/aws-cdk/package.json @@ -0,0 +1,7 @@ +{ + "name": "aws-cdk", + "version": "1.2.999", + "dependencies": { + "aws-cdk-lib": "^2.3.4" + } +} diff --git a/tools/yarn-cling/test/test-fixture/jsii/node_modules/cdk b/tools/yarn-cling/test/test-fixture/jsii/node_modules/cdk new file mode 120000 index 0000000000000..7ce3fadc4aadf --- /dev/null +++ b/tools/yarn-cling/test/test-fixture/jsii/node_modules/cdk @@ -0,0 +1 @@ +../../cdk \ No newline at end of file diff --git a/tools/yarn-cling/test/test-fixture/jsii/package.json b/tools/yarn-cling/test/test-fixture/jsii/package.json new file mode 100644 index 0000000000000..8508d1f52fc9e --- /dev/null +++ b/tools/yarn-cling/test/test-fixture/jsii/package.json @@ -0,0 +1,8 @@ +{ + "name": "jsii", + "version": "1.1.1", + "dependencies": { + "aws-cdk": "^1.2.3", + "cdk": "2.2.2" + } +} diff --git a/tools/yarn-cling/test/test-fixture/package1/node_modules/package2 b/tools/yarn-cling/test/test-fixture/package1/node_modules/package2 deleted file mode 120000 index 8db12e196dfbd..0000000000000 --- a/tools/yarn-cling/test/test-fixture/package1/node_modules/package2 +++ /dev/null @@ -1 +0,0 @@ -../../package2 \ No newline at end of file diff --git a/tools/yarn-cling/test/test-fixture/package1/node_modules/registrydependency1/package.json b/tools/yarn-cling/test/test-fixture/package1/node_modules/registrydependency1/package.json deleted file mode 100644 index 8ecbc5b50e2e6..0000000000000 --- a/tools/yarn-cling/test/test-fixture/package1/node_modules/registrydependency1/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "registrydependency1", - "version": "1.2.999", - "dependencies": { - "registrydependency2": "^2.3.4" - } -} diff --git a/tools/yarn-cling/test/test-fixture/package1/node_modules/registrydependency2/package.json b/tools/yarn-cling/test/test-fixture/package1/node_modules/registrydependency2/package.json deleted file mode 100644 index 8b9d7f39ababd..0000000000000 --- a/tools/yarn-cling/test/test-fixture/package1/node_modules/registrydependency2/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "registrydependency2", - "version": "2.3.999" -} diff --git a/tools/yarn-cling/test/test-fixture/package1/package.json b/tools/yarn-cling/test/test-fixture/package1/package.json deleted file mode 100644 index 8edf6d46e6537..0000000000000 --- a/tools/yarn-cling/test/test-fixture/package1/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "package1", - "version": "1.1.1", - "dependencies": { - "registrydependency1": "^1.2.3", - "package2": "2.2.2" - } -} diff --git a/tools/yarn-cling/test/test-fixture/yarn.lock b/tools/yarn-cling/test/test-fixture/yarn.lock index fda733502a052..bb94543e94422 100644 --- a/tools/yarn-cling/test/test-fixture/yarn.lock +++ b/tools/yarn-cling/test/test-fixture/yarn.lock @@ -1,9 +1,9 @@ -"registrydependency1@^1.2.3": +"aws-cdk@^1.2.3": version "1.2.999" resolved "https://registry.bla.com/stuff" integrity sha512-banana -"registrydependency2@^2.3.4": +"aws-cdk-lib@^2.3.4": version "2.3.999" resolved "https://registry.bla.com/stuff" integrity sha512-pineapple From 66baca58adc294d5c5924cf8f8c5fa122c6d6dfc Mon Sep 17 00:00:00 2001 From: Unnati Parekh <80710604+upparekh@users.noreply.github.com> Date: Wed, 1 Sep 2021 13:50:54 -0700 Subject: [PATCH 55/91] feat(ecs-service-extensions): Subscribe Extension (#16049) ---- This PR adds a new service extension, `SubscribeExtension`. This extension can be added to a service to create SQS Queues which can subscribe to the SNS Topics provided by the user. It creates a default SQS Queue called `eventsQueue` . It also supports creation of topic-specific queues and sets up the SNS subscriptions accordingly. The created topic-queue subscriptions can be accessed using `subscriptions` field of the extension and the default queue for this service can be accessed using the `eventsQueue` getter method. (This PR does not include autoscaling, will be adding it in a separate PR) *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../ecs-service-extensions/README.md | 35 ++ .../lib/extensions/index.ts | 1 + .../lib/extensions/queue.ts | 212 ++++++++ .../ecs-service-extensions/package.json | 4 + .../ecs-service-extensions/test/test.queue.ts | 508 ++++++++++++++++++ 5 files changed, 760 insertions(+) create mode 100644 packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/queue.ts create mode 100644 packages/@aws-cdk-containers/ecs-service-extensions/test/test.queue.ts diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/README.md b/packages/@aws-cdk-containers/ecs-service-extensions/README.md index f3845e69a55e5..53e4d2b6f3c56 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/README.md +++ b/packages/@aws-cdk-containers/ecs-service-extensions/README.md @@ -19,6 +19,7 @@ The `Service` construct provided by this module can be extended with optional `S - [AWS AppMesh](https://aws.amazon.com/app-mesh/) for adding your application to a service mesh - [Application Load Balancer](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html), for exposing your service to the public - [AWS FireLens](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/using_firelens.html), for filtering and routing application logs +- Queue to allow your service to consume messages from an SQS Queue which is populated by one or more SNS Topics that it is subscribed to - [Community Extensions](#community-extensions), providing support for advanced use cases The `ServiceExtension` class is an abstract class which you can also implement in @@ -321,6 +322,40 @@ const environment = Environment.fromEnvironmentAttributes(stack, 'Environment', ``` +## Queue Extension + +This service extension creates a default SQS Queue `eventsQueue` for the service (if not provided) and accepts a list of `ISubscribable` objects that the `eventsQueue` can subscribe to. The service extension creates the subscriptions and sets up permissions for the service to consume messages from the SQS Queue. + +### Setting up SNS Topic Subscriptions for SQS Queues + +You can use this extension to set up SNS Topic subscriptions for the `eventsQueue`. To do this, create a new object of type `TopicSubscription` for every SNS Topic you want the `eventsQueue` to subscribe to and provide it as input to the service extension. + +```ts +const myServiceDescription = nameDescription.add(new QueueExtension({ + // Provide list of topic subscriptions that you want the `eventsQueue` to subscribe to + subscriptions: [new TopicSubscription({ + topic: new sns.Topic(stack, 'my-topic'), + }], +})); + +// To access the `eventsQueue` for the service, use the `eventsQueue` getter for the extension +const myQueueExtension = myServiceDescription.extensions.queue as QueueExtension; +const myEventsQueue = myQueueExtension.eventsQueue; +``` + +For setting up a topic-specific queue subscription, you can provide a custom queue in the `TopicSubscription` object along with the SNS Topic. The extension will set up a topic subscription for the provided queue instead of the default `eventsQueue` of the service. + +```ts +nameDescription.add(new QueueExtension({ + queue: myEventsQueue, + subscriptions: [new TopicSubscription({ + topic: new sns.Topic(stack, 'my-topic'), + // `myTopicQueue` will subscribe to the `my-topic` instead of `eventsQueue` + queue: myTopicQueue, + }], +})); +``` + ## Community Extensions We encourage the development of Community Service Extensions that support diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/index.ts b/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/index.ts index 4e464e0d0734e..78c138aba0102 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/index.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/index.ts @@ -6,3 +6,4 @@ export * from './cloudwatch-agent'; export * from './scale-on-cpu-utilization'; export * from './xray'; export * from './assign-public-ip'; +export * from './queue'; \ No newline at end of file diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/queue.ts b/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/queue.ts new file mode 100644 index 0000000000000..d43b587d6326e --- /dev/null +++ b/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/queue.ts @@ -0,0 +1,212 @@ +import * as ecs from '@aws-cdk/aws-ecs'; +import * as sns from '@aws-cdk/aws-sns'; +import * as subscription from '@aws-cdk/aws-sns-subscriptions'; +import * as sqs from '@aws-cdk/aws-sqs'; +import * as cdk from '@aws-cdk/core'; +import { Service } from '../service'; +import { Container } from './container'; +import { ContainerMutatingHook, ServiceExtension } from './extension-interfaces'; + +// Keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + +/** + * An interface that will be implemented by all the resources that can be subscribed to. + */ +export interface ISubscribable { + /** + * All classes implementing this interface must also implement the `subscribe()` method + */ + subscribe(extension: QueueExtension): sqs.IQueue; +} + +/** + * The settings for the Queue extension. + */ +export interface QueueExtensionProps { + /** + * The list of subscriptions for this service. + * + * @default none + */ + readonly subscriptions?: ISubscribable[]; + + /** + * The user-provided default queue for this service. + * + * @default If the `eventsQueue` is not provided, a default SQS Queue is created for the service. + */ + readonly eventsQueue?: sqs.IQueue; +} + +/** + * The topic-specific settings for creating the queue subscriptions. + */ +export interface TopicSubscriptionProps { + /** + * The SNS Topic to subscribe to. + */ + readonly topic: sns.ITopic; + + /** + * The user-provided queue to subscribe to the given topic. + * If the `queue` is not provided, the default `eventsQueue` will subscribe to the given topic. + * + * @default none + */ + readonly queue?: sqs.IQueue; +} + +/** + * The `TopicSubscription` class represents an SNS Topic resource that can be subscribed to by the service queues. + */ +export class TopicSubscription implements ISubscribable { + public readonly topic: sns.ITopic; + + public readonly queue?: sqs.IQueue; + + constructor(props: TopicSubscriptionProps) { + this.topic = props.topic; + this.queue = props.queue; + } + + /** + * This method sets up SNS Topic subscriptions for the SQS queue provided by the user. If a `queue` is not provided, + * the default `eventsQueue` subscribes to the given topic. + * + * @param extension `QueueExtension` added to the service + * @returns the queue subscribed to the given topic + */ + public subscribe(extension: QueueExtension) : sqs.IQueue { + let queue = extension.eventsQueue; + if (this.queue) { + queue = this.queue; + } + this.topic.addSubscription(new subscription.SqsSubscription(queue)); + return queue; + } +} + +/** + * Settings for the hook which mutates the application container + * to add the events queue URI to its environment. + */ +interface ContainerMutatingProps { + /** + * The events queue name and URI to be added to the container environment. + */ + readonly environment: { [key: string]: string }; +} + +/** + * This hook modifies the application container's environment to + * add the queue URL for the events queue of the service. + */ +class QueueExtensionMutatingHook extends ContainerMutatingHook { + private environment: { [key: string]: string }; + + constructor(props: ContainerMutatingProps) { + super(); + this.environment = props.environment; + } + + public mutateContainerDefinition(props: ecs.ContainerDefinitionOptions): ecs.ContainerDefinitionOptions { + return { + ...props, + + environment: { ...(props.environment || {}), ...this.environment }, + } as ecs.ContainerDefinitionOptions; + } +} + +/** + * This extension creates a default `eventsQueue` for the service (if not provided) and accepts a list of objects of + * type `ISubscribable` that the `eventsQueue` subscribes to. It creates the subscriptions and sets up permissions + * for the service to consume messages from the SQS Queues. + * + * The default queue for this service can be accessed using the getter `.eventsQueue`. + */ +export class QueueExtension extends ServiceExtension { + private _eventsQueue!: sqs.IQueue; + + private subscriptionQueues = new Set(); + + private environment: { [key: string]: string } = {}; + + private props?: QueueExtensionProps; + + constructor(props?: QueueExtensionProps) { + super('queue'); + + this.props = props; + } + + /** + * This hook creates (if required) and sets the default queue `eventsQueue`. It also sets up the subscriptions for + * the provided `ISubscribable` objects. + * + * @param service The parent service which this extension has been added to + * @param scope The scope that this extension should create resources in + */ + public prehook(service: Service, scope: Construct) { + this.parentService = service; + this.scope = scope; + + let eventsQueue = this.props?.eventsQueue; + if (!eventsQueue) { + const deadLetterQueue = new sqs.Queue(this.scope, 'EventsDeadLetterQueue', { + retentionPeriod: cdk.Duration.days(14), + }); + + eventsQueue = new sqs.Queue(this.scope, 'EventsQueue', { + deadLetterQueue: { + queue: deadLetterQueue, + maxReceiveCount: 3, + }, + }); + } + this._eventsQueue = eventsQueue; + + this.environment[`${this.parentService.id.toUpperCase()}_QUEUE_URI`] = this._eventsQueue.queueUrl; + + if (this.props?.subscriptions) { + for (const subs of this.props.subscriptions) { + const subsQueue = subs.subscribe(this); + this.subscriptionQueues.add(subsQueue); + } + } + } + + /** + * Add hooks to the main application extension so that it is modified to + * add the events queue URL to the container environment. + */ + public addHooks() { + const container = this.parentService.serviceDescription.get('service-container') as Container; + + if (!container) { + throw new Error('Queue Extension requires an application extension'); + } + + container.addContainerMutatingHook(new QueueExtensionMutatingHook({ + environment: this.environment, + })); + } + + /** + * After the task definition has been created, this hook grants SQS permissions to the task role. + * + * @param taskDefinition The created task definition + */ + public useTaskDefinition(taskDefinition: ecs.TaskDefinition) { + this._eventsQueue.grantConsumeMessages(taskDefinition.taskRole); + for (const queue of this.subscriptionQueues) { + queue.grantConsumeMessages(taskDefinition.taskRole); + } + } + + public get eventsQueue() : sqs.IQueue { + return this._eventsQueue; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/package.json b/packages/@aws-cdk-containers/ecs-service-extensions/package.json index 0d99dadbedb02..444e43ba9a19a 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/package.json +++ b/packages/@aws-cdk-containers/ecs-service-extensions/package.json @@ -64,6 +64,8 @@ "@aws-cdk/aws-route53": "0.0.0", "@aws-cdk/aws-route53-targets": "0.0.0", "@aws-cdk/aws-servicediscovery": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", + "@aws-cdk/aws-sns-subscriptions": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/custom-resources": "0.0.0", @@ -89,6 +91,8 @@ "@aws-cdk/aws-route53": "0.0.0", "@aws-cdk/aws-route53-targets": "0.0.0", "@aws-cdk/aws-servicediscovery": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", + "@aws-cdk/aws-sns-subscriptions": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/custom-resources": "0.0.0", diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/test.queue.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/test.queue.ts new file mode 100644 index 0000000000000..ec2fb234c16ce --- /dev/null +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/test.queue.ts @@ -0,0 +1,508 @@ +import { countResources, expect, haveResource } from '@aws-cdk/assert-internal'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as sns from '@aws-cdk/aws-sns'; +import * as sqs from '@aws-cdk/aws-sqs'; +import * as cdk from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import { Container, Environment, QueueExtension, Service, ServiceDescription, TopicSubscription } from '../lib'; + +export = { + 'should only create a default queue when no input props are provided'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const environment = new Environment(stack, 'production'); + const serviceDescription = new ServiceDescription(); + + serviceDescription.add(new Container({ + cpu: 256, + memoryMiB: 512, + trafficPort: 80, + image: ecs.ContainerImage.fromRegistry('nathanpeck/name'), + environment: { + PORT: '80', + }, + })); + + // WHEN + serviceDescription.add(new QueueExtension()); + + new Service(stack, 'my-service', { + environment, + serviceDescription, + }); + + // THEN + // Ensure creation of default queue and queue policy allowing SNS Topics to send message to the queue + expect(stack).to(haveResource('AWS::SQS::Queue', { + MessageRetentionPeriod: 1209600, + })); + + expect(stack).to(haveResource('AWS::SQS::Queue', { + RedrivePolicy: { + deadLetterTargetArn: { + 'Fn::GetAtt': [ + 'EventsDeadLetterQueue404572C7', + 'Arn', + ], + }, + maxReceiveCount: 3, + }, + })); + + // Ensure the task role is given permissions to consume messages from the queue + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'sqs:ReceiveMessage', + 'sqs:ChangeMessageVisibility', + 'sqs:GetQueueUrl', + 'sqs:DeleteMessage', + 'sqs:GetQueueAttributes', + ], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'EventsQueueB96EB0D2', + 'Arn', + ], + }, + }, + ], + Version: '2012-10-17', + }, + })); + + // Ensure there are no SNS Subscriptions created + expect(stack).to(countResources('AWS::SNS::Subscription', 0)); + + // Ensure that the queue URL has been correctly appended to the environment variables + expect(stack).to(haveResource('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Cpu: 256, + Environment: [ + { + Name: 'PORT', + Value: '80', + }, + { + Name: 'MY-SERVICE_QUEUE_URI', + Value: { + Ref: 'EventsQueueB96EB0D2', + }, + }, + ], + Image: 'nathanpeck/name', + Essential: true, + Memory: 512, + Name: 'app', + PortMappings: [ + { + ContainerPort: 80, + Protocol: 'tcp', + }, + ], + Ulimits: [ + { + HardLimit: 1024000, + Name: 'nofile', + SoftLimit: 1024000, + }, + ], + }, + ], + })); + + test.done(); + }, + + 'should be able to subscribe default events queue created by the extension to given topics'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const environment = new Environment(stack, 'production'); + const serviceDescription = new ServiceDescription(); + + serviceDescription.add(new Container({ + cpu: 256, + memoryMiB: 512, + trafficPort: 80, + image: ecs.ContainerImage.fromRegistry('nathanpeck/name'), + environment: { + PORT: '80', + }, + })); + + // WHEN + const topicSubscription1 = new TopicSubscription({ + topic: new sns.Topic(stack, 'topic1'), + }); + const topicSubscription2 = new TopicSubscription({ + topic: new sns.Topic(stack, 'topic2'), + }); + serviceDescription.add(new QueueExtension({ + subscriptions: [topicSubscription1, topicSubscription2], + })); + + new Service(stack, 'my-service', { + environment, + serviceDescription, + }); + + // THEN + // Ensure creation of default queue and queue policy allowing SNS Topics to send message to the queue + expect(stack).to(haveResource('AWS::SQS::Queue', { + MessageRetentionPeriod: 1209600, + })); + + expect(stack).to(haveResource('AWS::SQS::Queue', { + RedrivePolicy: { + deadLetterTargetArn: { + 'Fn::GetAtt': [ + 'EventsDeadLetterQueue404572C7', + 'Arn', + ], + }, + maxReceiveCount: 3, + }, + })); + + expect(stack).to(haveResource('AWS::SQS::QueuePolicy', { + PolicyDocument: { + Statement: [ + { + Action: 'sqs:SendMessage', + Condition: { + ArnEquals: { + 'aws:SourceArn': { + Ref: 'topic152D84A37', + }, + }, + }, + Effect: 'Allow', + Principal: { + Service: 'sns.amazonaws.com', + }, + Resource: { + 'Fn::GetAtt': [ + 'EventsQueueB96EB0D2', + 'Arn', + ], + }, + }, + { + Action: 'sqs:SendMessage', + Condition: { + ArnEquals: { + 'aws:SourceArn': { + Ref: 'topic2A4FB547F', + }, + }, + }, + Effect: 'Allow', + Principal: { + Service: 'sns.amazonaws.com', + }, + Resource: { + 'Fn::GetAtt': [ + 'EventsQueueB96EB0D2', + 'Arn', + ], + }, + }, + ], + Version: '2012-10-17', + }, + })); + + // Ensure the task role is given permissions to consume messages from the queue + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'sqs:ReceiveMessage', + 'sqs:ChangeMessageVisibility', + 'sqs:GetQueueUrl', + 'sqs:DeleteMessage', + 'sqs:GetQueueAttributes', + ], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'EventsQueueB96EB0D2', + 'Arn', + ], + }, + }, + ], + Version: '2012-10-17', + }, + })); + + // Ensure SNS Subscriptions for given topics + expect(stack).to(haveResource('AWS::SNS::Subscription', { + Protocol: 'sqs', + TopicArn: { + Ref: 'topic152D84A37', + }, + Endpoint: { + 'Fn::GetAtt': [ + 'EventsQueueB96EB0D2', + 'Arn', + ], + }, + })); + + expect(stack).to(haveResource('AWS::SNS::Subscription', { + Protocol: 'sqs', + TopicArn: { + Ref: 'topic2A4FB547F', + }, + Endpoint: { + 'Fn::GetAtt': [ + 'EventsQueueB96EB0D2', + 'Arn', + ], + }, + })); + + // Ensure that the queue URL has been correctly appended to the environment variables + expect(stack).to(haveResource('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Cpu: 256, + Environment: [ + { + Name: 'PORT', + Value: '80', + }, + { + Name: 'MY-SERVICE_QUEUE_URI', + Value: { + Ref: 'EventsQueueB96EB0D2', + }, + }, + ], + Image: 'nathanpeck/name', + Essential: true, + Memory: 512, + Name: 'app', + PortMappings: [ + { + ContainerPort: 80, + Protocol: 'tcp', + }, + ], + Ulimits: [ + { + HardLimit: 1024000, + Name: 'nofile', + SoftLimit: 1024000, + }, + ], + }, + ], + })); + + test.done(); + }, + + 'should be able to subscribe user-provided queue to given topics'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const environment = new Environment(stack, 'production'); + const serviceDescription = new ServiceDescription(); + + serviceDescription.add(new Container({ + cpu: 256, + memoryMiB: 512, + trafficPort: 80, + image: ecs.ContainerImage.fromRegistry('nathanpeck/name'), + })); + + const topicSubscription1 = new TopicSubscription({ + topic: new sns.Topic(stack, 'topic1'), + queue: new sqs.Queue(stack, 'myQueue'), + }); + const topicSubscription2 = new TopicSubscription({ + topic: new sns.Topic(stack, 'topic2'), + }); + serviceDescription.add(new QueueExtension({ + subscriptions: [topicSubscription1, topicSubscription2], + eventsQueue: new sqs.Queue(stack, 'defQueue'), + })); + + new Service(stack, 'my-service', { + environment, + serviceDescription, + }); + + // THEN + // Ensure queue policy allows SNS Topics to send message to the queue + expect(stack).to(haveResource('AWS::SQS::QueuePolicy', { + PolicyDocument: { + Statement: [ + { + Action: 'sqs:SendMessage', + Condition: { + ArnEquals: { + 'aws:SourceArn': { + Ref: 'topic152D84A37', + }, + }, + }, + Effect: 'Allow', + Principal: { + Service: 'sns.amazonaws.com', + }, + Resource: { + 'Fn::GetAtt': [ + 'myQueue4FDFF71C', + 'Arn', + ], + }, + }, + ], + Version: '2012-10-17', + }, + })); + + expect(stack).to(haveResource('AWS::SQS::QueuePolicy', { + PolicyDocument: { + Statement: [ + { + Action: 'sqs:SendMessage', + Condition: { + ArnEquals: { + 'aws:SourceArn': { + Ref: 'topic2A4FB547F', + }, + }, + }, + Effect: 'Allow', + Principal: { + Service: 'sns.amazonaws.com', + }, + Resource: { + 'Fn::GetAtt': [ + 'defQueue1F91A65B', + 'Arn', + ], + }, + }, + ], + Version: '2012-10-17', + }, + })); + + // Ensure the task role is given permissions to consume messages from the queue + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'sqs:ReceiveMessage', + 'sqs:ChangeMessageVisibility', + 'sqs:GetQueueUrl', + 'sqs:DeleteMessage', + 'sqs:GetQueueAttributes', + ], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'defQueue1F91A65B', + 'Arn', + ], + }, + }, + { + Action: [ + 'sqs:ReceiveMessage', + 'sqs:ChangeMessageVisibility', + 'sqs:GetQueueUrl', + 'sqs:DeleteMessage', + 'sqs:GetQueueAttributes', + ], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'myQueue4FDFF71C', + 'Arn', + ], + }, + }, + ], + Version: '2012-10-17', + }, + })); + + // Ensure SNS Subscriptions for given topics + expect(stack).to(haveResource('AWS::SNS::Subscription', { + Protocol: 'sqs', + TopicArn: { + Ref: 'topic152D84A37', + }, + Endpoint: { + 'Fn::GetAtt': [ + 'myQueue4FDFF71C', + 'Arn', + ], + }, + })); + + expect(stack).to(haveResource('AWS::SNS::Subscription', { + Protocol: 'sqs', + TopicArn: { + Ref: 'topic2A4FB547F', + }, + Endpoint: { + 'Fn::GetAtt': [ + 'defQueue1F91A65B', + 'Arn', + ], + }, + })); + + // Ensure that the queue URL has been correctly added to the environment variables + expect(stack).to(haveResource('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Cpu: 256, + Environment: [ + { + Name: 'MY-SERVICE_QUEUE_URI', + Value: { + Ref: 'defQueue1F91A65B', + }, + }, + ], + Image: 'nathanpeck/name', + Essential: true, + Memory: 512, + Name: 'app', + PortMappings: [ + { + ContainerPort: 80, + Protocol: 'tcp', + }, + ], + Ulimits: [ + { + HardLimit: 1024000, + Name: 'nofile', + SoftLimit: 1024000, + }, + ], + }, + ], + })); + + test.done(); + }, +}; \ No newline at end of file From 349361d0399c05e21c5c4c5a2b69edfc6774c94f Mon Sep 17 00:00:00 2001 From: Douglas Naphas Date: Wed, 1 Sep 2021 18:10:46 -0400 Subject: [PATCH 56/91] docs(aws-ecr-assets): Correct un-closed parenthetical expression (#16172) Closes #16171. The expression is missing a right parentheses, and it is just as clear anyway as a new sentence. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-ecr-assets/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecr-assets/README.md b/packages/@aws-cdk/aws-ecr-assets/README.md index 2414dc57c084e..f5993097418ac 100644 --- a/packages/@aws-cdk/aws-ecr-assets/README.md +++ b/packages/@aws-cdk/aws-ecr-assets/README.md @@ -46,7 +46,7 @@ interpreted. The recommended setting for Docker image assets is old projects) then `IgnoreMode.DOCKER` is the default and you don't need to configure it on the asset itself. -Use `asset.imageUri` to reference the image (it includes both the ECR image URL +Use `asset.imageUri` to reference the image. It includes both the ECR image URL and tag. You can optionally pass build args to the `docker build` command by specifying From 2c1eb08ad174ba56c4f2a0d807a0cb039534d1a9 Mon Sep 17 00:00:00 2001 From: Markus Lindqvist Date: Thu, 2 Sep 2021 01:52:19 +0300 Subject: [PATCH 57/91] docs(pipelines): Fix documentation regarding rolePolicyStatements of CodeBuildStep (#16294) fix(pipelines): Fix documentation regarding rolePolicyStatements of CodeBuildStep ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/pipelines/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index bab41842c2316..a63f1eab4d7c4 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -575,7 +575,7 @@ new CodeBuildStep('Synth', { securityGroups: [mySecurityGroup], // Additional policy statements for the execution role - rolePolicy: [ + rolePolicyStatements: [ new iam.PolicyStatement({ /* ... */ }), ], }); From dbfebb47a8ae61b2bb0557b6ba79a7b073f9d0df Mon Sep 17 00:00:00 2001 From: maafk Date: Thu, 2 Sep 2021 05:14:11 -0400 Subject: [PATCH 58/91] fix(core): allow asset bundling when selinux is enabled (#15742) ---- Revisiting [#9445](https://github.com/aws/aws-cdk/pull/9445) but looking to see if running on linux with selinux enabled. This PR aims to allow for asset bundling on linux os with selinux enabled *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/core/lib/bundling.ts | 27 +++- packages/@aws-cdk/core/test/bundling.test.ts | 134 ++++++++++++++++++- 2 files changed, 158 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/core/lib/bundling.ts b/packages/@aws-cdk/core/lib/bundling.ts index c6b6b66881771..1ce4633354511 100644 --- a/packages/@aws-cdk/core/lib/bundling.ts +++ b/packages/@aws-cdk/core/lib/bundling.ts @@ -200,7 +200,7 @@ export class BundlingDockerImage { ...options.user ? ['-u', options.user] : [], - ...flatten(volumes.map(v => ['-v', `${v.hostPath}:${v.containerPath}:${v.consistency ?? DockerVolumeConsistency.DELEGATED}`])), + ...flatten(volumes.map(v => ['-v', `${v.hostPath}:${v.containerPath}:${isSeLinux() ? 'z,' : ''}${v.consistency ?? DockerVolumeConsistency.DELEGATED}`])), ...flatten(Object.entries(environment).map(([k, v]) => ['--env', `${k}=${v}`])), ...options.workingDirectory ? ['-w', options.workingDirectory] @@ -481,3 +481,28 @@ function dockerExec(args: string[], options?: SpawnSyncOptions) { return proc; } + +function isSeLinux() : boolean { + if (process.platform != 'linux') { + return false; + } + const prog = 'selinuxenabled'; + const proc = spawnSync(prog, [], { + stdio: [ // show selinux status output + 'pipe', // get value of stdio + process.stderr, // redirect stdout to stderr + 'inherit', // inherit stderr + ], + }); + if (proc.error) { + // selinuxenabled not a valid command, therefore not enabled + return false; + } + if (proc.status == 0) { + // selinux enabled + return true; + } else { + // selinux not enabled + return false; + } +} diff --git a/packages/@aws-cdk/core/test/bundling.test.ts b/packages/@aws-cdk/core/test/bundling.test.ts index 174bc15c7b115..48522347e698c 100644 --- a/packages/@aws-cdk/core/test/bundling.test.ts +++ b/packages/@aws-cdk/core/test/bundling.test.ts @@ -12,6 +12,7 @@ nodeunitShim({ }, 'bundling with image from registry'(test: Test) { + sinon.stub(process, 'platform').value('darwin'); const spawnSyncStub = sinon.stub(child_process, 'spawnSync').returns({ status: 0, stderr: Buffer.from('stderr'), @@ -230,6 +231,7 @@ nodeunitShim({ }, 'custom entrypoint is passed through to docker exec'(test: Test) { + sinon.stub(process, 'platform').value('darwin'); const spawnSyncStub = sinon.stub(child_process, 'spawnSync').returns({ status: 0, stderr: Buffer.from('stderr'), @@ -343,7 +345,9 @@ nodeunitShim({ test.done(); }, - 'adding user provided securit-opt'(test: Test) { + 'adding user provided security-opt'(test: Test) { + // GIVEN + sinon.stub(process, 'platform').value('darwin'); const spawnSyncStub = sinon.stub(child_process, 'spawnSync').returns({ status: 0, stderr: Buffer.from('stderr'), @@ -352,8 +356,9 @@ nodeunitShim({ output: ['stdout', 'stderr'], signal: null, }); - const image = DockerImage.fromRegistry('alpine'); + + // GIVEN image.run({ command: ['cool', 'command'], environment: { @@ -379,4 +384,129 @@ nodeunitShim({ ], { stdio: ['ignore', process.stderr, 'inherit'] })); test.done(); }, + + 'ensure selinux docker mount'(test: Test) { + // GIVEN + sinon.stub(process, 'platform').value('linux'); + const spawnSyncStub = sinon.stub(child_process, 'spawnSync'); + spawnSyncStub.onFirstCall().returns({ + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from('stdout'), + pid: 123, + output: ['selinuxenable-command', 'stderr'], + signal: null, + }); + spawnSyncStub.onSecondCall().returns({ + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from('stdout'), + pid: 124, + output: ['docker run command', 'stderr'], + signal: null, + }); + + // WHEN + const image = DockerImage.fromRegistry('alpine'); + image.run({ + command: ['cool', 'command'], + volumes: [{ hostPath: '/host-path', containerPath: '/container-path' }], + workingDirectory: '/working-directory', + user: 'user:group', + }); + + // THEN + test.ok(spawnSyncStub.secondCall.calledWith('docker', [ + 'run', '--rm', + '-u', 'user:group', + '-v', '/host-path:/container-path:z,delegated', + '-w', '/working-directory', + 'alpine', + 'cool', 'command', + ], { stdio: ['ignore', process.stderr, 'inherit'] })); + test.done(); + }, + + 'ensure selinux docker mount on linux with selinux disabled'(test: Test) { + // GIVEN + sinon.stub(process, 'platform').value('linux'); + const spawnSyncStub = sinon.stub(child_process, 'spawnSync'); + spawnSyncStub.onFirstCall().returns({ + status: 1, + stderr: Buffer.from('stderr'), + stdout: Buffer.from('stdout'), + pid: 123, + output: ['selinuxenabled output', 'stderr'], + signal: null, + }); + spawnSyncStub.onSecondCall().returns({ + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from('stdout'), + pid: 124, + output: ['docker run command', 'stderr'], + signal: null, + }); + + // WHEN + const image = DockerImage.fromRegistry('alpine'); + image.run({ + command: ['cool', 'command'], + volumes: [{ hostPath: '/host-path', containerPath: '/container-path' }], + workingDirectory: '/working-directory', + user: 'user:group', + }); + + // THEN + test.ok(spawnSyncStub.secondCall.calledWith('docker', [ + 'run', '--rm', + '-u', 'user:group', + '-v', '/host-path:/container-path:delegated', + '-w', '/working-directory', + 'alpine', + 'cool', 'command', + ], { stdio: ['ignore', process.stderr, 'inherit'] })); + test.done(); + }, + 'ensure no selinux docker mount if selinuxenabled isn\'t an available command'(test: Test) { + // GIVEN + sinon.stub(process, 'platform').value('linux'); + const spawnSyncStub = sinon.stub(child_process, 'spawnSync'); + spawnSyncStub.onFirstCall().returns({ + status: 127, + stderr: Buffer.from('stderr'), + stdout: Buffer.from('stdout'), + pid: 123, + output: ['selinuxenabled output', 'stderr'], + signal: null, + }); + spawnSyncStub.onSecondCall().returns({ + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from('stdout'), + pid: 124, + output: ['docker run command', 'stderr'], + signal: null, + }); + + // WHEN + const image = DockerImage.fromRegistry('alpine'); + image.run({ + command: ['cool', 'command'], + volumes: [{ hostPath: '/host-path', containerPath: '/container-path' }], + workingDirectory: '/working-directory', + user: 'user:group', + }); + + // THEN + test.ok(spawnSyncStub.secondCall.calledWith('docker', [ + 'run', '--rm', + '-u', 'user:group', + '-v', '/host-path:/container-path:delegated', + '-w', '/working-directory', + 'alpine', + 'cool', 'command', + ], { stdio: ['ignore', process.stderr, 'inherit'] })); + test.done(); + }, }); From 6e55c952d683f87bb815deb29124b9a37824749a Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Thu, 2 Sep 2021 15:32:47 -0700 Subject: [PATCH 59/91] feat(cli): hotswap deployments (#15748) This is the first PR implementing the ["Accelerated personal deployments" RFC](https://github.com/aws/aws-cdk-rfcs/blob/master/text/0001-cdk-update.md). It adds a (boolean) `--hotswap` flag to the `deploy` command that attempts to perform a short-circuit deployment, updating the resource directly, and skipping CloudFormation. If we detect that the current change cannot be short-circuited (because it contains an infrastructure change to the CDK code, most likely), we fall back on performing a full CloudFormation deployment, same as if `cdk deploy` was called without the `--hotswap` flag. In this PR, the new switch supports only Lambda functions. Later PRs will add support for new resource types. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../cloudformation-diff/lib/diff/types.ts | 6 +- packages/aws-cdk/README.md | 36 ++++ packages/aws-cdk/bin/cdk.ts | 12 +- packages/aws-cdk/lib/api/aws-auth/sdk.ts | 5 + .../lib/api/cloudformation-deployments.ts | 10 ++ packages/aws-cdk/lib/api/deploy-stack.ts | 46 ++++- .../aws-cdk/lib/api/hotswap-deployments.ts | 100 +++++++++++ packages/aws-cdk/lib/api/hotswap/common.ts | 57 +++++++ .../lib/api/hotswap/lambda-functions.ts | 159 ++++++++++++++++++ .../api/util/cloudformation/evaluate-cfn.ts | 89 ++++++++++ packages/aws-cdk/lib/cdk-toolkit.ts | 20 ++- .../api/cloudformation-deployments.test.ts | 15 ++ .../aws-cdk/test/api/deploy-stack.test.ts | 62 ++++++- .../test/api/fake-cloudformation-stack.ts | 34 ++++ .../test/api/hotswap-deployments.test.ts | 113 +++++++++++++ packages/aws-cdk/test/cdk-toolkit.test.ts | 34 +++- packages/aws-cdk/test/util.ts | 2 +- packages/aws-cdk/test/util/mock-sdk.ts | 5 + 18 files changed, 787 insertions(+), 18 deletions(-) create mode 100644 packages/aws-cdk/lib/api/hotswap-deployments.ts create mode 100644 packages/aws-cdk/lib/api/hotswap/common.ts create mode 100644 packages/aws-cdk/lib/api/hotswap/lambda-functions.ts create mode 100644 packages/aws-cdk/lib/api/util/cloudformation/evaluate-cfn.ts create mode 100644 packages/aws-cdk/test/api/fake-cloudformation-stack.ts create mode 100644 packages/aws-cdk/test/api/hotswap-deployments.test.ts diff --git a/packages/@aws-cdk/cloudformation-diff/lib/diff/types.ts b/packages/@aws-cdk/cloudformation-diff/lib/diff/types.ts index c8911c5da19fe..335783388275f 100644 --- a/packages/@aws-cdk/cloudformation-diff/lib/diff/types.ts +++ b/packages/@aws-cdk/cloudformation-diff/lib/diff/types.ts @@ -282,7 +282,7 @@ export class Difference implements IDifference { * @param oldValue the old value, cannot be equal (to the sense of +deepEqual+) to +newValue+. * @param newValue the new value, cannot be equal (to the sense of +deepEqual+) to +oldValue+. */ - constructor(public readonly oldValue: ValueType | undefined, public readonly newValue: ValueType | undefined) { + constructor(public readonly oldValue: ValueType | undefined, public readonly newValue: ValueType | undefined) { if (oldValue === undefined && newValue === undefined) { throw new AssertionError({ message: 'oldValue and newValue are both undefined!' }); } @@ -309,7 +309,7 @@ export class Difference implements IDifference { export class PropertyDifference extends Difference { public readonly changeImpact?: ResourceImpact; - constructor(oldValue: ValueType | undefined, newValue: ValueType | undefined, args: { changeImpact?: ResourceImpact }) { + constructor(oldValue: ValueType | undefined, newValue: ValueType | undefined, args: { changeImpact?: ResourceImpact }) { super(oldValue, newValue); this.changeImpact = args.changeImpact; } @@ -506,7 +506,7 @@ export class ResourceDifference implements IDifference { constructor( public readonly oldValue: Resource | undefined, - public readonly newValue: Resource | undefined, + public readonly newValue: Resource | undefined, args: { resourceType: { oldType?: string, newType?: string }, propertyDiffs: { [key: string]: PropertyDifference }, diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index 729413ae6859d..3a1b2882b2506 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -334,6 +334,42 @@ execute. $ cdk deploy --no-execute --change-set-name MyChangeSetName ``` +#### Hotswap deployments for faster development + +You can pass the `--hotswap` flag to the `deploy` command: + +```console +$ cdk deploy --hotswap [StackNames] +``` + +This will attempt to perform a faster, short-circuit deployment if possible +(for example, if you only changed the code of a Lambda function in your CDK app, +but nothing else in your CDK code), +skipping CloudFormation, and updating the affected resources directly. +If the tool detects that the change does not support hotswapping, +it will fall back and perform a full CloudFormation deployment, +exactly like `cdk deploy` does without the `--hotswap` flag. + +Passing this option to `cdk deploy` will make it use your current AWS credentials to perform the API calls - +it will not assume the Roles from your bootstrap stack, +even if the `@aws-cdk/core:newStyleStackSynthesis` feature flag is set to `true` +(as those Roles do not have the necessary permissions to update AWS resources directly, without using CloudFormation). +For that reason, make sure that your credentials are for the same AWS account that the Stack(s) +you are performing the hotswap deployment for belong to, +and that you have the necessary IAM permissions to update the resources that are being deployed. + +Hotswapping is currently supported for the following changes +(additional changes will be supported in the future): + +- Code asset changes of AWS Lambda functions. + +**⚠ Note #1**: This command deliberately introduces drift in CloudFormation stacks in order to speed up deployments. +For this reason, only use it for development purposes. +**Never use this flag for your production deployments**! + +**⚠ Note #2**: This command is considered experimental, +and might have breaking changes in the future. + ### `cdk destroy` Deletes a stack from it's environment. This will cause the resources in the stack to be destroyed (unless they were diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index a21a350415223..aaaef0deb3182 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -105,10 +105,17 @@ async function parseCommandLineArguments() { .option('outputs-file', { type: 'string', alias: 'O', desc: 'Path to file where stack outputs will be written as JSON', requiresArg: true }) .option('previous-parameters', { type: 'boolean', default: true, desc: 'Use previous values for existing parameters (you must specify all parameters on every deployment if this is disabled)' }) .option('progress', { type: 'string', choices: [StackActivityProgress.BAR, StackActivityProgress.EVENTS], desc: 'Display mode for stack activity events' }) - .option('rollback', { type: 'boolean', default: true, desc: 'Rollback stack to stable state on failure (iterate more rapidly with --no-rollback or -R)' }) + .option('rollback', { type: 'boolean', desc: 'Rollback stack to stable state on failure (iterate more rapidly with --no-rollback or -R)' }) // Hack to get '-R' as an alias for '--no-rollback', suggested by: https://github.com/yargs/yargs/issues/1729 .option('R', { type: 'boolean', hidden: true }) - .middleware(yargsNegativeAlias('R', 'rollback'), true), + .middleware(yargsNegativeAlias('R', 'rollback'), true) + .option('hotswap', { + type: 'boolean', + desc: "Attempts to perform a 'hotswap' deployment, " + + 'which skips CloudFormation and updates the resources directly, ' + + 'and falls back to a full deployment if that is not possible. ' + + 'Do not use this in production environments', + }), ) .command('destroy [STACKS..]', 'Destroy the stack(s) named STACKS', yargs => yargs .option('all', { type: 'boolean', default: false, desc: 'Destroy all available stacks' }) @@ -324,6 +331,7 @@ async function initCommandLine() { progress: configuration.settings.get(['progress']), ci: args.ci, rollback: configuration.settings.get(['rollback']), + hotswap: args.hotswap, }); case 'destroy': diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk.ts b/packages/aws-cdk/lib/api/aws-auth/sdk.ts index ee21d8c17111b..3e6a935acdb9a 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk.ts @@ -22,6 +22,7 @@ export interface ISDK { */ currentAccount(): Promise; + lambda(): AWS.Lambda; cloudFormation(): AWS.CloudFormation; ec2(): AWS.EC2; ssm(): AWS.SSM; @@ -83,6 +84,10 @@ export class SDK implements ISDK { this.currentRegion = region; } + public lambda(): AWS.Lambda { + return this.wrapServiceErrorHandling(new AWS.Lambda(this.config)); + } + public cloudFormation(): AWS.CloudFormation { return this.wrapServiceErrorHandling(new AWS.CloudFormation({ ...this.config, diff --git a/packages/aws-cdk/lib/api/cloudformation-deployments.ts b/packages/aws-cdk/lib/api/cloudformation-deployments.ts index 8cea2ae411212..914efd51cd574 100644 --- a/packages/aws-cdk/lib/api/cloudformation-deployments.ts +++ b/packages/aws-cdk/lib/api/cloudformation-deployments.ts @@ -136,6 +136,15 @@ export interface DeployStackOptions { * @default true */ readonly rollback?: boolean; + + /* + * Whether to perform a 'hotswap' deployment. + * A 'hotswap' deployment will attempt to short-circuit CloudFormation + * and update the affected resources like Lambda functions directly. + * + * @default - false (do not perform a 'hotswap' deployment) + */ + readonly hotswap?: boolean; } export interface DestroyStackOptions { @@ -212,6 +221,7 @@ export class CloudFormationDeployments { progress: options.progress, ci: options.ci, rollback: options.rollback, + hotswap: options.hotswap, }); } diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index 62c5815197fc2..b0770ebdf8ea7 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -9,8 +9,12 @@ import { AssetManifestBuilder } from '../util/asset-manifest-builder'; import { publishAssets } from '../util/asset-publishing'; import { contentHash } from '../util/content-hash'; import { ISDK, SdkProvider } from './aws-auth'; +import { tryHotswapDeployment } from './hotswap-deployments'; import { ToolkitInfo } from './toolkit-info'; -import { changeSetHasNoChanges, CloudFormationStack, TemplateParameters, waitForChangeSet, waitForStackDeploy, waitForStackDelete } from './util/cloudformation'; +import { + changeSetHasNoChanges, CloudFormationStack, TemplateParameters, waitForChangeSet, + waitForStackDeploy, waitForStackDelete, ParameterValues, +} from './util/cloudformation'; import { StackActivityMonitor, StackActivityProgress } from './util/cloudformation/stack-activity-monitor'; // We need to map regions to domain suffixes, and the SDK already has a function to do this. @@ -182,6 +186,15 @@ export interface DeployStackOptions { * @default true */ readonly rollback?: boolean; + + /* + * Whether to perform a 'hotswap' deployment. + * A 'hotswap' deployment will attempt to short-circuit CloudFormation + * and update the affected resources like Lambda functions directly. + * + * @default - false (do not perform a 'hotswap' deployment) + */ + readonly hotswap?: boolean; } const LARGE_TEMPLATE_SIZE_KB = 50; @@ -233,12 +246,32 @@ export async function deployStack(options: DeployStackOptions): Promise { + const cfn = options.sdk.cloudFormation(); + const deployName = options.deployName ?? stackArtifact.stackName; + + const changeSetName = options.changeSetName ?? 'cdk-deploy-change-set'; if (cloudFormationStack.exists) { //Delete any existing change sets generated by CDK since change set names must be unique. //The delete request is successful as long as the stack exists (even if the change set does not exist). @@ -250,6 +283,7 @@ export async function deployStack(options: DeployStackOptions): Promise { + const currentTemplate = await cloudFormationStack.template(); + const stackChanges = cfn_diff.diffTemplate(currentTemplate, stackArtifact.template); + + // resolve the environment, so we can substitute things like AWS::Region in CFN expressions + const resolvedEnv = await sdkProvider.resolveEnvironment(stackArtifact.environment); + const hotswappableChanges = findAllHotswappableChanges(stackChanges, { + ...assetParams, + 'AWS::Region': resolvedEnv.region, + 'AWS::AccountId': resolvedEnv.account, + }); + if (!hotswappableChanges) { + // this means there were changes to the template that cannot be short-circuited + return undefined; + } + + // create a new SDK using the CLI credentials, because the default one will not work for new-style synthesis - + // it assumes the bootstrap deploy Role, which doesn't have permissions to update Lambda functions + const sdk = await sdkProvider.forEnvironment(resolvedEnv, Mode.ForWriting); + // apply the short-circuitable changes + await applyAllHotswappableChanges(sdk, stackArtifact, hotswappableChanges); + + return { noOp: hotswappableChanges.length === 0, stackArn: cloudFormationStack.stackId, outputs: cloudFormationStack.outputs, stackArtifact }; +} + +function findAllHotswappableChanges( + stackChanges: cfn_diff.TemplateDiff, assetParamsWithEnv: { [key: string]: string }, +): HotswapOperation[] | undefined { + const hotswappableResources = new Array(); + let foundNonHotswappableChange = false; + stackChanges.resources.forEachDifference((logicalId: string, change: cfn_diff.ResourceDifference) => { + const lambdaFunctionShortCircuitChange = isHotswappableLambdaFunctionChange(logicalId, change, assetParamsWithEnv); + if (lambdaFunctionShortCircuitChange === ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT) { + foundNonHotswappableChange = true; + } else if (lambdaFunctionShortCircuitChange === ChangeHotswapImpact.IRRELEVANT) { + // empty 'if' just for flow-aware typing to kick in... + } else { + hotswappableResources.push(lambdaFunctionShortCircuitChange); + } + }); + return foundNonHotswappableChange ? undefined : hotswappableResources; +} + +async function applyAllHotswappableChanges( + sdk: ISDK, stackArtifact: cxapi.CloudFormationStackArtifact, hotswappableChanges: HotswapOperation[], +): Promise { + // The current resources of the Stack. + // We need them to figure out the physical name of a function in case it wasn't specified by the user. + // We fetch it lazily, to save a service call, in case all updated Lambdas have their names set. + const listStackResources = new LazyListStackResources(sdk, stackArtifact.stackName); + + return Promise.all(hotswappableChanges.map(hotswapOperation => hotswapOperation.apply(sdk, listStackResources))); +} + +class LazyListStackResources implements ListStackResources { + private stackResources: CloudFormation.StackResourceSummary[] | undefined; + + constructor(private readonly sdk: ISDK, private readonly stackName: string) { + } + + async listStackResources(): Promise { + if (this.stackResources === undefined) { + this.stackResources = await this.getStackResource(); + } + return this.stackResources; + } + + private async getStackResource(): Promise { + const ret = new Array(); + let nextToken: string | undefined; + do { + const stackResourcesResponse = await this.sdk.cloudFormation().listStackResources({ + StackName: this.stackName, + NextToken: nextToken, + }).promise(); + ret.push(...(stackResourcesResponse.StackResourceSummaries ?? [])); + nextToken = stackResourcesResponse.NextToken; + } while (nextToken); + return ret; + } +} diff --git a/packages/aws-cdk/lib/api/hotswap/common.ts b/packages/aws-cdk/lib/api/hotswap/common.ts new file mode 100644 index 0000000000000..d509c32b4c781 --- /dev/null +++ b/packages/aws-cdk/lib/api/hotswap/common.ts @@ -0,0 +1,57 @@ +import * as cfn_diff from '@aws-cdk/cloudformation-diff'; +import { CloudFormation } from 'aws-sdk'; +import { ISDK } from '../aws-auth'; +import { evaluateCfn } from '../util/cloudformation/evaluate-cfn'; + +export interface ListStackResources { + listStackResources(): Promise; +} + +/** + * An interface that represents a change that can be deployed in a short-circuit manner. + */ +export interface HotswapOperation { + apply(sdk: ISDK, stackResources: ListStackResources): Promise; +} + +/** + * An enum that represents the result of detection whether a given change can be hotswapped. + */ +export enum ChangeHotswapImpact { + /** + * This result means that the given change cannot be hotswapped, + * and requires a full deployment. + */ + REQUIRES_FULL_DEPLOYMENT = 'requires-full-deployment', + + /** + * This result means that the given change can be safely be ignored when determining + * whether the given Stack can be hotswapped or not + * (for example, it's a change to the CDKMetadata resource). + */ + IRRELEVANT = 'irrelevant', +} + +export type ChangeHotswapResult = HotswapOperation | ChangeHotswapImpact; + +/** + * For old-style synthesis which uses CFN Parameters, + * the Code properties can have the values of complex CFN expressions. + * For new-style synthesis of env-agnostic stacks, + * the Fn::Sub expression is used for the Asset bucket. + * Evaluate the CFN expressions to concrete string values which we need for the + * updateFunctionCode() service call. + */ +export function stringifyPotentialCfnExpression(value: any, assetParamsWithEnv: { [key: string]: string }): string { + // if we already have a string, nothing to do + if (value == null || typeof value === 'string') { + return value; + } + + // otherwise, we assume this is a CloudFormation expression that we need to evaluate + return evaluateCfn(value, assetParamsWithEnv); +} + +export function assetMetadataChanged(change: cfn_diff.ResourceDifference): boolean { + return !!change.newValue?.Metadata['aws:asset:path']; +} diff --git a/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts b/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts new file mode 100644 index 0000000000000..73b4c529188c5 --- /dev/null +++ b/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts @@ -0,0 +1,159 @@ +import * as cfn_diff from '@aws-cdk/cloudformation-diff'; +import { ISDK } from '../aws-auth'; +import { assetMetadataChanged, ChangeHotswapImpact, ChangeHotswapResult, HotswapOperation, ListStackResources, stringifyPotentialCfnExpression } from './common'; + +/** + * Returns `false` if the change cannot be short-circuited, + * `true` if the change is irrelevant from a short-circuit perspective + * (like a change to CDKMetadata), + * or a LambdaFunctionResource if the change can be short-circuited. + */ +export function isHotswappableLambdaFunctionChange( + logicalId: string, change: cfn_diff.ResourceDifference, assetParamsWithEnv: { [key: string]: string }, +): ChangeHotswapResult { + const lambdaCodeChange = isLambdaFunctionCodeOnlyChange(change, assetParamsWithEnv); + if (typeof lambdaCodeChange === 'string') { + return lambdaCodeChange; + } else { + // verify that the Asset changed - otherwise, + // it's a Code property-only change, + // but not to an asset change + // (for example, going from Code.fromAsset() to Code.fromInline()) + if (!assetMetadataChanged(change)) { + return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; + } + + let functionPhysicalName: string | undefined; + try { + functionPhysicalName = stringifyPotentialCfnExpression(change.newValue?.Properties?.FunctionName, assetParamsWithEnv); + } catch (e) { + // It's possible we can't evaluate the function's name - + // for example, it can use a Ref to a different resource, + // which we wouldn't have in `assetParamsWithEnv`. + // That's fine though - ignore any errors, + // and treat this case the same way as if the name wasn't provided at all, + // which means it will be looked up using the listStackResources() call + // by the later phase (which actually does the Lambda function update) + functionPhysicalName = undefined; + } + + return new LambdaFunctionHotswapOperation({ + logicalId, + physicalName: functionPhysicalName, + code: lambdaCodeChange, + }); + } +} + +/** + * Returns `true` if the change is not for a AWS::Lambda::Function, + * but doesn't prevent short-circuiting + * (like a change to CDKMetadata resource), + * `false` if the change is to a AWS::Lambda::Function, + * but not only to its Code property, + * or a LambdaFunctionCode if the change is to a AWS::Lambda::Function, + * and only affects its Code property. + */ +function isLambdaFunctionCodeOnlyChange( + change: cfn_diff.ResourceDifference, assetParamsWithEnv: { [key: string]: string }, +): LambdaFunctionCode | ChangeHotswapImpact { + if (!change.newValue) { + return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; + } + const newResourceType = change.newValue.Type; + // Ignore Metadata changes + if (newResourceType === 'AWS::CDK::Metadata') { + return ChangeHotswapImpact.IRRELEVANT; + } + // The only other resource change we should see is a Lambda function + if (newResourceType !== 'AWS::Lambda::Function') { + return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; + } + if (change.oldValue?.Type == null) { + // this means this is a brand-new Lambda function - + // obviously, we can't short-circuit that! + return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; + } + + /* + * On first glance, we would want to initialize these using the "previous" values (change.oldValue), + * in case only one of them changed, like the key, and the Bucket stayed the same. + * However, that actually fails for old-style synthesis, which uses CFN Parameters! + * Because the names of the Parameters depend on the hash of the Asset, + * the Parameters used for the "old" values no longer exist in `assetParams` at this point, + * which means we don't have the correct values available to evaluate the CFN expression with. + * Fortunately, the diff will always include both the s3Bucket and s3Key parts of the Lambda's Code property, + * even if only one of them was actually changed, + * which means we don't need the "old" values at all, and we can safely initialize these with just `''`. + */ + let s3Bucket = '', s3Key = ''; + let foundCodeDifference = false; + // Make sure only the code in the Lambda function changed + const propertyUpdates = change.propertyUpdates; + for (const updatedPropName in propertyUpdates) { + const updatedProp = propertyUpdates[updatedPropName]; + if (updatedProp.newValue === undefined) { + return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; + } + for (const newPropName in updatedProp.newValue) { + switch (newPropName) { + case 'S3Bucket': + foundCodeDifference = true; + s3Bucket = stringifyPotentialCfnExpression(updatedProp.newValue[newPropName], assetParamsWithEnv); + break; + case 'S3Key': + foundCodeDifference = true; + s3Key = stringifyPotentialCfnExpression(updatedProp.newValue[newPropName], assetParamsWithEnv); + break; + default: + return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; + } + } + } + + return foundCodeDifference + ? { + s3Bucket, + s3Key, + } + : ChangeHotswapImpact.IRRELEVANT; +} + +interface LambdaFunctionCode { + readonly s3Bucket: string; + readonly s3Key: string; +} + +interface LambdaFunctionResource { + readonly logicalId: string; + readonly physicalName?: string; + readonly code: LambdaFunctionCode; +} + +class LambdaFunctionHotswapOperation implements HotswapOperation { + constructor(private readonly lambdaFunctionResource: LambdaFunctionResource) { + } + + public async apply(sdk: ISDK, stackResources: ListStackResources): Promise { + let functionPhysicalName: string; + if (this.lambdaFunctionResource.physicalName) { + functionPhysicalName = this.lambdaFunctionResource.physicalName; + } else { + const stackResourceList = await stackResources.listStackResources(); + const foundFunctionName = stackResourceList + .find(resSummary => resSummary.LogicalResourceId === this.lambdaFunctionResource.logicalId) + ?.PhysicalResourceId; + if (!foundFunctionName) { + // if we couldn't find the function in the current stack, we can't update it + return; + } + functionPhysicalName = foundFunctionName; + } + + return sdk.lambda().updateFunctionCode({ + FunctionName: functionPhysicalName, + S3Bucket: this.lambdaFunctionResource.code.s3Bucket, + S3Key: this.lambdaFunctionResource.code.s3Key, + }).promise(); + } +} diff --git a/packages/aws-cdk/lib/api/util/cloudformation/evaluate-cfn.ts b/packages/aws-cdk/lib/api/util/cloudformation/evaluate-cfn.ts new file mode 100644 index 0000000000000..bdc395df83814 --- /dev/null +++ b/packages/aws-cdk/lib/api/util/cloudformation/evaluate-cfn.ts @@ -0,0 +1,89 @@ +export function evaluateCfn(object: any, context: { [key: string]: string }): any { + const intrinsicFns: any = { + 'Fn::Join'(separator: string, args: string[]): string { + return evaluate(args).map(evaluate).join(separator); + }, + + 'Fn::Split'(separator: string, args: string): string { + return evaluate(args).split(separator); + }, + + 'Fn::Select'(index: number, args: string[]): string { + return evaluate(args).map(evaluate)[index]; + }, + + 'Ref'(logicalId: string): string { + if (logicalId in context) { + return context[logicalId]; + } else { + throw new Error(`Reference target '${logicalId}' was not found`); + } + }, + + 'Fn::Sub'(template: string, explicitPlaceholders?: { [variable: string]: string }): string { + const placeholders = explicitPlaceholders + ? { ...context, ...evaluate(explicitPlaceholders) } + : context; + + return template.replace(/\${([^}]*)}/g, (_: string, key: string) => { + if (key in placeholders) { + return placeholders[key]; + } else { + throw new Error(`Fn::Sub target '${key}' was not found`); + } + }); + }, + }; + + return evaluate(object); + + function evaluate(obj: any): any { + if (Array.isArray(obj)) { + return obj.map(evaluate); + } + + if (typeof obj === 'object') { + const intrinsic = parseIntrinsic(obj); + if (intrinsic) { + return evaluateIntrinsic(intrinsic); + } + + const ret: { [key: string]: any } = {}; + for (const key of Object.keys(obj)) { + ret[key] = evaluate(obj[key]); + } + return ret; + } + + return obj; + } + + function evaluateIntrinsic(intrinsic: Intrinsic) { + if (!(intrinsic.name in intrinsicFns)) { + throw new Error(`Intrinsic ${intrinsic.name} not supported here`); + } + + const argsAsArray = Array.isArray(intrinsic.args) ? intrinsic.args : [intrinsic.args]; + + return intrinsicFns[intrinsic.name].apply(intrinsicFns, argsAsArray); + } +} + +interface Intrinsic { + readonly name: string; + readonly args: any; +} + +function parseIntrinsic(x: any): Intrinsic | undefined { + if (typeof x !== 'object' || x === null) { + return undefined; + } + const keys = Object.keys(x); + if (keys.length === 1 && (keys[0].startsWith('Fn::') || keys[0] === 'Ref')) { + return { + name: keys[0], + args: x[keys[0]], + }; + } + return undefined; +} diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 427139535bde5..636da75d12975 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -131,6 +131,11 @@ export class CdkToolkit { } } + if (options.hotswap) { + warning('⚠️ The --hotswap flag deliberately introduces CloudFormation drift to speed up deployments'); + warning('⚠️ It should only be used for development - never use it for your production Stacks!'); + } + const stackOutputs: { [key: string]: any } = { }; const outputsFile = options.outputsFile; @@ -197,6 +202,7 @@ export class CdkToolkit { progress: options.progress, ci: options.ci, rollback: options.rollback, + hotswap: options.hotswap, }); const message = result.noOp @@ -391,7 +397,7 @@ export class CdkToolkit { return stacks; } - private async selectStacksForDeploy(selector: StackSelector, exclusively?: boolean) { + private async selectStacksForDeploy(selector: StackSelector, exclusively?: boolean): Promise { const assembly = await this.assembly(); const stacks = await assembly.selectStacks(selector, { extend: exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Upstream, @@ -403,7 +409,7 @@ export class CdkToolkit { return stacks; } - private async selectStacksForDiff(stackNames: string[], exclusively?: boolean, autoValidate?: boolean) { + private async selectStacksForDiff(stackNames: string[], exclusively?: boolean, autoValidate?: boolean): Promise { const assembly = await this.assembly(); const selectedForDiff = await assembly.selectStacks({ patterns: stackNames }, { @@ -633,6 +639,14 @@ export interface DeployOptions { * @default true */ readonly rollback?: boolean; + /* + * Whether to perform a 'hotswap' deployment. + * A 'hotswap' deployment will attempt to short-circuit CloudFormation + * and update the affected resources like Lambda functions directly. + * + * @default - false (do not perform a 'hotswap' deployment) + */ + readonly hotswap?: boolean; } export interface DestroyOptions { @@ -672,4 +686,4 @@ function tagsForStack(stack: cxapi.CloudFormationStackArtifact): Tag[] { export interface Tag { readonly Key: string; readonly Value: string; -} \ No newline at end of file +} diff --git a/packages/aws-cdk/test/api/cloudformation-deployments.test.ts b/packages/aws-cdk/test/api/cloudformation-deployments.test.ts index 7a4e29628564c..c7a64a7b19ff3 100644 --- a/packages/aws-cdk/test/api/cloudformation-deployments.test.ts +++ b/packages/aws-cdk/test/api/cloudformation-deployments.test.ts @@ -34,6 +34,21 @@ function mockSuccessfulBootstrapStackLookup(props?: Record) { mockToolkitInfoLookup.mockResolvedValue(ToolkitInfo.fromStack(fakeStack, sdkProvider.sdk)); } +test('passes through hotswap=true to deployStack()', async () => { + // WHEN + await deployments.deployStack({ + stack: testStack({ + stackName: 'boop', + }), + hotswap: true, + }); + + // THEN + expect(deployStack).toHaveBeenCalledWith(expect.objectContaining({ + hotswap: true, + })); +}); + test('placeholders are substituted in CloudFormation execution role', async () => { await deployments.deployStack({ stack: testStack({ diff --git a/packages/aws-cdk/test/api/deploy-stack.test.ts b/packages/aws-cdk/test/api/deploy-stack.test.ts index 844b971e7f2d4..eb551ac528ec4 100644 --- a/packages/aws-cdk/test/api/deploy-stack.test.ts +++ b/packages/aws-cdk/test/api/deploy-stack.test.ts @@ -1,7 +1,10 @@ -import { deployStack, ToolkitInfo } from '../../lib/api'; +import { deployStack, DeployStackOptions, ToolkitInfo } from '../../lib/api'; +import { tryHotswapDeployment } from '../../lib/api/hotswap-deployments'; import { DEFAULT_FAKE_TEMPLATE, testStack } from '../util'; import { MockedObject, mockResolvedEnvironment, MockSdk, MockSdkProvider, SyncHandlerSubsetOf } from '../util/mock-sdk'; +jest.mock('../../lib/api/hotswap-deployments'); + const FAKE_STACK = testStack({ stackName: 'withouterrors', }); @@ -27,6 +30,8 @@ let sdk: MockSdk; let sdkProvider: MockSdkProvider; let cfnMocks: MockedObject>; beforeEach(() => { + jest.resetAllMocks(); + sdkProvider = new MockSdkProvider(); sdk = new MockSdk(); @@ -60,7 +65,7 @@ beforeEach(() => { }); -function standardDeployStackArguments() { +function standardDeployStackArguments(): DeployStackOptions { return { stack: FAKE_STACK, sdk, @@ -70,6 +75,57 @@ function standardDeployStackArguments() { }; } +test("calls tryHotswapDeployment() if 'hotswap' is true", async () => { + // WHEN + await deployStack({ + ...standardDeployStackArguments(), + hotswap: true, + }); + + // THEN + expect(tryHotswapDeployment).toHaveBeenCalled(); +}); + +test("does not call tryHotswapDeployment() if 'hotswap' is false", async () => { + // WHEN + await deployStack({ + ...standardDeployStackArguments(), + hotswap: false, + }); + + // THEN + expect(tryHotswapDeployment).not.toHaveBeenCalled(); +}); + +test("rollback defaults to disabled if 'hotswap' is true", async () => { + // WHEN + await deployStack({ + ...standardDeployStackArguments(), + hotswap: true, + rollback: undefined, + }); + + // THEN + expect(cfnMocks.executeChangeSet).toHaveBeenCalledWith(expect.objectContaining({ + DisableRollback: true, + })); +}); + +test("rollback defaults to enabled if 'hotswap' is false", async () => { + // WHEN + await deployStack({ + ...standardDeployStackArguments(), + hotswap: false, + rollback: undefined, + }); + + // THEN + expect(cfnMocks.executeChangeSet).toHaveBeenCalledTimes(1); + expect(cfnMocks.executeChangeSet).not.toHaveBeenCalledWith(expect.objectContaining({ + DisableRollback: expect.anything(), + })); +}); + test('do deploy executable change set with 0 changes', async () => { // WHEN const ret = await deployStack({ @@ -699,4 +755,4 @@ function givenTemplateIs(template: any) { cfnMocks.getTemplate!.mockReturnValue({ TemplateBody: JSON.stringify(template), }); -} \ No newline at end of file +} diff --git a/packages/aws-cdk/test/api/fake-cloudformation-stack.ts b/packages/aws-cdk/test/api/fake-cloudformation-stack.ts new file mode 100644 index 0000000000000..3e722f3cad249 --- /dev/null +++ b/packages/aws-cdk/test/api/fake-cloudformation-stack.ts @@ -0,0 +1,34 @@ +import { CloudFormation } from 'aws-sdk'; +import { CloudFormationStack, Template } from '../../lib/api/util/cloudformation'; +import { instanceMockFrom } from '../util'; + +export interface FakeCloudFormationStackProps { + readonly stackName: string; + readonly stackId: string; +} + +export class FakeCloudformationStack extends CloudFormationStack { + public readonly cfnMock: jest.Mocked; + private readonly props: FakeCloudFormationStackProps; + private __template: Template; + + public constructor(props: FakeCloudFormationStackProps) { + const cfnMock = instanceMockFrom(CloudFormation); + super(cfnMock, props.stackName); + this.cfnMock = cfnMock; + this.props = props; + this.__template = {}; + } + + public setTemplate(template: Template): void { + this.__template = template; + } + + public async template(): Promise