From 8e2325cfb7dc5377755b561532b6c81caebc688f Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 6 Apr 2021 14:52:09 +0200 Subject: [PATCH 01/25] fix(cloudfront): cannot use same EdgeFunction in multiple stacks (#13790) Using the same `EdgeFunction` in multiple stacks correctly created multiple stacks in `us-east-1` but with the same SSM parameter name. As a consquence, only one stack could be deployed. Fix it by including the stack unique address in the SSM parameter name. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/experimental/edge-function.ts | 14 ++++----- .../test/experimental/edge-function.test.ts | 31 ++++++++++++++++--- ...ribution-lambda-cross-region.expected.json | 10 +++--- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts b/packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts index b12a56fe67e80..ab8d94e79baa1 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts @@ -148,8 +148,11 @@ export class EdgeFunction extends Resource implements lambda.IVersion { /** Create a support stack and function in us-east-1, and a SSM reader in-region */ private createCrossRegionFunction(id: string, props: EdgeFunctionProps): FunctionConfig { - const parameterNamePrefix = 'EdgeFunctionArn'; - const parameterName = `${parameterNamePrefix}${id}`; + const parameterNamePrefix = '/cdk/EdgeFunctionArn'; + if (Token.isUnresolved(this.env.region)) { + throw new Error('stacks which use EdgeFunctions must have an explicitly set region'); + } + const parameterName = `${parameterNamePrefix}/${this.env.region}/${this.node.path}`; const functionStack = this.edgeStack(props.stackId); const edgeFunction = new lambda.Function(functionStack, id, props); @@ -174,7 +177,8 @@ export class EdgeFunction extends Resource implements lambda.IVersion { service: 'ssm', region: EdgeFunction.EDGE_REGION, resource: 'parameter', - resourceName: parameterNamePrefix + '*', + resourceName: parameterNamePrefix + '/*', + sep: '', }); const resourceType = 'Custom::CrossRegionStringParameterReader'; @@ -206,10 +210,6 @@ export class EdgeFunction extends Resource implements lambda.IVersion { if (!stage) { throw new Error('stacks which use EdgeFunctions must be part of a CDK app or stage'); } - const region = this.env.region; - if (Token.isUnresolved(region)) { - throw new Error('stacks which use EdgeFunctions must have an explicitly set region'); - } const edgeStackId = stackId ?? `edge-lambda-stack-${this.stack.node.addr}`; let edgeStack = stage.node.tryFindChild(edgeStackId) as Stack; diff --git a/packages/@aws-cdk/aws-cloudfront/test/experimental/edge-function.test.ts b/packages/@aws-cdk/aws-cloudfront/test/experimental/edge-function.test.ts index 55b0c2f4aeaac..36edf19a39056 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/experimental/edge-function.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/experimental/edge-function.test.ts @@ -39,7 +39,7 @@ describe('stacks', () => { Statement: [{ Effect: 'Allow', Resource: { - 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:us-east-1:111111111111:parameter/EdgeFunctionArn*']], + 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':ssm:us-east-1:111111111111:parameter/cdk/EdgeFunctionArn/*']], }, Action: ['ssm:GetParameter'], }], @@ -57,7 +57,7 @@ describe('stacks', () => { 'Fn::GetAtt': ['CustomCrossRegionStringParameterReaderCustomResourceProviderHandler65B5F33A', 'Arn'], }, Region: 'us-east-1', - ParameterName: 'EdgeFunctionArnMyFn', + ParameterName: '/cdk/EdgeFunctionArn/testregion/Stack/MyFn', }); }); @@ -98,7 +98,7 @@ describe('stacks', () => { expect(fnStack).toHaveResource('AWS::SSM::Parameter', { Type: 'String', Value: { Ref: 'MyFnCurrentVersion309B29FC29686ce94039b6e08d1645be854b3ac9' }, - Name: 'EdgeFunctionArnMyFn', + Name: '/cdk/EdgeFunctionArn/testregion/Stack/MyFn', }); }); @@ -201,7 +201,30 @@ describe('stacks', () => { 'Fn::GetAtt': ['CustomCrossRegionStringParameterReaderCustomResourceProviderHandler65B5F33A', 'Arn'], }, Region: 'us-east-1', - ParameterName: 'EdgeFunctionArnMyFn', + ParameterName: '/cdk/EdgeFunctionArn/testregion/Stage/Stack/MyFn', + }); + }); + + test('a single EdgeFunction used in multiple stacks creates mutiple stacks in us-east-1', () => { + const firstStack = new cdk.Stack(app, 'FirstStack', { + env: { account: '111111111111', region: 'testregion' }, + }); + const secondStack = new cdk.Stack(app, 'SecondStack', { + env: { account: '111111111111', region: 'testregion' }, + }); + new cloudfront.experimental.EdgeFunction(firstStack, 'MyFn', defaultEdgeFunctionProps()); + new cloudfront.experimental.EdgeFunction(secondStack, 'MyFn', defaultEdgeFunctionProps()); + + // Two stacks in us-east-1 + const firstFnStack = app.node.findChild(`edge-lambda-stack-${firstStack.node.addr}`) as cdk.Stack; + const secondFnStack = app.node.findChild(`edge-lambda-stack-${secondStack.node.addr}`) as cdk.Stack; + + // Two SSM parameters + expect(firstFnStack).toHaveResourceLike('AWS::SSM::Parameter', { + Name: '/cdk/EdgeFunctionArn/testregion/FirstStack/MyFn', + }); + expect(secondFnStack).toHaveResourceLike('AWS::SSM::Parameter', { + Name: '/cdk/EdgeFunctionArn/testregion/SecondStack/MyFn', }); }); }); diff --git a/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda-cross-region.expected.json b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda-cross-region.expected.json index c251765b20980..6da7e8717d61f 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda-cross-region.expected.json +++ b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda-cross-region.expected.json @@ -11,7 +11,7 @@ ] }, "Region": "us-east-1", - "ParameterName": "EdgeFunctionArnLambda", + "ParameterName": "/cdk/EdgeFunctionArn/eu-west-1/integ-distribution-lambda-cross-region/Lambda", "RefreshToken": "4412ddb0ae449da20173ca211c51fddc" }, "UpdateReplacePolicy": "Delete", @@ -57,7 +57,7 @@ { "Ref": "AWS::AccountId" }, - ":parameter/EdgeFunctionArn*" + ":parameter/cdk/EdgeFunctionArn/*" ] ] }, @@ -137,7 +137,7 @@ ] }, "Region": "us-east-1", - "ParameterName": "EdgeFunctionArnLambda2", + "ParameterName": "/cdk/EdgeFunctionArn/eu-west-1/integ-distribution-lambda-cross-region/Lambda2", "RefreshToken": "8f81ceb404ac454f09648e62822d9ca9" }, "UpdateReplacePolicy": "Delete", @@ -278,7 +278,7 @@ "Value": { "Ref": "LambdaCurrentVersionDF706F6A97fb843e9bd06fcd2bb15eeace80e13e" }, - "Name": "EdgeFunctionArnLambda" + "Name": "/cdk/EdgeFunctionArn/eu-west-1/integ-distribution-lambda-cross-region/Lambda" } }, "LambdaAliaslive79C8A712": { @@ -372,7 +372,7 @@ "Value": { "Ref": "Lambda2CurrentVersion72012B74b9eef8becb98501bc795baca3c6169c4" }, - "Name": "EdgeFunctionArnLambda2" + "Name": "/cdk/EdgeFunctionArn/eu-west-1/integ-distribution-lambda-cross-region/Lambda2" } }, "Lambda2Aliaslive77F6085F": { From 02c7c1d9aab6ed8f806052d3102a037e112b8786 Mon Sep 17 00:00:00 2001 From: Stijn Brouwers Date: Tue, 6 Apr 2021 15:41:01 +0200 Subject: [PATCH 02/25] feat(route-53): add ability to create NS Records (#13895) Adds feature as requested in issue #13816 ---- *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-route53/README.md | 16 ++++++++++ .../@aws-cdk/aws-route53/lib/record-set.ts | 25 +++++++++++++++ packages/@aws-cdk/aws-route53/package.json | 1 + .../aws-route53/test/record-set.test.ts | 31 +++++++++++++++++++ 4 files changed, 73 insertions(+) diff --git a/packages/@aws-cdk/aws-route53/README.md b/packages/@aws-cdk/aws-route53/README.md index 9f36aa7f58765..0213665ff2b65 100644 --- a/packages/@aws-cdk/aws-route53/README.md +++ b/packages/@aws-cdk/aws-route53/README.md @@ -60,6 +60,22 @@ new route53.TxtRecord(this, 'TXTRecord', { }); ``` +To add a NS record to your zone: + +```ts +import * as route53 from '@aws-cdk/aws-route53'; + +new route53.NsRecord(this, 'NSRecord', { + zone: myZone, + recordName: 'foo', + values: [ + 'ns-1.awsdns.co.uk.', + 'ns-2.awsdns.com.' + ], + ttl: Duration.minutes(90), // Optional - default is 30 minutes +}); +``` + To add an A record to your zone: ```ts diff --git a/packages/@aws-cdk/aws-route53/lib/record-set.ts b/packages/@aws-cdk/aws-route53/lib/record-set.ts index a86195f1d0055..566aea6d97a70 100644 --- a/packages/@aws-cdk/aws-route53/lib/record-set.ts +++ b/packages/@aws-cdk/aws-route53/lib/record-set.ts @@ -541,6 +541,31 @@ export class MxRecord extends RecordSet { } } +/** + * Construction properties for a NSRecord. + */ +export interface NsRecordProps extends RecordSetOptions { + /** + * The NS values. + */ + readonly values: string[]; +} + +/** + * A DNS NS record + * + * @resource AWS::Route53::RecordSet + */ +export class NsRecord extends RecordSet { + constructor(scope: Construct, id: string, props: NsRecordProps) { + super(scope, id, { + ...props, + recordType: RecordType.NS, + target: RecordTarget.fromValues(...props.values), + }); + } +} + /** * Construction properties for a ZoneDelegationRecord */ diff --git a/packages/@aws-cdk/aws-route53/package.json b/packages/@aws-cdk/aws-route53/package.json index 4693e9d5c2d19..5fc7ed278b89e 100644 --- a/packages/@aws-cdk/aws-route53/package.json +++ b/packages/@aws-cdk/aws-route53/package.json @@ -112,6 +112,7 @@ "props-physical-name:@aws-cdk/aws-route53.CnameRecordProps", "props-physical-name:@aws-cdk/aws-route53.HostedZoneProps", "props-physical-name:@aws-cdk/aws-route53.MxRecordProps", + "props-physical-name:@aws-cdk/aws-route53.NsRecordProps", "props-physical-name:@aws-cdk/aws-route53.PrivateHostedZoneProps", "props-physical-name:@aws-cdk/aws-route53.PublicHostedZoneProps", "props-physical-name:@aws-cdk/aws-route53.RecordSetProps", 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 373464f455992..e1a8d177b52d6 100644 --- a/packages/@aws-cdk/aws-route53/test/record-set.test.ts +++ b/packages/@aws-cdk/aws-route53/test/record-set.test.ts @@ -485,6 +485,37 @@ nodeunitShim({ test.done(); }, + 'NS record'(test: Test) { + // GIVEN + const stack = new Stack(); + + const zone = new route53.HostedZone(stack, 'HostedZone', { + zoneName: 'myzone', + }); + + // WHEN + new route53.NsRecord(stack, 'NS', { + zone, + recordName: 'www', + values: ['ns-1.awsdns.co.uk.', 'ns-2.awsdns.com.'], + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::RecordSet', { + Name: 'www.myzone.', + Type: 'NS', + HostedZoneId: { + Ref: 'HostedZoneDB99F866', + }, + ResourceRecords: [ + 'ns-1.awsdns.co.uk.', + 'ns-2.awsdns.com.', + ], + TTL: '1800', + })); + test.done(); + }, + 'Zone delegation record'(test: Test) { // GIVEN const stack = new Stack(); From 4a342c811bac1173ecb164384d4c1b4ee001d454 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 6 Apr 2021 17:54:26 +0200 Subject: [PATCH 03/25] chore(cli): change Java init templates to be less repetitive (#13995) The Java init templates were recently fixed to use `new` instead of the builder, because the builder would instantiate the wrong class (`Stack` instead of `MyStack`) (#13988). However, the structure was changed to have 3 different `new` calls in 3 different comment blocks. Change it back to the old structure: there is one instantiation, and uncommenting just passes or doesn't pass the `env` property. Except this time we use `new` instead of the builder. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../myorg/%name.PascalCased%App.template.java | 31 ++++++++--------- .../myorg/%name.PascalCased%App.template.java | 33 +++++++++---------- 2 files changed, 28 insertions(+), 36 deletions(-) diff --git a/packages/aws-cdk/lib/init-templates/v1/app/java/src/main/java/com/myorg/%name.PascalCased%App.template.java b/packages/aws-cdk/lib/init-templates/v1/app/java/src/main/java/com/myorg/%name.PascalCased%App.template.java index ba81d115702f8..f3eb72f9dfe0c 100644 --- a/packages/aws-cdk/lib/init-templates/v1/app/java/src/main/java/com/myorg/%name.PascalCased%App.template.java +++ b/packages/aws-cdk/lib/init-templates/v1/app/java/src/main/java/com/myorg/%name.PascalCased%App.template.java @@ -10,36 +10,31 @@ public class %name.PascalCased%App { public static void main(final String[] args) { App app = new App(); - // If you don't specify 'env', this stack will be environment-agnostic. - // Account/Region-dependent features and context lookups will not work, - // but a single synthesized template can be deployed anywhere. - new %name.PascalCased%Stack(app, "%name.PascalCased%Stack"); - - // Replace the above stack intialization with the following to specialize - // this stack for the AWS Account and Region that are implied by the current - // CLI configuration. - /* new %name.PascalCased%Stack(app, "%name.PascalCased%Stack", StackProps.builder() + // If you don't specify 'env', this stack will be environment-agnostic. + // Account/Region-dependent features and context lookups will not work, + // but a single synthesized template can be deployed anywhere. + + // Uncomment the next block to specialize this stack for the AWS Account + // and Region that are implied by the current CLI configuration. + /* .env(Environment.builder() .account(System.getenv("CDK_DEFAULT_ACCOUNT")) .region(System.getenv("CDK_DEFAULT_REGION")) .build()) - .build()); - */ + */ - // Replace the above stack initialization with the following if you know exactly - // what Account and Region you want to deploy the stack to. - /* - new %name.PascalCased%Stack(app, "%name.PascalCased%Stack", StackProps.builder() + // Uncomment the next block if you know exactly what Account and Region you + // want to deploy the stack to. + /* .env(Environment.builder() .account("123456789012") .region("us-east-1") .build()) + */ + // For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html .build()); - */ - - // For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html app.synth(); } diff --git a/packages/aws-cdk/lib/init-templates/v2/app/java/src/main/java/com/myorg/%name.PascalCased%App.template.java b/packages/aws-cdk/lib/init-templates/v2/app/java/src/main/java/com/myorg/%name.PascalCased%App.template.java index fb0d9b89350fa..08bccaddc2695 100644 --- a/packages/aws-cdk/lib/init-templates/v2/app/java/src/main/java/com/myorg/%name.PascalCased%App.template.java +++ b/packages/aws-cdk/lib/init-templates/v2/app/java/src/main/java/com/myorg/%name.PascalCased%App.template.java @@ -2,6 +2,7 @@ import software.amazon.awscdk.lib.App; import software.amazon.awscdk.lib.Environment; +import software.amazon.awscdk.lib.StackProps; import java.util.Arrays; @@ -9,37 +10,33 @@ public class %name.PascalCased%App { public static void main(final String[] args) { App app = new App(); - // If you don't specify 'env', this stack will be environment-agnostic. - // Account/Region-dependent features and context lookups will not work, - // but a single synthesized template can be deployed anywhere. - new %name.PascalCased%Stack(app, "%name.PascalCased%Stack"); - - // Replace the above stack intialization with the following to specialize - // this stack for the AWS Account and Region that are implied by the current - // CLI configuration. - /* new %name.PascalCased%Stack(app, "%name.PascalCased%Stack", StackProps.builder() + // If you don't specify 'env', this stack will be environment-agnostic. + // Account/Region-dependent features and context lookups will not work, + // but a single synthesized template can be deployed anywhere. + + // Uncomment the next block to specialize this stack for the AWS Account + // and Region that are implied by the current CLI configuration. + /* .env(Environment.builder() .account(System.getenv("CDK_DEFAULT_ACCOUNT")) .region(System.getenv("CDK_DEFAULT_REGION")) .build()) - .build()); - */ + */ - // Replace the above stack initialization with the following if you know exactly - // what Account and Region you want to deploy the stack to. - /* - new %name.PascalCased%Stack(app, "%name.PascalCased%Stack", StackProps.builder() + // Uncomment the next block if you know exactly what Account and Region you + // want to deploy the stack to. + /* .env(Environment.builder() .account("123456789012") .region("us-east-1") .build()) + */ + // For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html .build()); - */ - - // For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html app.synth(); } } + From 036d869dc1382d3fb2d8541f5adf534ea3424667 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Tue, 6 Apr 2021 18:07:37 +0100 Subject: [PATCH 04/25] fix(apigateway): cannot remove first api key from usage plan (#13817) The UsagePlanKey resource connects an ApiKey with a UsagePlan. The API Gateway service does not allow more than one UsagePlanKey for any given UsagePlan and ApiKey combination. For this reason, CloudFormation cannot replace this resource without either the UsagePlan or ApiKey changing. A feature was added back in Nov 2019 - 142bd0e2 - that allows multiple UsagePlanKey resources. The above limitation was recognized and logical id of the existing UsagePlanKey was retained. However, this unintentionally caused the logical id of the UsagePlanKey to be sensitive to order. That is, when the 'first' UsagePlanKey resource is removed, the logical id of the what was the 'second' UsagePlanKey is changed to be the logical id of what was the 'first'. This change to the logical id is, again, disallowed. To get out of this mess, we do two things - 1. introduce a feature flag that changes the default behaviour for all new CDK apps. 2. for customers with existing CDK apps who are would want to remove UsagePlanKey resource, introduce a 'overrideLogicalId' option that they can manually configure with the existing logical id. fixes #11876 ---- *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-apigateway/README.md | 65 ++++---- .../@aws-cdk/aws-apigateway/lib/usage-plan.ts | 34 +++- .../test/integ.restapi.expected.json | 2 +- .../integ.usage-plan.multikey.expected.json | 2 +- .../aws-apigateway/test/usage-plan.test.ts | 148 ++++++++++++------ packages/@aws-cdk/cx-api/lib/features.ts | 18 +++ 6 files changed, 187 insertions(+), 82 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 926f45daf1436..4607442308138 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -24,7 +24,7 @@ running on AWS Lambda, or any web application. - [Breaking up Methods and Resources across Stacks](#breaking-up-methods-and-resources-across-stacks) - [AWS Lambda-backed APIs](#aws-lambda-backed-apis) - [Integration Targets](#integration-targets) -- [API Keys](#api-keys) +- [Usage Plan & API Keys](#usage-plan--api-keys) - [Working with models](#working-with-models) - [Default Integration and Method Options](#default-integration-and-method-options) - [Proxy Routes](#proxy-routes) @@ -168,34 +168,36 @@ const getMessageIntegration = new apigateway.AwsIntegration({ }); ``` -## API Keys +## Usage Plan & API Keys -The following example shows how to use an API Key with a usage plan: +A usage plan specifies who can access one or more deployed API stages and methods, and the rate at which they can be +accessed. The plan uses API keys to identify API clients and meters access to the associated API stages for each key. +Usage plans also allow configuring throttling limits and quota limits that are enforced on individual client API keys. -```ts -const hello = new lambda.Function(this, 'hello', { - runtime: lambda.Runtime.NODEJS_12_X, - handler: 'hello.handler', - code: lambda.Code.fromAsset('lambda') -}); +The following example shows how to create and asscociate a usage plan and an API key: -const api = new apigateway.RestApi(this, 'hello-api', { }); -const integration = new apigateway.LambdaIntegration(hello); +```ts +const api = new apigateway.RestApi(this, 'hello-api'); const v1 = api.root.addResource('v1'); const echo = v1.addResource('echo'); const echoMethod = echo.addMethod('GET', integration, { apiKeyRequired: true }); -const key = api.addApiKey('ApiKey'); const plan = api.addUsagePlan('UsagePlan', { name: 'Easy', - apiKey: key, throttle: { rateLimit: 10, burstLimit: 2 } }); +const key = api.addApiKey('ApiKey'); +plan.addApiKey(key); +``` + +To associate a plan to a given RestAPI stage: + +```ts plan.addApiStage({ stage: api.deploymentStage, throttle: [ @@ -233,26 +235,36 @@ following code provides read permission to an API key. importedKey.grantRead(lambda); ``` -In scenarios where you need to create a single api key and configure rate limiting for it, you can use `RateLimitedApiKey`. -This construct lets you specify rate limiting properties which should be applied only to the api key being created. -The API key created has the specified rate limits, such as quota and throttles, applied. +### ⚠️ Multiple API Keys -The following example shows how to use a rate limited api key : +It is possible to specify multiple API keys for a given Usage Plan, by calling `usagePlan.addApiKey()`. + +When using multiple API keys, a past bug of the CDK prevents API key associations to a Usage Plan to be deleted. +If the CDK app had the [feature flag] - `@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId` - enabled when the API +keys were created, then the app will not be affected by this bug. + +If this is not the case, you will need to ensure that the CloudFormation [logical ids] of the API keys that are not +being deleted remain unchanged. +Make note of the logical ids of these API keys before removing any, and set it as part of the `addApiKey()` method: ```ts -const hello = new lambda.Function(this, 'hello', { - runtime: lambda.Runtime.NODEJS_12_X, - handler: 'hello.handler', - code: lambda.Code.fromAsset('lambda') +usageplan.addApiKey(apiKey, { + overrideLogicalId: '...', }); +``` -const api = new apigateway.RestApi(this, 'hello-api', { }); -const integration = new apigateway.LambdaIntegration(hello); +[feature flag]: https://docs.aws.amazon.com/cdk/latest/guide/featureflags.html +[logical ids]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resources-section-structure.html -const v1 = api.root.addResource('v1'); -const echo = v1.addResource('echo'); -const echoMethod = echo.addMethod('GET', integration, { apiKeyRequired: true }); +### Rate Limited API Key + +In scenarios where you need to create a single api key and configure rate limiting for it, you can use `RateLimitedApiKey`. +This construct lets you specify rate limiting properties which should be applied only to the api key being created. +The API key created has the specified rate limits, such as quota and throttles, applied. +The following example shows how to use a rate limited api key : + +```ts const key = new apigateway.RateLimitedApiKey(this, 'rate-limited-api-key', { customerId: 'hello-customer', resources: [api], @@ -261,7 +273,6 @@ const key = new apigateway.RateLimitedApiKey(this, 'rate-limited-api-key', { period: apigateway.Period.MONTH } }); - ``` ## Working with models diff --git a/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts b/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts index 49b3ee19cd017..82f45bc538db6 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts @@ -1,4 +1,5 @@ -import { Lazy, Names, Resource, Token } from '@aws-cdk/core'; +import { FeatureFlags, Lazy, Names, Resource, Token } from '@aws-cdk/core'; +import { APIGATEWAY_USAGEPLANKEY_ORDERINSENSITIVE_ID } from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { IApiKey } from './api-key'; import { CfnUsagePlan, CfnUsagePlanKey } from './apigateway.generated'; @@ -139,10 +140,22 @@ export interface UsagePlanProps { /** * ApiKey to be associated with the usage plan. * @default none + * @deprecated use `addApiKey()` */ readonly apiKey?: IApiKey; } +/** + * Options to the UsagePlan.addApiKey() method + */ +export interface AddApiKeyOptions { + /** + * Override the CloudFormation logical id of the AWS::ApiGateway::UsagePlanKey resource + * @default - autogenerated by the CDK + */ + readonly overrideLogicalId?: string; +} + export class UsagePlan extends Resource { /** * @attribute @@ -176,19 +189,28 @@ export class UsagePlan extends Resource { /** * Adds an ApiKey. * - * @param apiKey + * @param apiKey the api key to associate with this usage plan + * @param options options that control the behaviour of this method */ - public addApiKey(apiKey: IApiKey): void { + public addApiKey(apiKey: IApiKey, options?: AddApiKeyOptions): void { + let id: string; const prefix = 'UsagePlanKeyResource'; - // Postfixing apikey id only from the 2nd child, to keep physicalIds of UsagePlanKey for existing CDK apps unmodified. - const id = this.node.tryFindChild(prefix) ? `${prefix}:${Names.nodeUniqueId(apiKey.node)}` : prefix; + if (FeatureFlags.of(this).isEnabled(APIGATEWAY_USAGEPLANKEY_ORDERINSENSITIVE_ID)) { + id = `${prefix}:${Names.nodeUniqueId(apiKey.node)}`; + } else { + // Postfixing apikey id only from the 2nd child, to keep physicalIds of UsagePlanKey for existing CDK apps unmodified. + id = this.node.tryFindChild(prefix) ? `${prefix}:${Names.nodeUniqueId(apiKey.node)}` : prefix; + } - new CfnUsagePlanKey(this, id, { + const resource = new CfnUsagePlanKey(this, id, { keyId: apiKey.keyId, keyType: UsagePlanKeyType.API_KEY, usagePlanId: this.usagePlanId, }); + if (options?.overrideLogicalId) { + resource.overrideLogicalId(options?.overrideLogicalId); + } } /** diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json index 91af3471593eb..a0fb6357db3c7 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json @@ -602,7 +602,7 @@ "UsagePlanName": "Basic" } }, - "myapiUsagePlanUsagePlanKeyResource050D133F": { + "myapiUsagePlanUsagePlanKeyResourcetestapigatewayrestapimyapiApiKeyC43601CB600D112D": { "Type": "AWS::ApiGateway::UsagePlanKey", "Properties": { "KeyId": { diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.usage-plan.multikey.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.usage-plan.multikey.expected.json index 8e761f40e2a26..9dee2e7aa07b0 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.usage-plan.multikey.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.usage-plan.multikey.expected.json @@ -3,7 +3,7 @@ "myusageplan4B391740": { "Type": "AWS::ApiGateway::UsagePlan" }, - "myusageplanUsagePlanKeyResource095B4EA9": { + "myusageplanUsagePlanKeyResourcetestapigatewayusageplanmultikeymyapikey1DDABC389A2809A73": { "Type": "AWS::ApiGateway::UsagePlanKey", "Properties": { "KeyId": { diff --git a/packages/@aws-cdk/aws-apigateway/test/usage-plan.test.ts b/packages/@aws-cdk/aws-apigateway/test/usage-plan.test.ts index f183d08796388..cef19db1e9789 100644 --- a/packages/@aws-cdk/aws-apigateway/test/usage-plan.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/usage-plan.test.ts @@ -1,6 +1,8 @@ import '@aws-cdk/assert/jest'; import { ResourcePart } from '@aws-cdk/assert'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; +import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as apigateway from '../lib'; const RESOURCE_TYPE = 'AWS::ApiGateway::UsagePlan'; @@ -149,60 +151,112 @@ describe('usage plan', () => { }, ResourcePart.Properties); }); - test('UsagePlanKey', () => { - // GIVEN - const stack = new cdk.Stack(); - const usagePlan: apigateway.UsagePlan = new apigateway.UsagePlan(stack, 'my-usage-plan', { - name: 'Basic', + describe('UsagePlanKey', () => { + + test('default', () => { + // GIVEN + const stack = new cdk.Stack(); + const usagePlan: apigateway.UsagePlan = new apigateway.UsagePlan(stack, 'my-usage-plan', { + name: 'Basic', + }); + const apiKey: apigateway.ApiKey = new apigateway.ApiKey(stack, 'my-api-key'); + + // WHEN + usagePlan.addApiKey(apiKey); + + // THEN + expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', { + KeyId: { + Ref: 'myapikey1B052F70', + }, + KeyType: 'API_KEY', + UsagePlanId: { + Ref: 'myusageplan23AA1E32', + }, + }, ResourcePart.Properties); }); - const apiKey: apigateway.ApiKey = new apigateway.ApiKey(stack, 'my-api-key'); - // WHEN - usagePlan.addApiKey(apiKey); + test('multiple keys', () => { + // GIVEN + const stack = new cdk.Stack(); + const usagePlan = new apigateway.UsagePlan(stack, 'my-usage-plan'); + const apiKey1 = new apigateway.ApiKey(stack, 'my-api-key-1', { + apiKeyName: 'my-api-key-1', + }); + const apiKey2 = new apigateway.ApiKey(stack, 'my-api-key-2', { + apiKeyName: 'my-api-key-2', + }); - // THEN - expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', { - KeyId: { - Ref: 'myapikey1B052F70', - }, - KeyType: 'API_KEY', - UsagePlanId: { - Ref: 'myusageplan23AA1E32', - }, - }, ResourcePart.Properties); - }); + // WHEN + usagePlan.addApiKey(apiKey1); + usagePlan.addApiKey(apiKey2); - test('UsagePlan can have multiple keys', () => { - // GIVEN - const stack = new cdk.Stack(); - const usagePlan = new apigateway.UsagePlan(stack, 'my-usage-plan'); - const apiKey1 = new apigateway.ApiKey(stack, 'my-api-key-1', { - apiKeyName: 'my-api-key-1', + // THEN + expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', { + Name: 'my-api-key-1', + }, ResourcePart.Properties); + expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', { + Name: 'my-api-key-2', + }, ResourcePart.Properties); + expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', { + KeyId: { + Ref: 'myapikey11F723FC7', + }, + }, ResourcePart.Properties); + expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', { + KeyId: { + Ref: 'myapikey2ABDEF012', + }, + }, ResourcePart.Properties); }); - const apiKey2 = new apigateway.ApiKey(stack, 'my-api-key-2', { - apiKeyName: 'my-api-key-2', + + test('overrideLogicalId', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app); + const usagePlan: apigateway.UsagePlan = new apigateway.UsagePlan(stack, 'my-usage-plan', { name: 'Basic' }); + const apiKey: apigateway.ApiKey = new apigateway.ApiKey(stack, 'my-api-key'); + + // WHEN + usagePlan.addApiKey(apiKey, { overrideLogicalId: 'mylogicalid' }); + + // THEN + const template = app.synth().getStackByName(stack.stackName).template; + const logicalIds = Object.entries(template.Resources) + .filter(([_, v]) => (v as any).Type === 'AWS::ApiGateway::UsagePlanKey') + .map(([k, _]) => k); + expect(logicalIds).toEqual(['mylogicalid']); }); - // WHEN - usagePlan.addApiKey(apiKey1); - usagePlan.addApiKey(apiKey2); + describe('future flag: @aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId', () => { + const flags = { [cxapi.APIGATEWAY_USAGEPLANKEY_ORDERINSENSITIVE_ID]: true }; - // THEN - expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', { - Name: 'my-api-key-1', - }, ResourcePart.Properties); - expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', { - Name: 'my-api-key-2', - }, ResourcePart.Properties); - expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', { - KeyId: { - Ref: 'myapikey11F723FC7', - }, - }, ResourcePart.Properties); - expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', { - KeyId: { - Ref: 'myapikey2ABDEF012', - }, - }, ResourcePart.Properties); + testFutureBehavior('UsagePlanKeys have unique logical ids', flags, cdk.App, (app) => { + // GIVEN + const stack = new cdk.Stack(app, 'my-stack'); + const usagePlan = new apigateway.UsagePlan(stack, 'my-usage-plan'); + const apiKey1 = new apigateway.ApiKey(stack, 'my-api-key-1', { + apiKeyName: 'my-api-key-1', + }); + const apiKey2 = new apigateway.ApiKey(stack, 'my-api-key-2', { + apiKeyName: 'my-api-key-2', + }); + + // WHEN + usagePlan.addApiKey(apiKey1); + usagePlan.addApiKey(apiKey2); + + // THEN + const template = app.synth().getStackByName(stack.stackName).template; + const logicalIds = Object.entries(template.Resources) + .filter(([_, v]) => (v as any).Type === 'AWS::ApiGateway::UsagePlanKey') + .map(([k, _]) => k); + + expect(logicalIds).toEqual([ + 'myusageplanUsagePlanKeyResourcemystackmyapikey1EE9AA1B359121274', + 'myusageplanUsagePlanKeyResourcemystackmyapikey2B4E8EB1456DC88E9', + ]); + }); + }); }); }); diff --git a/packages/@aws-cdk/cx-api/lib/features.ts b/packages/@aws-cdk/cx-api/lib/features.ts index 91f6039625a1b..cdb2217a16b4e 100644 --- a/packages/@aws-cdk/cx-api/lib/features.ts +++ b/packages/@aws-cdk/cx-api/lib/features.ts @@ -119,6 +119,22 @@ export const ECS_REMOVE_DEFAULT_DESIRED_COUNT = '@aws-cdk/aws-ecs-patterns:remov */ export const RDS_LOWERCASE_DB_IDENTIFIER = '@aws-cdk/aws-rds:lowercaseDbIdentifier'; +/** + * The UsagePlanKey resource connects an ApiKey with a UsagePlan. API Gateway does not allow more than one UsagePlanKey + * for any given UsagePlan and ApiKey combination. For this reason, CloudFormation cannot replace this resource without + * either the UsagePlan or ApiKey changing. + * + * The feature addition to support multiple UsagePlanKey resources - 142bd0e2 - recognized this and attempted to keep + * existing UsagePlanKey logical ids unchanged. + * However, this intentionally caused the logical id of the UsagePlanKey to be sensitive to order. That is, when + * the 'first' UsagePlanKey resource is removed, the logical id of the 'second' assumes what was originally the 'first', + * which again is disallowed. + * + * In effect, there is no way to get out of this mess in a backwards compatible way, while supporting existing stacks. + * This flag changes the logical id layout of UsagePlanKey to not be sensitive to order. + */ +export const APIGATEWAY_USAGEPLANKEY_ORDERINSENSITIVE_ID = '@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId'; + /** * This map includes context keys and values for feature flags that enable * capabilities "from the future", which we could not introduce as the default @@ -133,6 +149,7 @@ export const RDS_LOWERCASE_DB_IDENTIFIER = '@aws-cdk/aws-rds:lowercaseDbIdentifi * Tests must cover the default (disabled) case and the future (enabled) case. */ export const FUTURE_FLAGS: { [key: string]: any } = { + [APIGATEWAY_USAGEPLANKEY_ORDERINSENSITIVE_ID]: true, [ENABLE_STACK_NAME_DUPLICATES_CONTEXT]: 'true', [ENABLE_DIFF_NO_FAIL_CONTEXT]: 'true', [STACK_RELATIVE_EXPORTS_CONTEXT]: 'true', @@ -159,6 +176,7 @@ export const FUTURE_FLAGS_EXPIRED: string[] = [ * explicitly configured. */ const FUTURE_FLAGS_DEFAULTS: { [key: string]: boolean } = { + [APIGATEWAY_USAGEPLANKEY_ORDERINSENSITIVE_ID]: false, [ENABLE_STACK_NAME_DUPLICATES_CONTEXT]: false, [ENABLE_DIFF_NO_FAIL_CONTEXT]: false, [STACK_RELATIVE_EXPORTS_CONTEXT]: false, From a37f178b52a91d43b237013d7cb42c44c1774307 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christophe=20Boug=C3=A8re?= Date: Tue, 6 Apr 2021 20:36:01 +0200 Subject: [PATCH 05/25] feat(elasticloadbalancingv2): add grpc code matcher for alb (#13948) With #13570, it is now possible to set the protocol version for ALB target groups, and thus make it working for GRPC. However, it is not yet possible to define a GrpcCode for the matcher (see [CloudFormation doc](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticloadbalancingv2-targetgroup-matcher.html)). I added a `healthyGrpcCodes` working the same way as `healthyHttpCodes`. closes #13947 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-elasticloadbalancingv2/README.md | 4 ++++ .../lib/shared/base-target-group.ts | 13 ++++++++++++- .../test/alb/target-group.test.ts | 18 ++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md index b04d39e1862d7..76a6b7e1ddd5c 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md @@ -283,6 +283,10 @@ const tg = new elbv2.ApplicationTargetGroup(stack, 'TG', { port: 50051, protocol: elbv2.ApplicationProtocol.HTTP, protocolVersion: elbv2.ApplicationProtocolVersion.GRPC, + healthCheck: { + enabled: true, + healthyGrpcCodes: '0-99', + }, vpc, }); ``` 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 5e569fd8213ca..175f63ddc4d3d 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 @@ -136,6 +136,16 @@ export interface HealthCheck { */ readonly unhealthyThresholdCount?: number; + /** + * GRPC code to use when checking for a successful response from a target. + * + * You can specify values between 0 and 99. You can specify multiple values + * (for example, "0,1") or a range of values (for example, "0-5"). + * + * @default - 12 + */ + readonly healthyGrpcCodes?: string; + /** * HTTP code to use when checking for a successful response from a target. * @@ -259,7 +269,8 @@ export abstract class TargetGroupBase extends CoreConstruct implements ITargetGr healthyThresholdCount: cdk.Lazy.number({ produce: () => this.healthCheck?.healthyThresholdCount }), unhealthyThresholdCount: cdk.Lazy.number({ produce: () => this.healthCheck?.unhealthyThresholdCount }), matcher: cdk.Lazy.any({ - produce: () => this.healthCheck?.healthyHttpCodes !== undefined ? { + produce: () => this.healthCheck?.healthyHttpCodes !== undefined || this.healthCheck?.healthyGrpcCodes !== undefined ? { + grpcCode: this.healthCheck.healthyGrpcCodes, httpCode: this.healthCheck.healthyHttpCodes, } : undefined, }), 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 ea028b543096f..f4b1212942dd6 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 @@ -166,11 +166,29 @@ describe('tests', () => { new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', { vpc, protocolVersion: elbv2.ApplicationProtocolVersion.GRPC, + healthCheck: { + enabled: true, + healthyGrpcCodes: '0-99', + interval: cdk.Duration.seconds(255), + timeout: cdk.Duration.seconds(192), + healthyThresholdCount: 29, + unhealthyThresholdCount: 27, + path: '/arbitrary', + }, }); // THEN expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { ProtocolVersion: 'GRPC', + HealthCheckEnabled: true, + HealthCheckIntervalSeconds: 255, + HealthCheckPath: '/arbitrary', + HealthCheckTimeoutSeconds: 192, + HealthyThresholdCount: 29, + Matcher: { + GrpcCode: '0-99', + }, + UnhealthyThresholdCount: 27, }); }); From c62fd6b50c2d0dcddb279d4235775c9997b9cdbd Mon Sep 17 00:00:00 2001 From: Neta Nir Date: Wed, 7 Apr 2021 01:14:08 -0700 Subject: [PATCH 06/25] chore(lambda-layer-kubctl): graduate to stable (#14010) Intentionally using `chore` so it won't show up in the CHANGELOG ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/lambda-layer-kubectl/README.md | 9 +-------- packages/@aws-cdk/lambda-layer-kubectl/package.json | 4 ++-- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/@aws-cdk/lambda-layer-kubectl/README.md b/packages/@aws-cdk/lambda-layer-kubectl/README.md index d2c0343807369..3782174b1aa72 100644 --- a/packages/@aws-cdk/lambda-layer-kubectl/README.md +++ b/packages/@aws-cdk/lambda-layer-kubectl/README.md @@ -3,19 +3,12 @@ --- -![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. +![cdk-constructs: Stable](https://img.shields.io/badge/cdk--constructs-stable-success.svg?style=for-the-badge) --- - This module exports a single class called `KubectlLayer` which is a `lambda.Layer` that bundles the [`kubectl`](https://kubernetes.io/docs/reference/kubectl/kubectl/) and the [`helm`](https://helm.sh/) command line. > - Helm Version: 1.20.0 diff --git a/packages/@aws-cdk/lambda-layer-kubectl/package.json b/packages/@aws-cdk/lambda-layer-kubectl/package.json index fd2f129bb6999..be1e90d54d39c 100644 --- a/packages/@aws-cdk/lambda-layer-kubectl/package.json +++ b/packages/@aws-cdk/lambda-layer-kubectl/package.json @@ -90,8 +90,8 @@ "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, - "stability": "experimental", - "maturity": "experimental", + "stability": "stable", + "maturity": "stable", "awscdkio": { "announce": false }, From 1f99e52b92bff56204785de6805afed868657472 Mon Sep 17 00:00:00 2001 From: Neta Nir Date: Wed, 7 Apr 2021 01:56:59 -0700 Subject: [PATCH 07/25] chore(lambda-layer-awscli): graduate to stable (#14012) Intentionally setting to `chore` so it will not show up in the CHANGELOG ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/lambda-layer-awscli/README.md | 8 +------- packages/@aws-cdk/lambda-layer-awscli/package.json | 4 ++-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/@aws-cdk/lambda-layer-awscli/README.md b/packages/@aws-cdk/lambda-layer-awscli/README.md index ae6460fae09d3..baad6362f5e21 100644 --- a/packages/@aws-cdk/lambda-layer-awscli/README.md +++ b/packages/@aws-cdk/lambda-layer-awscli/README.md @@ -3,13 +3,7 @@ --- -![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. +![cdk-constructs: Stable](https://img.shields.io/badge/cdk--constructs-stable-success.svg?style=for-the-badge) --- diff --git a/packages/@aws-cdk/lambda-layer-awscli/package.json b/packages/@aws-cdk/lambda-layer-awscli/package.json index 46262f6008feb..501fbf68d68b0 100644 --- a/packages/@aws-cdk/lambda-layer-awscli/package.json +++ b/packages/@aws-cdk/lambda-layer-awscli/package.json @@ -84,8 +84,8 @@ "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, - "stability": "experimental", - "maturity": "experimental", + "stability": "stable", + "maturity": "stable", "awscdkio": { "announce": false }, From 0d2755b97486e4222d1f3b020b8126fefeda20d0 Mon Sep 17 00:00:00 2001 From: Neta Nir Date: Wed, 7 Apr 2021 02:36:56 -0700 Subject: [PATCH 08/25] =?UTF-8?q?feat(region-info):=20graduate=20to=20stab?= =?UTF-8?q?le=20=F0=9F=9A=80=20=20(#14013)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/region-info/README.md | 8 +------- packages/@aws-cdk/region-info/package.json | 4 ++-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/@aws-cdk/region-info/README.md b/packages/@aws-cdk/region-info/README.md index 07119849babb4..0f1186318ee49 100644 --- a/packages/@aws-cdk/region-info/README.md +++ b/packages/@aws-cdk/region-info/README.md @@ -3,13 +3,7 @@ --- -![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. +![cdk-constructs: Stable](https://img.shields.io/badge/cdk--constructs-stable-success.svg?style=for-the-badge) --- diff --git a/packages/@aws-cdk/region-info/package.json b/packages/@aws-cdk/region-info/package.json index fe5a7e1acf1ec..1bbd1367662b1 100644 --- a/packages/@aws-cdk/region-info/package.json +++ b/packages/@aws-cdk/region-info/package.json @@ -72,8 +72,8 @@ "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, - "stability": "experimental", - "maturity": "experimental", + "stability": "stable", + "maturity": "stable", "awslint": { "exclude": [ "docs-public-apis:@aws-cdk/region-info.Fact.regions", From b1ecd3d49d7ebf97a54a80d06779ef0f0b113c16 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 7 Apr 2021 12:20:55 +0200 Subject: [PATCH 09/25] chore: introduce `@aws-cdk/assert-internal`, `aws-cdk-migration` (#13700) `@aws-cdk/assert` will be a publish-only package: we no longer use it to test our own packages. Instead, our own packages are tested using `@aws-cdk/assert-internal`. `@aws-cdk/assert` will be built by cloning the `assert-internal` package, and made to depend on either `@aws-cdk/core` on the v1 branch, and `aws-cdk-lib` on the v2 branch. ---- The `rewrite-imports` script has been moved to a top-level tools package, `aws-cdk-migration`. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../ecs-service-extensions/package.json | 4 +- .../test/test.appmesh.ts | 2 +- .../test/test.assign-public-ip.ts | 2 +- .../test/test.cloudwatch-agent.ts | 2 +- .../test/test.environment.ts | 2 +- .../test/test.firelens.ts | 2 +- .../test/test.http-load-balancer.ts | 2 +- .../test/test.scale-on-cpu-utilization.ts | 2 +- .../test/test.service.ts | 2 +- .../ecs-service-extensions/test/test.xray.ts | 2 +- packages/@aws-cdk/alexa-ask/package.json | 4 +- packages/@aws-cdk/alexa-ask/test/ask.test.ts | 2 +- packages/@aws-cdk/app-delivery/package.json | 4 +- .../test/test.pipeline-deploy-stack-action.ts | 2 +- .../@aws-cdk/assert-internal/.eslintrc.js | 3 + packages/@aws-cdk/assert-internal/.gitignore | 16 + packages/@aws-cdk/assert-internal/.npmignore | 22 + packages/@aws-cdk/assert-internal/LICENSE | 201 ++++++++ packages/@aws-cdk/assert-internal/NOTICE | 2 + packages/@aws-cdk/assert-internal/README.md | 227 +++++++++ .../@aws-cdk/assert-internal/jest.config.js | 10 + packages/@aws-cdk/assert-internal/jest.ts | 107 +++++ .../@aws-cdk/assert-internal/lib/assertion.ts | 40 ++ .../lib/assertions/and-assertion.ts | 19 + .../lib/assertions/count-resources.ts | 58 +++ .../assert-internal/lib/assertions/exist.ts | 18 + .../lib/assertions/have-output.ts | 116 +++++ .../lib/assertions/have-resource-matchers.ts | 430 ++++++++++++++++++ .../lib/assertions/have-resource.ts | 163 +++++++ .../lib/assertions/have-type.ts | 21 + .../lib/assertions/match-template.ts | 96 ++++ .../lib/assertions/negated-assertion.ts | 16 + .../lib/canonicalize-assets.ts | 71 +++ .../@aws-cdk/assert-internal/lib/expect.ts | 12 + .../@aws-cdk/assert-internal/lib/index.ts | 15 + .../@aws-cdk/assert-internal/lib/inspector.ts | 74 +++ .../assert-internal/lib/synth-utils.ts | 87 ++++ .../@aws-cdk/assert-internal/package.json | 67 +++ .../assert-internal/test/assertions.test.ts | 349 ++++++++++++++ .../test/canonicalize-assets.test.ts | 135 ++++++ .../assert-internal/test/have-output.test.ts | 202 ++++++++ .../test/have-resource.test.ts | 279 ++++++++++++ .../assert-internal/test/synth-utils.test.ts | 14 + .../@aws-cdk/assert-internal/tsconfig.json | 30 ++ packages/@aws-cdk/assert/.gitignore | 17 +- packages/@aws-cdk/assert/BUILD.md | 15 + packages/@aws-cdk/assert/clone.sh | 20 + packages/@aws-cdk/assert/jest.config.js | 2 +- packages/@aws-cdk/assert/package.json | 24 +- packages/@aws-cdk/assert/tsconfig.json | 16 +- packages/@aws-cdk/assets/package.json | 4 +- .../@aws-cdk/aws-accessanalyzer/package.json | 4 +- .../test/accessanalyzer.test.ts | 2 +- packages/@aws-cdk/aws-acmpca/package.json | 4 +- .../@aws-cdk/aws-acmpca/test/acmpca.test.ts | 2 +- packages/@aws-cdk/aws-amazonmq/package.json | 4 +- .../aws-amazonmq/test/amazonmq.test.ts | 2 +- packages/@aws-cdk/aws-amplify/package.json | 4 +- .../@aws-cdk/aws-amplify/test/app.test.ts | 2 +- .../@aws-cdk/aws-amplify/test/branch.test.ts | 2 +- .../@aws-cdk/aws-amplify/test/domain.test.ts | 2 +- packages/@aws-cdk/aws-apigateway/package.json | 4 +- .../aws-apigateway/test/access-log.test.ts | 2 +- .../test/api-definition.test.ts | 2 +- .../aws-apigateway/test/api-key.test.ts | 4 +- .../test/authorizers/cognito.test.ts | 2 +- .../test/authorizers/lambda.test.ts | 4 +- .../test/base-path-mapping.test.ts | 2 +- .../@aws-cdk/aws-apigateway/test/cors.test.ts | 2 +- .../aws-apigateway/test/deployment.test.ts | 4 +- .../aws-apigateway/test/domains.test.ts | 4 +- .../test/gateway-response.test.ts | 4 +- .../@aws-cdk/aws-apigateway/test/http.test.ts | 2 +- .../aws-apigateway/test/integration.test.ts | 4 +- .../test/integrations/lambda.test.ts | 2 +- .../aws-apigateway/test/lambda-api.test.ts | 2 +- .../aws-apigateway/test/method.test.ts | 4 +- .../aws-apigateway/test/model.test.ts | 2 +- .../test/requestvalidator.test.ts | 2 +- .../aws-apigateway/test/resource.test.ts | 2 +- .../aws-apigateway/test/restapi.test.ts | 4 +- .../aws-apigateway/test/stage.test.ts | 2 +- .../aws-apigateway/test/usage-plan.test.ts | 4 +- .../@aws-cdk/aws-apigateway/test/util.test.ts | 2 +- .../aws-apigateway/test/vpc-link.test.ts | 2 +- .../aws-apigatewayv2-authorizers/package.json | 4 +- .../test/http/jwt.test.ts | 2 +- .../test/http/user-pool.test.ts | 2 +- .../package.json | 4 +- .../test/http/alb.test.ts | 2 +- .../test/http/http-proxy.test.ts | 2 +- .../test/http/lambda.test.ts | 2 +- .../test/http/nlb.test.ts | 2 +- .../test/http/private/integration.test.ts | 2 +- .../test/http/service-discovery.test.ts | 2 +- .../test/websocket/lambda.test.ts | 2 +- .../@aws-cdk/aws-apigatewayv2/package.json | 4 +- .../test/common/api-mapping.test.ts | 2 +- .../aws-apigatewayv2/test/http/api.test.ts | 4 +- .../test/http/authorizer.test.ts | 2 +- .../test/http/domain-name.test.ts | 4 +- .../aws-apigatewayv2/test/http/route.test.ts | 2 +- .../aws-apigatewayv2/test/http/stage.test.ts | 2 +- .../test/http/vpc-link.test.ts | 2 +- .../test/websocket/api.test.ts | 2 +- .../test/websocket/route.test.ts | 2 +- .../test/websocket/stage.test.ts | 2 +- packages/@aws-cdk/aws-appconfig/package.json | 4 +- .../aws-appconfig/test/appconfig.test.ts | 2 +- packages/@aws-cdk/aws-appflow/package.json | 4 +- .../@aws-cdk/aws-appflow/test/appflow.test.ts | 2 +- .../aws-applicationautoscaling/package.json | 4 +- .../test/test.scalable-target.ts | 2 +- .../test/test.step-scaling-policy.ts | 2 +- .../test/test.target-tracking.ts | 2 +- .../aws-applicationinsights/package.json | 4 +- .../test/applicationinsights.test.ts | 2 +- packages/@aws-cdk/aws-appmesh/package.json | 4 +- .../aws-appmesh/test/test.gateway-route.ts | 2 +- .../@aws-cdk/aws-appmesh/test/test.mesh.ts | 2 +- .../@aws-cdk/aws-appmesh/test/test.route.ts | 2 +- .../aws-appmesh/test/test.virtual-gateway.ts | 2 +- .../aws-appmesh/test/test.virtual-node.ts | 2 +- .../aws-appmesh/test/test.virtual-router.ts | 2 +- .../aws-appmesh/test/test.virtual-service.ts | 2 +- packages/@aws-cdk/aws-appstream/package.json | 4 +- .../aws-appstream/test/appstream.test.ts | 2 +- packages/@aws-cdk/aws-appsync/package.json | 4 +- .../aws-appsync/test/appsync-auth.test.ts | 2 +- .../test/appsync-code-first.test.ts | 2 +- .../test/appsync-directives.test.ts | 2 +- .../aws-appsync/test/appsync-dynamodb.test.ts | 2 +- .../test/appsync-enum-type.test.ts | 2 +- .../aws-appsync/test/appsync-grant.test.ts | 2 +- .../aws-appsync/test/appsync-http.test.ts | 2 +- .../test/appsync-input-types.test.ts | 2 +- .../test/appsync-interface-type.test.ts | 2 +- .../aws-appsync/test/appsync-lambda.test.ts | 2 +- .../aws-appsync/test/appsync-none.test.ts | 2 +- .../test/appsync-object-type.test.ts | 2 +- .../aws-appsync/test/appsync-rds.test.ts | 2 +- .../test/appsync-scalar-type.test.ts | 2 +- .../aws-appsync/test/appsync-schema.test.ts | 2 +- .../test/appsync-union-types.test.ts | 2 +- .../@aws-cdk/aws-appsync/test/appsync.test.ts | 2 +- packages/@aws-cdk/aws-athena/package.json | 4 +- .../@aws-cdk/aws-athena/test/athena.test.ts | 4 +- .../@aws-cdk/aws-auditmanager/package.json | 4 +- .../test/auditmanager.test.ts | 2 +- .../aws-autoscaling-common/package.json | 4 +- .../aws-autoscaling-hooktargets/package.json | 4 +- .../test/hooks.test.ts | 4 +- .../@aws-cdk/aws-autoscaling/package.json | 4 +- .../test/auto-scaling-group.test.ts | 2 +- .../aws-autoscaling/test/cfn-init.test.ts | 2 +- .../test/lifecyclehooks.test.ts | 2 +- .../aws-autoscaling/test/scaling.test.ts | 2 +- .../test/scheduled-action.test.ts | 2 +- .../aws-autoscalingplans/package.json | 4 +- .../test/autoscalingplans.test.ts | 2 +- packages/@aws-cdk/aws-backup/package.json | 4 +- .../@aws-cdk/aws-backup/test/plan.test.ts | 2 +- .../aws-backup/test/selection.test.ts | 2 +- .../@aws-cdk/aws-backup/test/vault.test.ts | 2 +- packages/@aws-cdk/aws-batch/package.json | 4 +- .../@aws-cdk/aws-batch/test/batch.test.ts | 2 +- .../test/compute-environment.test.ts | 4 +- .../aws-batch/test/job-definition.test.ts | 4 +- .../@aws-cdk/aws-batch/test/job-queue.test.ts | 4 +- packages/@aws-cdk/aws-budgets/package.json | 4 +- .../@aws-cdk/aws-budgets/test/budgets.test.ts | 2 +- packages/@aws-cdk/aws-cassandra/package.json | 4 +- .../aws-cassandra/test/cassandra.test.ts | 2 +- packages/@aws-cdk/aws-ce/package.json | 4 +- packages/@aws-cdk/aws-ce/test/ce.test.ts | 2 +- .../aws-certificatemanager/package.json | 4 +- .../test/certificate.test.ts | 2 +- .../test/dns-validated-certificate.test.ts | 4 +- .../aws-certificatemanager/test/util.test.ts | 2 +- packages/@aws-cdk/aws-chatbot/package.json | 4 +- .../test/slack-channel-configuration.test.ts | 4 +- packages/@aws-cdk/aws-cloud9/package.json | 4 +- .../test/cloud9.environment.test.ts | 2 +- .../@aws-cdk/aws-cloud9/test/cloud9.test.ts | 2 +- .../@aws-cdk/aws-cloudformation/package.json | 4 +- .../aws-cloudformation/test/test.deps.ts | 2 +- .../test/test.nested-stack.ts | 2 +- .../aws-cloudformation/test/test.resource.ts | 2 +- .../aws-cloudfront-origins/package.json | 4 +- .../test/http-origin.test.ts | 2 +- .../test/load-balancer-origin.test.ts | 2 +- .../test/origin-group.test.ts | 2 +- .../test/s3-origin.test.ts | 2 +- packages/@aws-cdk/aws-cloudfront/package.json | 4 +- .../aws-cloudfront/test/cache-policy.test.ts | 2 +- .../aws-cloudfront/test/distribution.test.ts | 4 +- .../test/experimental/edge-function.test.ts | 2 +- .../test/geo-restriction.test.ts | 2 +- .../aws-cloudfront/test/key-group.test.ts | 4 +- .../@aws-cdk/aws-cloudfront/test/oai.test.ts | 2 +- .../aws-cloudfront/test/origin-groups.test.ts | 2 +- .../test/origin-request-policy.test.ts | 2 +- .../aws-cloudfront/test/origin.test.ts | 2 +- .../test/private/cache-behavior.test.ts | 2 +- .../aws-cloudfront/test/public-key.test.ts | 4 +- .../test/web-distribution.test.ts | 2 +- packages/@aws-cdk/aws-cloudtrail/package.json | 4 +- .../aws-cloudtrail/test/cloudtrail.test.ts | 4 +- .../aws-cloudwatch-actions/package.json | 4 +- .../test/appscaling.test.ts | 2 +- .../aws-cloudwatch-actions/test/ec2.test.ts | 2 +- .../test/scaling.test.ts | 2 +- .../aws-cloudwatch-actions/test/sns.test.ts | 2 +- packages/@aws-cdk/aws-cloudwatch/package.json | 4 +- .../aws-cloudwatch/test/test.alarm.ts | 2 +- .../test/test.composite-alarm.ts | 2 +- .../test/test.cross-environment.ts | 2 +- .../aws-cloudwatch/test/test.dashboard.ts | 2 +- .../aws-cloudwatch/test/test.metric-math.ts | 2 +- .../aws-cloudwatch/test/test.metrics.ts | 2 +- .../@aws-cdk/aws-codeartifact/package.json | 4 +- .../test/codeartifact.test.ts | 2 +- packages/@aws-cdk/aws-codebuild/package.json | 4 +- .../aws-codebuild/test/test.codebuild.ts | 2 +- .../test/test.linux-gpu-build-image.ts | 2 +- .../aws-codebuild/test/test.project.ts | 2 +- .../aws-codebuild/test/test.report-group.ts | 2 +- .../test/test.untrusted-code-boundary.ts | 2 +- packages/@aws-cdk/aws-codecommit/package.json | 4 +- .../aws-codecommit/test/test.codecommit.ts | 2 +- packages/@aws-cdk/aws-codedeploy/package.json | 4 +- .../test/ecs/test.application.ts | 2 +- .../test/lambda/test.application.ts | 2 +- .../lambda/test.custom-deployment-config.ts | 2 +- .../test/lambda/test.deployment-group.ts | 2 +- .../test/server/test.deployment-config.ts | 2 +- .../test/server/test.deployment-group.ts | 2 +- .../aws-codeguruprofiler/package.json | 4 +- .../test/codeguruprofiler.test.ts | 2 +- .../test/profiling-group.test.ts | 2 +- .../aws-codegurureviewer/package.json | 4 +- .../test/codegurureviewer.test.ts | 2 +- .../aws-codepipeline-actions/package.json | 4 +- .../bitbucket/bitbucket-source-action.test.ts | 2 +- .../cloudformation-pipeline-actions.test.ts | 2 +- .../test/codebuild/codebuild-action.test.ts | 2 +- .../codecommit-source-action.test.ts | 2 +- .../test/codedeploy/ecs-deploy-action.test.ts | 2 +- .../test/ecr/ecr-source-action.test.ts | 2 +- .../test/ecs/ecs-deploy-action.test.ts | 2 +- .../test/github/github-source-action.test.ts | 2 +- .../test/lambda/lambda-invoke-action.test.ts | 2 +- .../test/manual-approval.test.ts | 2 +- .../test/pipeline.test.ts | 2 +- .../test/s3/s3-deploy-action.test.ts | 2 +- .../test/s3/s3-source-action.test.ts | 2 +- .../servicecatalog-action.test.ts | 2 +- .../stepfunctions-invoke-actions.test.ts | 2 +- .../@aws-cdk/aws-codepipeline/package.json | 4 +- .../aws-codepipeline/test/action.test.ts | 2 +- .../aws-codepipeline/test/artifacts.test.ts | 2 +- .../aws-codepipeline/test/cross-env.test.ts | 2 +- .../aws-codepipeline/test/pipeline.test.ts | 4 +- .../aws-codepipeline/test/stages.test.ts | 2 +- .../aws-codepipeline/test/variables.test.ts | 2 +- packages/@aws-cdk/aws-codestar/package.json | 4 +- .../aws-codestar/test/codestar.test.ts | 2 +- .../aws-codestarconnections/package.json | 4 +- .../test/codestarconnections.test.ts | 2 +- .../aws-codestarnotifications/package.json | 4 +- .../test/codestarnotifications.test.ts | 2 +- packages/@aws-cdk/aws-cognito/package.json | 4 +- .../aws-cognito/test/user-pool-attr.test.ts | 2 +- .../aws-cognito/test/user-pool-client.test.ts | 4 +- .../aws-cognito/test/user-pool-domain.test.ts | 2 +- .../test/user-pool-idps/amazon.test.ts | 2 +- .../aws-cognito/test/user-pool-idps/apple.ts | 2 +- .../test/user-pool-idps/base.test.ts | 2 +- .../test/user-pool-idps/facebook.test.ts | 2 +- .../test/user-pool-idps/google.test.ts | 2 +- .../test/user-pool-resource-server.test.ts | 2 +- .../aws-cognito/test/user-pool.test.ts | 4 +- packages/@aws-cdk/aws-config/package.json | 4 +- .../aws-config/test/test.managed-rules.ts | 2 +- .../@aws-cdk/aws-config/test/test.rule.ts | 2 +- packages/@aws-cdk/aws-databrew/package.json | 4 +- .../aws-databrew/test/databrew.test.ts | 2 +- .../@aws-cdk/aws-datapipeline/package.json | 4 +- .../test/datapipeline.test.ts | 2 +- packages/@aws-cdk/aws-datasync/package.json | 4 +- .../aws-datasync/test/datasync.test.ts | 2 +- packages/@aws-cdk/aws-dax/package.json | 4 +- packages/@aws-cdk/aws-dax/test/dax.test.ts | 2 +- packages/@aws-cdk/aws-detective/package.json | 4 +- .../aws-detective/test/detective.test.ts | 2 +- packages/@aws-cdk/aws-devopsguru/package.json | 4 +- .../aws-devopsguru/test/devopsguru.test.ts | 2 +- .../aws-directoryservice/package.json | 4 +- .../test/directoryservice.test.ts | 2 +- packages/@aws-cdk/aws-dlm/package.json | 4 +- packages/@aws-cdk/aws-dlm/test/dlm.test.ts | 2 +- packages/@aws-cdk/aws-dms/package.json | 4 +- packages/@aws-cdk/aws-dms/test/dms.test.ts | 2 +- packages/@aws-cdk/aws-docdb/package.json | 4 +- .../@aws-cdk/aws-docdb/test/cluster.test.ts | 2 +- .../@aws-cdk/aws-docdb/test/instance.test.ts | 2 +- .../aws-docdb/test/parameter-group.test.ts | 2 +- .../@aws-cdk/aws-dynamodb-global/package.json | 4 +- .../test/test.dynamodb.global.ts | 2 +- packages/@aws-cdk/aws-dynamodb/package.json | 4 +- .../aws-dynamodb/test/dynamodb.test.ts | 4 +- packages/@aws-cdk/aws-ec2/package.json | 4 +- .../aws-ec2/test/bastion-host.test.ts | 2 +- .../@aws-cdk/aws-ec2/test/cfn-init.test.ts | 4 +- .../aws-ec2/test/client-vpn-endpoint.test.ts | 4 +- .../@aws-cdk/aws-ec2/test/connections.test.ts | 2 +- .../@aws-cdk/aws-ec2/test/instance.test.ts | 4 +- .../aws-ec2/test/launch-template.test.ts | 2 +- .../aws-ec2/test/machine-image.test.ts | 2 +- .../aws-ec2/test/security-group.test.ts | 2 +- packages/@aws-cdk/aws-ec2/test/volume.test.ts | 2 +- .../aws-ec2/test/vpc-endpoint-service.test.ts | 2 +- .../aws-ec2/test/vpc-endpoint.test.ts | 2 +- .../aws-ec2/test/vpc-flow-logs.test.ts | 2 +- packages/@aws-cdk/aws-ec2/test/vpc.test.ts | 2 +- packages/@aws-cdk/aws-ec2/test/vpn.test.ts | 2 +- packages/@aws-cdk/aws-ecr-assets/package.json | 4 +- .../aws-ecr-assets/test/image-asset.test.ts | 2 +- packages/@aws-cdk/aws-ecr/package.json | 4 +- .../@aws-cdk/aws-ecr/test/test.auth-token.ts | 2 +- .../@aws-cdk/aws-ecr/test/test.repository.ts | 2 +- .../@aws-cdk/aws-ecs-patterns/package.json | 4 +- .../aws-ecs-patterns/test/ec2/test.l3s-v2.ts | 2 +- .../aws-ecs-patterns/test/ec2/test.l3s.ts | 2 +- .../ec2/test.queue-processing-ecs-service.ts | 2 +- .../test/ec2/test.scheduled-ecs-task.ts | 2 +- .../test.load-balanced-fargate-service-v2.ts | 2 +- .../test.load-balanced-fargate-service.ts | 2 +- .../test.queue-processing-fargate-service.ts | 2 +- .../fargate/test.scheduled-fargate-task.ts | 2 +- packages/@aws-cdk/aws-ecs/package.json | 4 +- .../test/app-mesh-proxy-configuration.test.ts | 2 +- .../aws-ecs/test/aws-log-driver.test.ts | 2 +- .../aws-ecs/test/container-definition.test.ts | 4 +- .../aws-ecs/test/ec2/cross-stack.test.ts | 2 +- .../aws-ecs/test/ec2/ec2-service.test.ts | 2 +- .../test/ec2/ec2-task-definition.test.ts | 2 +- .../@aws-cdk/aws-ecs/test/ecs-cluster.test.ts | 2 +- .../test/fargate/fargate-service.test.ts | 2 +- .../fargate/fargate-task-definition.test.ts | 2 +- .../aws-ecs/test/firelens-log-driver.test.ts | 2 +- .../aws-ecs/test/fluentd-log-driver.test.ts | 2 +- .../aws-ecs/test/gelf-log-driver.test.ts | 2 +- .../tag-parameter-container-image.test.ts | 2 +- .../aws-ecs/test/journald-log-driver.test.ts | 2 +- .../aws-ecs/test/json-file-log-driver.test.ts | 2 +- .../aws-ecs/test/splunk-log-driver.test.ts | 2 +- .../aws-ecs/test/syslog-log-driver.test.ts | 2 +- .../aws-ecs/test/task-definition.test.ts | 2 +- packages/@aws-cdk/aws-efs/package.json | 4 +- .../aws-efs/test/access-point.test.ts | 2 +- .../aws-efs/test/efs-file-system.test.ts | 2 +- packages/@aws-cdk/aws-eks-legacy/package.json | 4 +- .../aws-eks-legacy/test/test.awsauth.ts | 2 +- .../aws-eks-legacy/test/test.cluster.ts | 2 +- .../aws-eks-legacy/test/test.helm-chart.ts | 2 +- .../aws-eks-legacy/test/test.manifest.ts | 2 +- packages/@aws-cdk/aws-eks/package.json | 4 +- .../@aws-cdk/aws-eks/test/test.awsauth.ts | 2 +- .../@aws-cdk/aws-eks/test/test.cluster.ts | 2 +- .../@aws-cdk/aws-eks/test/test.fargate.ts | 2 +- .../@aws-cdk/aws-eks/test/test.helm-chart.ts | 2 +- .../aws-eks/test/test.k8s-manifest.ts | 2 +- .../aws-eks/test/test.k8s-object-value.ts | 2 +- .../@aws-cdk/aws-eks/test/test.k8s-patch.ts | 2 +- .../@aws-cdk/aws-eks/test/test.nodegroup.ts | 2 +- .../aws-eks/test/test.service-account.ts | 2 +- .../@aws-cdk/aws-elasticache/package.json | 4 +- .../aws-elasticache/test/elasticache.test.ts | 2 +- .../aws-elasticbeanstalk/package.json | 4 +- .../test/elasticbeanstalk.test.ts | 2 +- .../aws-elasticloadbalancing/package.json | 4 +- .../test/loadbalancer.test.ts | 2 +- .../package.json | 4 +- .../test/cognito.test.ts | 2 +- .../package.json | 4 +- .../test/instance-target.test.ts | 2 +- .../test/ip-target.test.ts | 2 +- .../test/lambda-target.test.ts | 2 +- .../aws-elasticloadbalancingv2/package.json | 4 +- .../test/alb/actions.test.ts | 2 +- .../test/alb/conditions.test.ts | 2 +- .../test/alb/listener.test.ts | 4 +- .../test/alb/load-balancer.test.ts | 4 +- .../test/alb/security-group.test.ts | 2 +- .../test/alb/target-group.test.ts | 2 +- .../test/nlb/actions.test.ts | 2 +- .../test/nlb/listener.test.ts | 4 +- .../test/nlb/load-balancer.test.ts | 4 +- .../test/nlb/target-group.test.ts | 2 +- .../@aws-cdk/aws-elasticsearch/package.json | 4 +- .../aws-elasticsearch/test/domain.test.ts | 4 +- .../test/elasticsearch-access-policy.test.ts | 2 +- .../test/log-group-resource-policy.test.ts | 2 +- packages/@aws-cdk/aws-emr/package.json | 4 +- packages/@aws-cdk/aws-emr/test/emr.test.ts | 2 +- .../@aws-cdk/aws-emrcontainers/package.json | 4 +- .../test/emrcontainers.test.ts | 2 +- .../@aws-cdk/aws-events-targets/package.json | 4 +- .../test/aws-api/aws-api.test.ts | 2 +- .../test/batch/batch.test.ts | 2 +- .../test/codebuild/codebuild.test.ts | 2 +- .../test/codepipeline/pipeline.test.ts | 2 +- .../test/ecs/event-rule-target.test.ts | 2 +- .../test/event-bus/event-rule-target.test.ts | 2 +- .../kinesis-firehose-stream.test.ts | 2 +- .../test/kinesis/kinesis-stream.test.ts | 2 +- .../test/lambda/lambda.test.ts | 2 +- .../logs/log-group-resource-policy.test.ts | 2 +- .../test/logs/log-group.test.ts | 2 +- .../aws-events-targets/test/sns/sns.test.ts | 2 +- .../aws-events-targets/test/sqs/sqs.test.ts | 2 +- .../test/stepfunctions/statemachine.test.ts | 2 +- packages/@aws-cdk/aws-events/package.json | 4 +- .../@aws-cdk/aws-events/test/test.archive.ts | 2 +- .../aws-events/test/test.event-bus.ts | 2 +- .../@aws-cdk/aws-events/test/test.input.ts | 2 +- .../@aws-cdk/aws-events/test/test.rule.ts | 2 +- .../@aws-cdk/aws-eventschemas/package.json | 4 +- .../test/eventschemas.test.ts | 2 +- packages/@aws-cdk/aws-fis/package.json | 4 +- packages/@aws-cdk/aws-fis/test/fis.test.ts | 2 +- packages/@aws-cdk/aws-fms/package.json | 4 +- packages/@aws-cdk/aws-fms/test/fms.test.ts | 2 +- packages/@aws-cdk/aws-fsx/package.json | 4 +- .../aws-fsx/test/lustre-file-system.test.ts | 2 +- packages/@aws-cdk/aws-gamelift/package.json | 4 +- .../aws-gamelift/test/gamelift.test.ts | 2 +- .../package.json | 2 +- .../test/endpoints.test.ts | 4 +- .../aws-globalaccelerator/package.json | 5 +- .../globalaccelerator-security-group.test.ts | 2 +- .../test/globalaccelerator.test.ts | 2 +- packages/@aws-cdk/aws-glue/package.json | 4 +- .../@aws-cdk/aws-glue/test/connection.test.ts | 4 +- .../@aws-cdk/aws-glue/test/database.test.ts | 4 +- .../@aws-cdk/aws-glue/test/schema.test.ts | 2 +- .../test/security-configuration.test.ts | 4 +- packages/@aws-cdk/aws-glue/test/table.test.ts | 4 +- packages/@aws-cdk/aws-greengrass/package.json | 4 +- .../aws-greengrass/test/greengrass.test.ts | 2 +- .../@aws-cdk/aws-greengrassv2/package.json | 4 +- .../aws-greengrassv2/test/greengrass.test.ts | 2 +- packages/@aws-cdk/aws-guardduty/package.json | 4 +- .../aws-guardduty/test/guardduty.test.ts | 2 +- packages/@aws-cdk/aws-iam/package.json | 4 +- .../test/auto-cross-stack-refs.test.ts | 4 +- .../aws-iam/test/cross-account.test.ts | 2 +- .../aws-iam/test/escape-hatch.test.ts | 2 +- packages/@aws-cdk/aws-iam/test/grant.test.ts | 4 +- packages/@aws-cdk/aws-iam/test/group.test.ts | 2 +- .../aws-iam/test/immutable-role.test.ts | 2 +- .../@aws-cdk/aws-iam/test/lazy-role.test.ts | 2 +- .../aws-iam/test/managed-policy.test.ts | 2 +- .../aws-iam/test/oidc-provider.test.ts | 2 +- .../aws-iam/test/permissions-boundary.test.ts | 4 +- .../aws-iam/test/policy-document.test.ts | 2 +- .../aws-iam/test/policy-statement.test.ts | 2 +- packages/@aws-cdk/aws-iam/test/policy.test.ts | 4 +- .../@aws-cdk/aws-iam/test/principals.test.ts | 2 +- .../aws-iam/test/role.from-role-arn.test.ts | 2 +- packages/@aws-cdk/aws-iam/test/role.test.ts | 2 +- .../aws-iam/test/saml-provider.test.ts | 2 +- packages/@aws-cdk/aws-iam/test/user.test.ts | 2 +- .../@aws-cdk/aws-imagebuilder/package.json | 4 +- .../test/imagebuilder.test.ts | 2 +- packages/@aws-cdk/aws-inspector/package.json | 4 +- .../aws-inspector/test/inspector.test.ts | 2 +- packages/@aws-cdk/aws-iot/package.json | 4 +- packages/@aws-cdk/aws-iot/test/iot.test.ts | 2 +- packages/@aws-cdk/aws-iot1click/package.json | 4 +- .../aws-iot1click/test/iot1click.test.ts | 2 +- .../@aws-cdk/aws-iotanalytics/package.json | 4 +- .../test/iotanalytics.test.ts | 2 +- packages/@aws-cdk/aws-iotevents/package.json | 4 +- .../aws-iotevents/test/iotevents.test.ts | 2 +- .../@aws-cdk/aws-iotsitewise/package.json | 4 +- .../aws-iotsitewise/test/iotsitewise.test.ts | 2 +- .../@aws-cdk/aws-iotthingsgraph/package.json | 4 +- .../test/iotthingsgraph.test.ts | 2 +- .../@aws-cdk/aws-iotwireless/package.json | 4 +- .../aws-iotwireless/test/iotwireless.test.ts | 2 +- packages/@aws-cdk/aws-ivs/package.json | 4 +- packages/@aws-cdk/aws-ivs/test/ivs.test.ts | 4 +- packages/@aws-cdk/aws-kendra/package.json | 4 +- .../@aws-cdk/aws-kendra/test/kendra.test.ts | 2 +- packages/@aws-cdk/aws-kinesis/package.json | 4 +- .../@aws-cdk/aws-kinesis/test/stream.test.ts | 4 +- .../aws-kinesisanalytics-flink/package.json | 4 +- .../test/application.test.ts | 4 +- .../aws-kinesisanalytics/package.json | 4 +- .../test/kinesisanalytics.test.ts | 2 +- .../@aws-cdk/aws-kinesisfirehose/package.json | 4 +- .../test/kinesisfirehose.test.ts | 2 +- packages/@aws-cdk/aws-kms/package.json | 4 +- packages/@aws-cdk/aws-kms/test/alias.test.ts | 2 +- packages/@aws-cdk/aws-kms/test/key.test.ts | 4 +- .../test/via-service-principal.test.ts | 2 +- .../@aws-cdk/aws-lakeformation/package.json | 4 +- .../test/lakeformation.test.ts | 2 +- .../aws-lambda-destinations/package.json | 4 +- .../test/destinations.test.ts | 2 +- .../aws-lambda-event-sources/package.json | 4 +- .../aws-lambda-event-sources/test/test.api.ts | 2 +- .../test/test.dynamo.ts | 2 +- .../test/test.kafka.ts | 2 +- .../test/test.kinesis.ts | 2 +- .../aws-lambda-event-sources/test/test.s3.ts | 2 +- .../aws-lambda-event-sources/test/test.sns.ts | 2 +- .../aws-lambda-event-sources/test/test.sqs.ts | 2 +- .../@aws-cdk/aws-lambda-nodejs/package.json | 4 +- .../aws-lambda-nodejs/test/function.test.ts | 4 +- .../@aws-cdk/aws-lambda-python/package.json | 4 +- .../aws-lambda-python/test/function.test.ts | 2 +- .../aws-lambda-python/test/layer.test.ts | 2 +- packages/@aws-cdk/aws-lambda/package.json | 4 +- .../@aws-cdk/aws-lambda/test/alias.test.ts | 4 +- .../test/code-signing-config.test.ts | 2 +- .../@aws-cdk/aws-lambda/test/code.test.ts | 4 +- .../test/event-source-mapping.test.ts | 2 +- .../aws-lambda/test/function-hash.test.ts | 2 +- .../@aws-cdk/aws-lambda/test/function.test.ts | 4 +- .../aws-lambda/test/lambda-version.test.ts | 2 +- .../@aws-cdk/aws-lambda/test/layers.test.ts | 4 +- .../@aws-cdk/aws-lambda/test/runtime.test.ts | 2 +- .../aws-lambda/test/singleton-lambda.test.ts | 4 +- .../aws-lambda/test/vpc-lambda.test.ts | 2 +- .../@aws-cdk/aws-licensemanager/package.json | 4 +- .../test/licensemanager.test.ts | 2 +- .../aws-logs-destinations/package.json | 4 +- .../test/kinesis.test.ts | 2 +- .../aws-logs-destinations/test/lambda.test.ts | 2 +- packages/@aws-cdk/aws-logs/package.json | 4 +- .../aws-logs/test/test.destination.ts | 2 +- .../aws-logs/test/test.log-retention.ts | 2 +- .../@aws-cdk/aws-logs/test/test.loggroup.ts | 2 +- .../@aws-cdk/aws-logs/test/test.logstream.ts | 2 +- .../aws-logs/test/test.metricfilter.ts | 2 +- .../aws-logs/test/test.subscriptionfilter.ts | 2 +- .../@aws-cdk/aws-lookoutvision/package.json | 4 +- .../test/lookoutvision.test.ts | 2 +- packages/@aws-cdk/aws-macie/package.json | 4 +- .../@aws-cdk/aws-macie/test/macie.test.ts | 2 +- .../aws-managedblockchain/package.json | 4 +- .../test/managedblockchain.test.ts | 2 +- .../@aws-cdk/aws-mediaconnect/package.json | 4 +- .../test/mediaconnect.test.ts | 2 +- .../@aws-cdk/aws-mediaconvert/package.json | 4 +- .../test/mediaconvert.test.ts | 2 +- packages/@aws-cdk/aws-medialive/package.json | 4 +- .../aws-medialive/test/medialive.test.ts | 2 +- .../@aws-cdk/aws-mediapackage/package.json | 4 +- .../test/mediapackage.test.ts | 2 +- packages/@aws-cdk/aws-mediastore/package.json | 4 +- .../aws-mediastore/test/mediastore.test.ts | 2 +- packages/@aws-cdk/aws-msk/package.json | 4 +- packages/@aws-cdk/aws-msk/test/msk.test.ts | 2 +- packages/@aws-cdk/aws-mwaa/package.json | 4 +- packages/@aws-cdk/aws-mwaa/test/mwaa.test.ts | 2 +- packages/@aws-cdk/aws-neptune/package.json | 4 +- .../@aws-cdk/aws-neptune/test/cluster.test.ts | 4 +- .../aws-neptune/test/instance.test.ts | 2 +- .../aws-neptune/test/parameter-group.test.ts | 2 +- .../aws-neptune/test/subnet-group.test.ts | 2 +- .../@aws-cdk/aws-networkfirewall/package.json | 4 +- .../test/networkfirewall.test.ts | 2 +- .../@aws-cdk/aws-networkmanager/package.json | 4 +- .../test/networkmanager.test.ts | 2 +- packages/@aws-cdk/aws-opsworks/package.json | 4 +- .../aws-opsworks/test/opsworks.test.ts | 2 +- packages/@aws-cdk/aws-opsworkscm/package.json | 4 +- .../aws-opsworkscm/test/opsworkscm.test.ts | 2 +- packages/@aws-cdk/aws-pinpoint/package.json | 4 +- .../aws-pinpoint/test/pinpoint.test.ts | 2 +- .../@aws-cdk/aws-pinpointemail/package.json | 4 +- .../test/pinpointemail.test.ts | 2 +- packages/@aws-cdk/aws-qldb/package.json | 4 +- packages/@aws-cdk/aws-qldb/test/qldb.test.ts | 2 +- packages/@aws-cdk/aws-quicksight/package.json | 4 +- .../aws-quicksight/test/quicksight.test.ts | 2 +- packages/@aws-cdk/aws-ram/package.json | 4 +- packages/@aws-cdk/aws-ram/test/ram.test.ts | 2 +- packages/@aws-cdk/aws-rds/package.json | 4 +- .../@aws-cdk/aws-rds/test/cluster.test.ts | 4 +- .../aws-rds/test/database-secret.test.ts | 2 +- .../test/database-secretmanager.test.ts | 2 +- .../aws-rds/test/instance-engine.test.ts | 2 +- .../@aws-cdk/aws-rds/test/instance.test.ts | 4 +- .../aws-rds/test/option-group.test.ts | 2 +- .../aws-rds/test/parameter-group.test.ts | 2 +- packages/@aws-cdk/aws-rds/test/proxy.test.ts | 2 +- .../aws-rds/test/serverless-cluster.test.ts | 2 +- .../sql-server.instance-engine.test.ts | 2 +- .../aws-rds/test/subnet-group.test.ts | 2 +- packages/@aws-cdk/aws-redshift/package.json | 4 +- .../aws-redshift/test/cluster.test.ts | 4 +- .../aws-redshift/test/parameter-group.test.ts | 2 +- .../aws-redshift/test/subnet-group.test.ts | 2 +- .../@aws-cdk/aws-resourcegroups/package.json | 4 +- .../test/resourcegroups.test.ts | 2 +- packages/@aws-cdk/aws-robomaker/package.json | 4 +- .../aws-robomaker/test/robomaker.test.ts | 2 +- .../aws-route53-patterns/package.json | 4 +- .../test/bucket-website-target.test.ts | 2 +- .../@aws-cdk/aws-route53-targets/package.json | 4 +- .../test/apigateway-target.test.ts | 2 +- .../test/apigatewayv2-target.test.ts | 2 +- .../test/bucket-website-target.test.ts | 2 +- .../test/classic-load-balancer-target.test.ts | 2 +- .../test/cloudfront-target.test.ts | 4 +- .../test/global-accelerator-target.test.ts | 2 +- .../interface-vpc-endpoint-target.test.ts | 2 +- .../test/load-balancer-target.test.ts | 2 +- .../test/userpool-domain.test.ts | 2 +- packages/@aws-cdk/aws-route53/package.json | 4 +- .../test/hosted-zone-provider.test.ts | 2 +- .../aws-route53/test/hosted-zone.test.ts | 2 +- .../aws-route53/test/record-set.test.ts | 2 +- .../@aws-cdk/aws-route53/test/route53.test.ts | 2 +- .../vpc-endpoint-service-domain-name.test.ts | 4 +- .../@aws-cdk/aws-route53resolver/package.json | 4 +- .../test/route53resolver.test.ts | 2 +- packages/@aws-cdk/aws-s3-assets/package.json | 4 +- .../@aws-cdk/aws-s3-assets/test/asset.test.ts | 4 +- .../@aws-cdk/aws-s3-deployment/package.json | 4 +- .../test/bucket-deployment.test.ts | 2 +- .../aws-s3-notifications/package.json | 4 +- .../test/lambda/lambda.test.ts | 6 +- .../test/notifications.test.ts | 4 +- .../aws-s3-notifications/test/queue.test.ts | 4 +- .../aws-s3-notifications/test/sns.test.ts | 2 +- packages/@aws-cdk/aws-s3/package.json | 4 +- packages/@aws-cdk/aws-s3/test/aspect.test.ts | 4 +- .../aws-s3/test/bucket-policy.test.ts | 2 +- packages/@aws-cdk/aws-s3/test/bucket.test.ts | 4 +- packages/@aws-cdk/aws-s3/test/cors.test.ts | 2 +- packages/@aws-cdk/aws-s3/test/metrics.test.ts | 2 +- .../@aws-cdk/aws-s3/test/notification.test.ts | 2 +- packages/@aws-cdk/aws-s3/test/rules.test.ts | 2 +- .../@aws-cdk/aws-s3objectlambda/package.json | 4 +- .../test/s3objectlambda.test.ts | 2 +- packages/@aws-cdk/aws-s3outposts/package.json | 4 +- .../aws-s3outposts/test/s3outposts.test.ts | 2 +- packages/@aws-cdk/aws-sagemaker/package.json | 4 +- .../aws-sagemaker/test/sagemaker.test.ts | 2 +- packages/@aws-cdk/aws-sam/package.json | 4 +- .../@aws-cdk/aws-sam/test/application.test.ts | 2 +- .../@aws-cdk/aws-sam/test/function.test.ts | 2 +- packages/@aws-cdk/aws-sdb/package.json | 4 +- packages/@aws-cdk/aws-sdb/test/sdb.test.ts | 2 +- .../@aws-cdk/aws-secretsmanager/package.json | 4 +- .../test/rotation-schedule.test.ts | 2 +- .../test/secret-rotation.test.ts | 2 +- .../aws-secretsmanager/test/secret.test.ts | 4 +- .../@aws-cdk/aws-securityhub/package.json | 4 +- .../aws-securityhub/test/securityhub.test.ts | 2 +- .../@aws-cdk/aws-servicecatalog/package.json | 4 +- .../test/servicecatalog.test.ts | 2 +- .../package.json | 4 +- .../test/servicecatalogappregistry.test.ts | 2 +- .../aws-servicediscovery/package.json | 4 +- .../test/test.instance.ts | 2 +- .../test/test.namespace.ts | 2 +- .../aws-servicediscovery/test/test.service.ts | 2 +- .../@aws-cdk/aws-ses-actions/package.json | 4 +- .../aws-ses-actions/test/actions.test.ts | 4 +- packages/@aws-cdk/aws-ses/package.json | 4 +- .../aws-ses/test/test.receipt-filter.ts | 2 +- .../aws-ses/test/test.receipt-rule-set.ts | 2 +- .../aws-ses/test/test.receipt-rule.ts | 2 +- packages/@aws-cdk/aws-signer/package.json | 4 +- .../aws-signer/test/signing-profile.test.ts | 2 +- .../aws-sns-subscriptions/package.json | 4 +- .../aws-sns-subscriptions/test/subs.test.ts | 2 +- packages/@aws-cdk/aws-sns/package.json | 4 +- packages/@aws-cdk/aws-sns/test/test.sns.ts | 2 +- .../aws-sns/test/test.subscription.ts | 2 +- packages/@aws-cdk/aws-sqs/package.json | 4 +- packages/@aws-cdk/aws-sqs/test/test.sqs.ts | 2 +- packages/@aws-cdk/aws-ssm/package.json | 4 +- .../test/test.parameter-store-string.ts | 2 +- .../@aws-cdk/aws-ssm/test/test.parameter.ts | 2 +- .../aws-ssm/test/test.ssm-document.ts | 2 +- packages/@aws-cdk/aws-sso/package.json | 4 +- packages/@aws-cdk/aws-sso/test/sso.test.ts | 2 +- .../aws-stepfunctions-tasks/package.json | 4 +- .../test/ecs/ecs-tasks.test.ts | 2 +- .../test/ecs/run-tasks.test.ts | 2 +- .../test/emr/emr-add-step.test.ts | 2 +- .../test/emr/emr-cancel-step.test.ts | 2 +- .../test/emr/emr-create-cluster.test.ts | 2 +- .../emr-modify-instance-fleet-by-name.test.ts | 2 +- .../emr-modify-instance-group-by-name.test.ts | 2 +- ...set-cluster-termination-protection.test.ts | 2 +- .../test/emr/emr-terminate-cluster.test.ts | 2 +- .../test/evaluate-expression.test.ts | 2 +- .../test/glue/run-glue-job-task.test.ts | 2 +- .../test/glue/start-job-run.test.ts | 2 +- .../test/invoke-activity.test.ts | 2 +- .../test/lambda/invoke-function.test.ts | 2 +- .../test/lambda/invoke.test.ts | 2 +- .../test/lambda/run-lambda-task.test.ts | 2 +- .../sagemaker/create-endpoint-config.test.ts | 2 +- .../test/sagemaker/create-endpoint.test.ts | 2 +- .../test/sagemaker/create-model.test.ts | 2 +- .../sagemaker/create-training-job.test.ts | 2 +- .../sagemaker/create-transform-job.test.ts | 2 +- .../test/sagemaker/update-endpoint.test.ts | 2 +- .../test/start-execution.test.ts | 2 +- .../test/stepfunctions/invoke-activity.ts | 2 +- .../stepfunctions/start-execution.test.ts | 2 +- .../@aws-cdk/aws-stepfunctions/package.json | 4 +- .../aws-stepfunctions/test/activity.test.ts | 4 +- .../aws-stepfunctions/test/condition.test.ts | 2 +- .../test/custom-state.test.ts | 2 +- .../aws-stepfunctions/test/fail.test.ts | 2 +- .../aws-stepfunctions/test/fields.test.ts | 2 +- .../aws-stepfunctions/test/map.test.ts | 2 +- .../aws-stepfunctions/test/parallel.test.ts | 2 +- .../aws-stepfunctions/test/pass.test.ts | 2 +- .../test/state-machine-resources.test.ts | 4 +- .../test/state-machine.test.ts | 2 +- .../test/states-language.test.ts | 2 +- .../aws-stepfunctions/test/task-base.test.ts | 2 +- .../aws-stepfunctions/test/wait.test.ts | 2 +- packages/@aws-cdk/aws-synthetics/package.json | 4 +- .../aws-synthetics/test/canary.test.ts | 4 +- .../@aws-cdk/aws-synthetics/test/code.test.ts | 2 +- .../aws-synthetics/test/metric.test.ts | 2 +- packages/@aws-cdk/aws-timestream/package.json | 4 +- .../aws-timestream/test/timestream.test.ts | 2 +- packages/@aws-cdk/aws-transfer/package.json | 4 +- .../aws-transfer/test/transfer.test.ts | 2 +- packages/@aws-cdk/aws-waf/package.json | 4 +- packages/@aws-cdk/aws-waf/test/waf.test.ts | 2 +- .../@aws-cdk/aws-wafregional/package.json | 4 +- .../aws-wafregional/test/wafregional.test.ts | 2 +- packages/@aws-cdk/aws-wafv2/package.json | 4 +- .../@aws-cdk/aws-wafv2/test/wafv2.test.ts | 2 +- packages/@aws-cdk/aws-workspaces/package.json | 4 +- .../aws-workspaces/test/workspaces.test.ts | 2 +- .../build-tools/create-missing-libraries.ts | 4 +- .../cloudformation-include/package.json | 4 +- .../test/invalid-templates.test.ts | 4 +- .../test/nested-stacks.test.ts | 4 +- .../test/serverless-transform.test.ts | 2 +- .../test/valid-templates.test.ts | 4 +- .../test/yaml-templates.test.ts | 2 +- .../@aws-cdk/core/lib/private/synthesis.ts | 2 +- .../stack-synthesizers/default-synthesizer.ts | 2 +- .../@aws-cdk/custom-resources/package.json | 4 +- .../aws-custom-resource.test.ts | 2 +- .../test/provider-framework/provider.test.ts | 2 +- .../waiter-state-machine.test.ts | 2 +- .../example-construct-library/package.json | 4 +- .../test/example-resource.test.ts | 4 +- .../@aws-cdk/lambda-layer-awscli/package.json | 4 +- .../test/awscli-layer.test.ts | 2 +- .../lambda-layer-kubectl/package.json | 4 +- .../test/kubectl-layer.test.ts | 2 +- packages/@aws-cdk/pipelines/package.json | 4 +- .../test/build-role-policy-statements.test.ts | 4 +- .../@aws-cdk/pipelines/test/builds.test.ts | 4 +- .../test/cross-environment-infra.test.ts | 4 +- .../pipelines/test/existing-pipeline.test.ts | 4 +- .../pipelines/test/pipeline-assets.test.ts | 4 +- .../@aws-cdk/pipelines/test/pipeline.test.ts | 4 +- .../pipelines/test/stack-ordering.test.ts | 4 +- .../@aws-cdk/pipelines/test/testmatchers.ts | 2 +- .../pipelines/test/validation.test.ts | 4 +- packages/@aws-cdk/yaml-cfn/package.json | 4 +- .../yaml-cfn/test/deserialization.test.ts | 2 +- .../yaml-cfn/test/serialization.test.ts | 2 +- packages/@monocdk-experiment/assert/clone.sh | 2 +- .../@monocdk-experiment/assert/package.json | 2 +- packages/aws-cdk-migration/.eslintrc.js | 3 + packages/aws-cdk-migration/.gitignore | 15 + packages/aws-cdk-migration/.npmignore | 17 + packages/aws-cdk-migration/LICENSE | 201 ++++++++ packages/aws-cdk-migration/NOTICE | 2 + packages/aws-cdk-migration/README.md | 26 ++ .../aws-cdk-migration/bin/rewrite-imports-v2 | 2 + .../bin/rewrite-imports-v2.ts | 37 ++ packages/aws-cdk-migration/jest.config.js | 10 + packages/aws-cdk-migration/lib/rewrite.ts | 114 +++++ packages/aws-cdk-migration/package.json | 59 +++ .../aws-cdk-migration/test/rewrite.test.ts | 91 ++++ packages/aws-cdk-migration/tsconfig.json | 27 ++ .../typescript/test/%name%.test.template.ts | 2 +- tools/pkglint/lib/rules.ts | 24 +- 800 files changed, 4622 insertions(+), 1041 deletions(-) create mode 100644 packages/@aws-cdk/assert-internal/.eslintrc.js create mode 100644 packages/@aws-cdk/assert-internal/.gitignore create mode 100644 packages/@aws-cdk/assert-internal/.npmignore create mode 100644 packages/@aws-cdk/assert-internal/LICENSE create mode 100644 packages/@aws-cdk/assert-internal/NOTICE create mode 100644 packages/@aws-cdk/assert-internal/README.md create mode 100644 packages/@aws-cdk/assert-internal/jest.config.js create mode 100644 packages/@aws-cdk/assert-internal/jest.ts create mode 100644 packages/@aws-cdk/assert-internal/lib/assertion.ts create mode 100644 packages/@aws-cdk/assert-internal/lib/assertions/and-assertion.ts create mode 100644 packages/@aws-cdk/assert-internal/lib/assertions/count-resources.ts create mode 100644 packages/@aws-cdk/assert-internal/lib/assertions/exist.ts create mode 100644 packages/@aws-cdk/assert-internal/lib/assertions/have-output.ts create mode 100644 packages/@aws-cdk/assert-internal/lib/assertions/have-resource-matchers.ts create mode 100644 packages/@aws-cdk/assert-internal/lib/assertions/have-resource.ts create mode 100644 packages/@aws-cdk/assert-internal/lib/assertions/have-type.ts create mode 100644 packages/@aws-cdk/assert-internal/lib/assertions/match-template.ts create mode 100644 packages/@aws-cdk/assert-internal/lib/assertions/negated-assertion.ts create mode 100644 packages/@aws-cdk/assert-internal/lib/canonicalize-assets.ts create mode 100644 packages/@aws-cdk/assert-internal/lib/expect.ts create mode 100644 packages/@aws-cdk/assert-internal/lib/index.ts create mode 100644 packages/@aws-cdk/assert-internal/lib/inspector.ts create mode 100644 packages/@aws-cdk/assert-internal/lib/synth-utils.ts create mode 100644 packages/@aws-cdk/assert-internal/package.json create mode 100644 packages/@aws-cdk/assert-internal/test/assertions.test.ts create mode 100644 packages/@aws-cdk/assert-internal/test/canonicalize-assets.test.ts create mode 100644 packages/@aws-cdk/assert-internal/test/have-output.test.ts create mode 100644 packages/@aws-cdk/assert-internal/test/have-resource.test.ts create mode 100644 packages/@aws-cdk/assert-internal/test/synth-utils.test.ts create mode 100644 packages/@aws-cdk/assert-internal/tsconfig.json create mode 100644 packages/@aws-cdk/assert/BUILD.md create mode 100755 packages/@aws-cdk/assert/clone.sh create mode 100644 packages/aws-cdk-migration/.eslintrc.js create mode 100644 packages/aws-cdk-migration/.gitignore create mode 100644 packages/aws-cdk-migration/.npmignore create mode 100644 packages/aws-cdk-migration/LICENSE create mode 100644 packages/aws-cdk-migration/NOTICE create mode 100644 packages/aws-cdk-migration/README.md create mode 100755 packages/aws-cdk-migration/bin/rewrite-imports-v2 create mode 100644 packages/aws-cdk-migration/bin/rewrite-imports-v2.ts create mode 100644 packages/aws-cdk-migration/jest.config.js create mode 100644 packages/aws-cdk-migration/lib/rewrite.ts create mode 100644 packages/aws-cdk-migration/package.json create mode 100644 packages/aws-cdk-migration/test/rewrite.test.ts create mode 100644 packages/aws-cdk-migration/tsconfig.json diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/package.json b/packages/@aws-cdk-containers/ecs-service-extensions/package.json index 4b75bbbd568f4..5f4683eb92881 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/package.json +++ b/packages/@aws-cdk-containers/ecs-service-extensions/package.json @@ -35,14 +35,14 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "jest": "^26.6.3", "nodeunit": "^0.11.3", - "pkglint": "0.0.0" + "pkglint": "0.0.0", + "@aws-cdk/assert-internal": "0.0.0" }, "dependencies": { "@aws-cdk/aws-applicationautoscaling": "0.0.0", diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/test.appmesh.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/test.appmesh.ts index a221b9d31933e..c38447ac912bb 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/test.appmesh.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/test.appmesh.ts @@ -1,4 +1,4 @@ -import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; +import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert-internal'; import * as appmesh from '@aws-cdk/aws-appmesh'; import * as ecs from '@aws-cdk/aws-ecs'; import * as cdk from '@aws-cdk/core'; diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/test.assign-public-ip.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/test.assign-public-ip.ts index 92a36dd1d788b..2f4f926e1746b 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/test.assign-public-ip.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/test.assign-public-ip.ts @@ -1,4 +1,4 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert'; +import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; import * as route53 from '@aws-cdk/aws-route53'; diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/test.cloudwatch-agent.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/test.cloudwatch-agent.ts index a8d94c3dc8e25..0b9ad5a09e647 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/test.cloudwatch-agent.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/test.cloudwatch-agent.ts @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import { expect, haveResource } from '@aws-cdk/assert-internal'; import * as ecs from '@aws-cdk/aws-ecs'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/test.environment.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/test.environment.ts index d029f81e34bc6..f1362695ac535 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/test.environment.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/test.environment.ts @@ -1,4 +1,4 @@ -import { countResources, expect, haveResource } from '@aws-cdk/assert'; +import { countResources, expect, haveResource } from '@aws-cdk/assert-internal'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; import * as cdk from '@aws-cdk/core'; diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/test.firelens.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/test.firelens.ts index e4ceb68444dd7..a6011a2caabf6 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/test.firelens.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/test.firelens.ts @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import { expect, haveResource } from '@aws-cdk/assert-internal'; import * as ecs from '@aws-cdk/aws-ecs'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/test.http-load-balancer.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/test.http-load-balancer.ts index 2cc26be97ddcf..2e977e19ee889 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/test.http-load-balancer.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/test.http-load-balancer.ts @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import { expect, haveResource } from '@aws-cdk/assert-internal'; import * as ecs from '@aws-cdk/aws-ecs'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/test.scale-on-cpu-utilization.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/test.scale-on-cpu-utilization.ts index 305655129eb1a..6d3e6ce715b1d 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/test.scale-on-cpu-utilization.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/test.scale-on-cpu-utilization.ts @@ -1,4 +1,4 @@ -import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; +import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert-internal'; import * as ecs from '@aws-cdk/aws-ecs'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/test.service.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/test.service.ts index 23b30f59afe3d..4fa8c80657cd0 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/test.service.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/test.service.ts @@ -1,4 +1,4 @@ -import { countResources, expect, haveResource } from '@aws-cdk/assert'; +import { countResources, expect, haveResource } from '@aws-cdk/assert-internal'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; import * as cdk from '@aws-cdk/core'; diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/test.xray.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/test.xray.ts index 36443123e8719..c591b5c6dd012 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/test.xray.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/test.xray.ts @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import { expect, haveResource } from '@aws-cdk/assert-internal'; import * as ecs from '@aws-cdk/aws-ecs'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; diff --git a/packages/@aws-cdk/alexa-ask/package.json b/packages/@aws-cdk/alexa-ask/package.json index 97bad8c492ebc..767c2013c8398 100644 --- a/packages/@aws-cdk/alexa-ask/package.json +++ b/packages/@aws-cdk/alexa-ask/package.json @@ -72,10 +72,10 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", - "pkglint": "0.0.0" + "pkglint": "0.0.0", + "@aws-cdk/assert-internal": "0.0.0" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/alexa-ask/test/ask.test.ts b/packages/@aws-cdk/alexa-ask/test/ask.test.ts index e394ef336bfb4..c4505ad966984 100644 --- a/packages/@aws-cdk/alexa-ask/test/ask.test.ts +++ b/packages/@aws-cdk/alexa-ask/test/ask.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert/jest'; +import '@aws-cdk/assert-internal/jest'; import {} from '../lib'; test('No tests are specified for this package', () => { diff --git a/packages/@aws-cdk/app-delivery/package.json b/packages/@aws-cdk/app-delivery/package.json index 7569a0e07a1cc..990a4dd376c50 100644 --- a/packages/@aws-cdk/app-delivery/package.json +++ b/packages/@aws-cdk/app-delivery/package.json @@ -58,14 +58,14 @@ "constructs": "^3.3.69" }, "devDependencies": { - "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "fast-check": "^2.14.0", "nodeunit": "^0.11.3", - "pkglint": "0.0.0" + "pkglint": "0.0.0", + "@aws-cdk/assert-internal": "0.0.0" }, "repository": { "type": "git", diff --git a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts b/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts index d63ddbf6acb50..e4611adf570f6 100644 --- a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts +++ b/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts @@ -1,4 +1,4 @@ -import { expect, haveResource, haveResourceLike, isSuperObject } from '@aws-cdk/assert'; +import { expect, haveResource, haveResourceLike, 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'; diff --git a/packages/@aws-cdk/assert-internal/.eslintrc.js b/packages/@aws-cdk/assert-internal/.eslintrc.js new file mode 100644 index 0000000000000..61dd8dd001f63 --- /dev/null +++ b/packages/@aws-cdk/assert-internal/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/assert-internal/.gitignore b/packages/@aws-cdk/assert-internal/.gitignore new file mode 100644 index 0000000000000..c9b9bcc8658a1 --- /dev/null +++ b/packages/@aws-cdk/assert-internal/.gitignore @@ -0,0 +1,16 @@ +*.js +*.js.map +*.d.ts +node_modules +dist + +.LAST_BUILD +.nyc_output +coverage +nyc.config.js +.LAST_PACKAGE +*.snk +!.eslintrc.js +!jest.config.js + +junit.xml \ No newline at end of file diff --git a/packages/@aws-cdk/assert-internal/.npmignore b/packages/@aws-cdk/assert-internal/.npmignore new file mode 100644 index 0000000000000..6f149ce45fddd --- /dev/null +++ b/packages/@aws-cdk/assert-internal/.npmignore @@ -0,0 +1,22 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +*.snk + +*.tsbuildinfo + +tsconfig.json +.eslintrc.js +jest.config.js + +# exclude cdk artifacts +**/cdk.out +junit.xml +test/ \ No newline at end of file diff --git a/packages/@aws-cdk/assert-internal/LICENSE b/packages/@aws-cdk/assert-internal/LICENSE new file mode 100644 index 0000000000000..28e4bdcec77ec --- /dev/null +++ b/packages/@aws-cdk/assert-internal/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/assert-internal/NOTICE b/packages/@aws-cdk/assert-internal/NOTICE new file mode 100644 index 0000000000000..5fc3826926b5b --- /dev/null +++ b/packages/@aws-cdk/assert-internal/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/assert-internal/README.md b/packages/@aws-cdk/assert-internal/README.md new file mode 100644 index 0000000000000..9256b46d2b154 --- /dev/null +++ b/packages/@aws-cdk/assert-internal/README.md @@ -0,0 +1,227 @@ +# Testing utilities and assertions for CDK libraries + + +--- + +![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. + +--- + + + +This library contains helpers for writing unit tests and integration tests for CDK libraries + +## Unit tests + +Write your unit tests like this: + +```ts +const stack = new Stack(); + +new MyConstruct(stack, 'MyConstruct', { + ... +}); + +expect(stack).to(someExpectation(...)); +``` + +Here are the expectations you can use: + +## Verify (parts of) a template + +Check that the synthesized stack template looks like the given template, or is a superset of it. These functions match logical IDs and all properties of a resource. + +```ts +matchTemplate(template, matchStyle) +exactlyMatchTemplate(template) +beASupersetOfTemplate(template) +``` + +Example: + +```ts +expect(stack).to(beASupersetOfTemplate({ + Resources: { + HostedZone674DD2B7: { + Type: "AWS::Route53::HostedZone", + Properties: { + Name: "test.private.", + VPCs: [{ + VPCId: { Ref: 'VPC06C5F037' }, + VPCRegion: { Ref: 'AWS::Region' } + }] + } + } + } +})); +``` + + +## Check existence of a resource + +If you only care that a resource of a particular type exists (regardless of its logical identifier), and that *some* of its properties are set to specific values: + +```ts +haveResource(type, subsetOfProperties) +haveResourceLike(type, subsetOfProperties) +``` + +Example: + +```ts +expect(stack).to(haveResource('AWS::CertificateManager::Certificate', { + DomainName: 'test.example.com', + // Note: some properties omitted here + + ShouldNotExist: ABSENT +})); +``` + +The object you give to `haveResource`/`haveResourceLike` like can contain the +following values: + +- **Literal values**: the given property in the resource must match the given value *exactly*. +- `ABSENT`: a magic value to assert that a particular key in an object is *not* set (or set to `undefined`). +- special matchers for inexact matching. You can use these to match values based on more lenient conditions + than the default (such as an array containing at least one element, ignoring the rest, or an inexact string + match). + +The following matchers exist: + +- `objectLike(O)` - the value has to be an object matching at least the keys in `O` (but may contain + more). The nested values must match exactly. +- `deepObjectLike(O)` - as `objectLike`, but nested objects are also treated as partial specifications. +- `exactValue(X)` - must match exactly the given value. Use this to escape from `deepObjectLike`'s leniency + back to exact value matching. +- `arrayWith(E, [F, ...])` - value must be an array containing the given elements (or matchers) in any order. +- `stringLike(S)` - value must be a string matching `S`. `S` may contain `*` as wildcard to match any number + of characters. +- `anything()` - matches any value. +- `notMatching(M)` - any value that does NOT match the given matcher (or exact value) given. +- `encodedJson(M)` - value must be a string which, when decoded as JSON, matches the given matcher or + exact value. + +Slightly more complex example with array matchers: + +```ts +expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith(objectLike({ + Action: ['s3:GetObject'], + Resource: ['arn:my:arn'], + }}) + } +})); +``` + +## Capturing values from a match + +Special `Capture` matchers exist to capture values encountered during a match. These can be +used for two typical purposes: + +- Apply additional assertions to the values found during a matching operation. +- Use the value found during a matching operation in a new matching operation. + +`Capture` matchers take an inner matcher as an argument, and will only capture the value +if the inner matcher succeeds in matching the given value. + +Here's an example which asserts that a policy for `RoleA` contains two statements +with *different* ARNs (without caring what those ARNs might be), and that +a policy for `RoleB` *also* has a statement for one of those ARNs (again, without +caring what the ARN might be): + +```ts +const arn1 = Capture.aString(); +const arn2 = Capture.aString(); + +expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + Roles: ['RoleA'], + PolicyDocument: { + Statement: [ + objectLike({ + Resource: [arn1.capture()], + }), + objectLike({ + Resource: [arn2.capture()], + }), + ], + }, +})); + +// Don't care about the values as long as they are not the same +expect(arn1.capturedValue).not.toEqual(arn2.capturedValue); + +expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + Roles: ['RoleB'], + PolicyDocument: { + Statement: [ + objectLike({ + // This ARN must be the same as ARN1 above. + Resource: [arn1.capturedValue] + }), + ], + }, +})); +``` + +NOTE: `Capture` look somewhat like *bindings* in other pattern matching +libraries you might be used to, but they are far simpler and very +deterministic. In particular, they don't do unification: if the same Capture +is either used multiple times in the same structure expression or matches +multiple times, no restarting of the match is done to make them all match the +same value: the last value encountered by the `Capture` (as determined by the +behavior of the matchers around it) is stored into it and will be the one +available after the match has completed. + +## Check number of resources + +If you want to assert that `n` number of resources of a particular type exist, with or without specific properties: + +```ts +countResources(type, count) +countResourcesLike(type, count, props) +``` + +Example: + +```ts +expect(stack).to(countResources('AWS::ApiGateway::Method', 3)); +expect(stack).to(countResourcesLike('AWS::ApiGateway::Method', 1, { + HttpMethod: 'GET', + ResourceId: { + "Ref": "MyResource01234" + } +})); +``` + +## Check existence of an output + +`haveOutput` assertion can be used to check that a stack contains specific output. +Parameters to check against can be: + +- `outputName` +- `outputValue` +- `exportName` + +If `outputValue` is provided, at least one of `outputName`, `exportName` should be provided as well + +Example + +```ts +expect(synthStack).to(haveOutput({ + outputName: 'TestOutputName', + exportName: 'TestOutputExportName', + outputValue: { + 'Fn::GetAtt': [ + 'TestResource', + 'Arn' + ] + } +})); +``` diff --git a/packages/@aws-cdk/assert-internal/jest.config.js b/packages/@aws-cdk/assert-internal/jest.config.js new file mode 100644 index 0000000000000..ac8c47076506a --- /dev/null +++ b/packages/@aws-cdk/assert-internal/jest.config.js @@ -0,0 +1,10 @@ +const baseConfig = require('cdk-build-tools/config/jest.config'); +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + statements: 75, + branches: 65, + }, + }, +}; diff --git a/packages/@aws-cdk/assert-internal/jest.ts b/packages/@aws-cdk/assert-internal/jest.ts new file mode 100644 index 0000000000000..5c6db5727ed8d --- /dev/null +++ b/packages/@aws-cdk/assert-internal/jest.ts @@ -0,0 +1,107 @@ +import * as core from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; +import { countResources } from './lib'; +import { JestFriendlyAssertion } from './lib/assertion'; +import { haveOutput, HaveOutputProperties } from './lib/assertions/have-output'; +import { HaveResourceAssertion, ResourcePart } from './lib/assertions/have-resource'; +import { MatchStyle, matchTemplate } from './lib/assertions/match-template'; +import { expect as ourExpect } from './lib/expect'; +import { StackInspector } from './lib/inspector'; + +declare global { + namespace jest { + interface Matchers { + toMatchTemplate( + template: any, + matchStyle?: MatchStyle): R; + + toHaveResource( + resourceType: string, + properties?: any, + comparison?: ResourcePart): R; + + toHaveResourceLike( + resourceType: string, + properties?: any, + comparison?: ResourcePart): R; + + toHaveOutput(props: HaveOutputProperties): R; + + toCountResources(resourceType: string, count: number): R; + } + } +} + +expect.extend({ + toMatchTemplate( + actual: cxapi.CloudFormationStackArtifact | core.Stack, + template: any, + matchStyle?: MatchStyle) { + + const assertion = matchTemplate(template, matchStyle); + const inspector = ourExpect(actual); + const pass = assertion.assertUsing(inspector); + if (pass) { + return { + pass, + message: () => 'Not ' + assertion.description, + }; + } else { + return { + pass, + message: () => assertion.description, + }; + } + }, + + toHaveResource( + actual: cxapi.CloudFormationStackArtifact | core.Stack, + resourceType: string, + properties?: any, + comparison?: ResourcePart) { + + const assertion = new HaveResourceAssertion(resourceType, properties, comparison, false); + return applyAssertion(assertion, actual); + }, + + toHaveResourceLike( + actual: cxapi.CloudFormationStackArtifact | core.Stack, + resourceType: string, + properties?: any, + comparison?: ResourcePart) { + + const assertion = new HaveResourceAssertion(resourceType, properties, comparison, true); + return applyAssertion(assertion, actual); + }, + + toHaveOutput( + actual: cxapi.CloudFormationStackArtifact | core.Stack, + props: HaveOutputProperties) { + + return applyAssertion(haveOutput(props), actual); + }, + + toCountResources( + actual: cxapi.CloudFormationStackArtifact | core.Stack, + resourceType: string, + count = 1) { + + return applyAssertion(countResources(resourceType, count), actual); + }, +}); + +function applyAssertion(assertion: JestFriendlyAssertion, actual: cxapi.CloudFormationStackArtifact | core.Stack) { + const inspector = ourExpect(actual); + const pass = assertion.assertUsing(inspector); + if (pass) { + return { + pass, + message: () => 'Not ' + assertion.generateErrorMessage(), + }; + } else { + return { + pass, + message: () => assertion.generateErrorMessage(), + }; + } +} diff --git a/packages/@aws-cdk/assert-internal/lib/assertion.ts b/packages/@aws-cdk/assert-internal/lib/assertion.ts new file mode 100644 index 0000000000000..376b099f8433f --- /dev/null +++ b/packages/@aws-cdk/assert-internal/lib/assertion.ts @@ -0,0 +1,40 @@ +import { Inspector } from './inspector'; + +export abstract class Assertion { + public abstract readonly description: string; + + public abstract assertUsing(inspector: InspectorClass): boolean; + + /** + * Assert this thing and another thing + */ + public and(assertion: Assertion): Assertion { + // Needs to delegate to a function so that we can import mutually dependent classes in the right order + return and(this, assertion); + } + + public assertOrThrow(inspector: InspectorClass) { + if (!this.assertUsing(inspector)) { + throw new Error(`${JSON.stringify(inspector.value, null, 2)} does not match ${this.description}`); + } + } +} + +export abstract class JestFriendlyAssertion extends Assertion { + /** + * Generates an error message that can be used by Jest. + */ + public abstract generateErrorMessage(): string; +} + +import { AndAssertion } from './assertions/and-assertion'; + +function and(left: Assertion, right: Assertion): Assertion { + return new AndAssertion(left, right); +} + +import { NegatedAssertion } from './assertions/negated-assertion'; + +export function not(assertion: Assertion): Assertion { + return new NegatedAssertion(assertion); +} diff --git a/packages/@aws-cdk/assert-internal/lib/assertions/and-assertion.ts b/packages/@aws-cdk/assert-internal/lib/assertions/and-assertion.ts new file mode 100644 index 0000000000000..737dbaca67e5e --- /dev/null +++ b/packages/@aws-cdk/assert-internal/lib/assertions/and-assertion.ts @@ -0,0 +1,19 @@ +import { Assertion } from '../assertion'; +import { Inspector } from '../inspector'; + +export class AndAssertion extends Assertion { + public description: string = 'Combined assertion'; + + constructor(private readonly first: Assertion, private readonly second: Assertion) { + super(); + } + + public assertUsing(_inspector: InspectorClass): boolean { + throw new Error('This is never called'); + } + + public assertOrThrow(inspector: InspectorClass) { + this.first.assertOrThrow(inspector); + this.second.assertOrThrow(inspector); + } +} diff --git a/packages/@aws-cdk/assert-internal/lib/assertions/count-resources.ts b/packages/@aws-cdk/assert-internal/lib/assertions/count-resources.ts new file mode 100644 index 0000000000000..0827ba1f18306 --- /dev/null +++ b/packages/@aws-cdk/assert-internal/lib/assertions/count-resources.ts @@ -0,0 +1,58 @@ +import { Assertion, JestFriendlyAssertion } from '../assertion'; +import { StackInspector } from '../inspector'; +import { isSuperObject } from './have-resource'; + +/** + * An assertion to check whether a resource of a given type and with the given properties exists, disregarding properties + */ +export function countResources(resourceType: string, count = 1): JestFriendlyAssertion { + return new CountResourcesAssertion(resourceType, count); +} + +/** + * An assertion to check whether a resource of a given type and with the given properties exists, considering properties + */ +export function countResourcesLike(resourceType: string, count = 1, props: any): Assertion { + return new CountResourcesAssertion(resourceType, count, props); +} + +class CountResourcesAssertion extends JestFriendlyAssertion { + private inspected: number = 0; + private readonly props: any; + + constructor( + private readonly resourceType: string, + private readonly count: number, + props: any = null) { + super(); + this.props = props; + } + + public assertUsing(inspector: StackInspector): boolean { + let counted = 0; + for (const logicalId of Object.keys(inspector.value.Resources || {})) { + const resource = inspector.value.Resources[logicalId]; + if (resource.Type === this.resourceType) { + if (this.props) { + if (isSuperObject(resource.Properties, this.props, [], true)) { + counted++; + this.inspected += 1; + } + } else { + counted++; + this.inspected += 1; + } + } + } + + return counted === this.count; + } + + public generateErrorMessage(): string { + return this.description; + } + + public get description(): string { + return `stack only has ${this.inspected} resource of type ${this.resourceType}${this.props ? ' with specified properties' : ''} but we expected to find ${this.count}`; + } +} diff --git a/packages/@aws-cdk/assert-internal/lib/assertions/exist.ts b/packages/@aws-cdk/assert-internal/lib/assertions/exist.ts new file mode 100644 index 0000000000000..3cc62f0444de4 --- /dev/null +++ b/packages/@aws-cdk/assert-internal/lib/assertions/exist.ts @@ -0,0 +1,18 @@ +import { Assertion } from '../assertion'; +import { StackPathInspector } from '../inspector'; + +class ExistingResourceAssertion extends Assertion { + public description: string = 'an existing resource'; + + constructor() { + super(); + } + + public assertUsing(inspector: StackPathInspector): boolean { + return inspector.value !== undefined; + } +} + +export function exist(): Assertion { + return new ExistingResourceAssertion(); +} diff --git a/packages/@aws-cdk/assert-internal/lib/assertions/have-output.ts b/packages/@aws-cdk/assert-internal/lib/assertions/have-output.ts new file mode 100644 index 0000000000000..36f76b3e573a0 --- /dev/null +++ b/packages/@aws-cdk/assert-internal/lib/assertions/have-output.ts @@ -0,0 +1,116 @@ +import { JestFriendlyAssertion } from '../assertion'; +import { StackInspector } from '../inspector'; + +class HaveOutputAssertion extends JestFriendlyAssertion { + private readonly inspected: InspectionFailure[] = []; + + constructor(private readonly outputName?: string, private readonly exportName?: any, private outputValue?: any) { + super(); + if (!this.outputName && !this.exportName) { + throw new Error('At least one of [outputName, exportName] should be provided'); + } + } + + public get description(): string { + const descriptionPartsArray = new Array(); + + if (this.outputName) { + descriptionPartsArray.push(`name '${this.outputName}'`); + } + if (this.exportName) { + descriptionPartsArray.push(`export name ${JSON.stringify(this.exportName)}`); + } + if (this.outputValue) { + descriptionPartsArray.push(`value ${JSON.stringify(this.outputValue)}`); + } + + return 'output with ' + descriptionPartsArray.join(', '); + } + + public assertUsing(inspector: StackInspector): boolean { + if (!('Outputs' in inspector.value)) { + return false; + } + + for (const [name, props] of Object.entries(inspector.value.Outputs as Record)) { + const mismatchedFields = new Array(); + + if (this.outputName && name !== this.outputName) { + mismatchedFields.push('name'); + } + + if (this.exportName && JSON.stringify(this.exportName) !== JSON.stringify(props.Export?.Name)) { + mismatchedFields.push('export name'); + } + + if (this.outputValue && JSON.stringify(this.outputValue) !== JSON.stringify(props.Value)) { + mismatchedFields.push('value'); + } + + if (mismatchedFields.length === 0) { + return true; + } + + this.inspected.push({ + output: { [name]: props }, + failureReason: `mismatched ${mismatchedFields.join(', ')}`, + }); + } + + return false; + } + + public generateErrorMessage() { + const lines = new Array(); + + lines.push(`None of ${this.inspected.length} outputs matches ${this.description}.`); + + for (const inspected of this.inspected) { + lines.push(`- ${inspected.failureReason} in:`); + lines.push(indent(4, JSON.stringify(inspected.output, null, 2))); + } + + return lines.join('\n'); + } +} + +/** + * Interface for haveOutput function properties + * NOTE that at least one of [outputName, exportName] should be provided + */ +export interface HaveOutputProperties { + /** + * Logical ID of the output + * @default - the logical ID of the output will not be checked + */ + outputName?: string; + /** + * Export name of the output, when it's exported for cross-stack referencing + * @default - the export name is not required and will not be checked + */ + exportName?: any; + /** + * Value of the output; + * @default - the value will not be checked + */ + outputValue?: any; +} + +interface InspectionFailure { + output: any; + failureReason: string; +} + +/** + * An assertion to check whether Output with particular properties is present in a stack + * @param props properties of the Output that is being asserted against. + * Check ``HaveOutputProperties`` interface to get full list of available parameters + */ +export function haveOutput(props: HaveOutputProperties): JestFriendlyAssertion { + return new HaveOutputAssertion(props.outputName, props.exportName, props.outputValue); +} + +function indent(n: number, s: string) { + const prefix = ' '.repeat(n); + return prefix + s.replace(/\n/g, '\n' + prefix); +} diff --git a/packages/@aws-cdk/assert-internal/lib/assertions/have-resource-matchers.ts b/packages/@aws-cdk/assert-internal/lib/assertions/have-resource-matchers.ts new file mode 100644 index 0000000000000..deb64b769ff16 --- /dev/null +++ b/packages/@aws-cdk/assert-internal/lib/assertions/have-resource-matchers.ts @@ -0,0 +1,430 @@ +import { ABSENT, InspectionFailure, PropertyMatcher } from './have-resource'; + +/** + * A matcher for an object that contains at least the given fields with the given matchers (or literals) + * + * Only does lenient matching one level deep, at the next level all objects must declare the + * exact expected keys again. + */ +export function objectLike(pattern: A): PropertyMatcher { + return _objectContaining(pattern, false); +} + +/** + * A matcher for an object that contains at least the given fields with the given matchers (or literals) + * + * Switches to "deep" lenient matching. Nested objects also only need to contain declared keys. + */ +export function deepObjectLike(pattern: A): PropertyMatcher { + return _objectContaining(pattern, true); +} + +function _objectContaining(pattern: A, deep: boolean): PropertyMatcher { + const anno = { [deep ? '$deepObjectLike' : '$objectLike']: pattern }; + + return annotateMatcher(anno, (value: any, inspection: InspectionFailure): boolean => { + if (typeof value !== 'object' || !value) { + return failMatcher(inspection, `Expect an object but got '${typeof value}'`); + } + + const errors = new Array(); + + for (const [patternKey, patternValue] of Object.entries(pattern)) { + if (patternValue === ABSENT) { + if (value[patternKey] !== undefined) { errors.push(`Field ${patternKey} present, but shouldn't be`); } + continue; + } + + if (!(patternKey in value)) { + errors.push(`Field ${patternKey} missing`); + continue; + } + + // If we are doing DEEP objectLike, translate object literals in the pattern into + // more `deepObjectLike` matchers, even if they occur in lists. + const matchValue = deep ? deepMatcherFromObjectLiteral(patternValue) : patternValue; + + const innerInspection = { ...inspection, failureReason: '' }; + const valueMatches = match(value[patternKey], matchValue, innerInspection); + if (!valueMatches) { + errors.push(`Field ${patternKey} mismatch: ${innerInspection.failureReason}`); + } + } + + /** + * Transform nested object literals into more deep object matchers, if applicable + * + * Object literals in lists are also transformed. + */ + function deepMatcherFromObjectLiteral(nestedPattern: any): any { + if (isObject(nestedPattern)) { + return deepObjectLike(nestedPattern); + } + if (Array.isArray(nestedPattern)) { + return nestedPattern.map(deepMatcherFromObjectLiteral); + } + return nestedPattern; + } + + if (errors.length > 0) { + return failMatcher(inspection, errors.join(', ')); + } + return true; + }); +} + +/** + * Match exactly the given value + * + * This is the default, you only need this to escape from the deep lenient matching + * of `deepObjectLike`. + */ +export function exactValue(expected: any): PropertyMatcher { + const anno = { $exactValue: expected }; + return annotateMatcher(anno, (value: any, inspection: InspectionFailure): boolean => { + return matchLiteral(value, expected, inspection); + }); +} + +/** + * A matcher for a list that contains all of the given elements in any order + */ +export function arrayWith(...elements: any[]): PropertyMatcher { + if (elements.length === 0) { return anything(); } + + const anno = { $arrayContaining: elements.length === 1 ? elements[0] : elements }; + return annotateMatcher(anno, (value: any, inspection: InspectionFailure): boolean => { + if (!Array.isArray(value)) { + return failMatcher(inspection, `Expect an array but got '${typeof value}'`); + } + + for (const element of elements) { + const failure = longestFailure(value, element); + if (failure) { + return failMatcher(inspection, `Array did not contain expected element, closest match at index ${failure[0]}: ${failure[1]}`); + } + } + + return true; + + /** + * Return 'null' if the matcher matches anywhere in the array, otherwise the longest error and its index + */ + function longestFailure(array: any[], matcher: any): [number, string] | null { + let fail: [number, string] | null = null; + for (let i = 0; i < array.length; i++) { + const innerInspection = { ...inspection, failureReason: '' }; + if (match(array[i], matcher, innerInspection)) { + return null; + } + + if (fail === null || innerInspection.failureReason.length > fail[1].length) { + fail = [i, innerInspection.failureReason]; + } + } + return fail; + } + }); +} + +/** + * Whether a value is an object + */ +function isObject(x: any): x is object { + // Because `typeof null === 'object'`. + return x && typeof x === 'object'; +} + +/** + * Helper function to make matcher failure reporting a little easier + * + * Our protocol is weird (change a string on a passed-in object and return 'false'), + * but I don't want to change that right now. + */ +export function failMatcher(inspection: InspectionFailure, error: string): boolean { + inspection.failureReason = error; + return false; +} + +/** + * Match a given literal value against a matcher + * + * If the matcher is a callable, use that to evaluate the value. Otherwise, the values + * must be literally the same. + */ +export function match(value: any, matcher: any, inspection: InspectionFailure) { + if (isCallable(matcher)) { + // Custom matcher (this mostly looks very weird because our `InspectionFailure` signature is weird) + const innerInspection: InspectionFailure = { ...inspection, failureReason: '' }; + const result = matcher(value, innerInspection); + if (typeof result !== 'boolean') { + return failMatcher(inspection, `Predicate returned non-boolean return value: ${result}`); + } + if (!result && !innerInspection.failureReason) { + // Custom matcher neglected to return an error + return failMatcher(inspection, 'Predicate returned false'); + } + // Propagate inner error in case of failure + if (!result) { inspection.failureReason = innerInspection.failureReason; } + return result; + } + + return matchLiteral(value, matcher, inspection); +} + +/** + * Match a literal value at the top level. + * + * When recursing into arrays or objects, the nested values can be either matchers + * or literals. + */ +function matchLiteral(value: any, pattern: any, inspection: InspectionFailure) { + if (pattern == null) { return true; } + + const errors = new Array(); + + if (Array.isArray(value) !== Array.isArray(pattern)) { + return failMatcher(inspection, 'Array type mismatch'); + } + if (Array.isArray(value)) { + if (pattern.length !== value.length) { + return failMatcher(inspection, 'Array length mismatch'); + } + + // Recurse comparison for individual objects + for (let i = 0; i < pattern.length; i++) { + if (!match(value[i], pattern[i], { ...inspection })) { + errors.push(`Array element ${i} mismatch`); + } + } + + if (errors.length > 0) { + return failMatcher(inspection, errors.join(', ')); + } + return true; + } + if ((typeof value === 'object') !== (typeof pattern === 'object')) { + return failMatcher(inspection, 'Object type mismatch'); + } + if (typeof pattern === 'object') { + // Check that all fields in the pattern have the right value + const innerInspection = { ...inspection, failureReason: '' }; + const matcher = objectLike(pattern)(value, innerInspection); + if (!matcher) { + inspection.failureReason = innerInspection.failureReason; + return false; + } + + // Check no fields uncovered + const realFields = new Set(Object.keys(value)); + for (const key of Object.keys(pattern)) { realFields.delete(key); } + if (realFields.size > 0) { + return failMatcher(inspection, `Unexpected keys present in object: ${Array.from(realFields).join(', ')}`); + } + return true; + } + + if (value !== pattern) { + return failMatcher(inspection, 'Different values'); + } + + return true; +} + +/** + * Whether a value is a callable + */ +function isCallable(x: any): x is ((...args: any[]) => any) { + return x && {}.toString.call(x) === '[object Function]'; +} + +/** + * Do a glob-like pattern match (which only supports *s) + */ +export function stringLike(pattern: string): PropertyMatcher { + // Replace * with .* in the string, escape the rest and brace with ^...$ + const regex = new RegExp(`^${pattern.split('*').map(escapeRegex).join('.*')}$`); + + return annotateMatcher({ $stringContaining: pattern }, (value: any, failure: InspectionFailure) => { + if (typeof value !== 'string') { + failure.failureReason = `Expected a string, but got '${typeof value}'`; + return false; + } + + if (!regex.test(value)) { + failure.failureReason = 'String did not match pattern'; + return false; + } + + return true; + }); +} + +/** + * Matches any value + */ +export function anything(): PropertyMatcher { + return annotateMatcher({ $anything: true }, () => true); +} + +/** + * Negate an inner matcher + */ +export function notMatching(matcher: any): PropertyMatcher { + return annotateMatcher({ $notMatching: matcher }, (value: any, failure: InspectionFailure) => { + const result = matcherFrom(matcher)(value, failure); + if (result) { + failure.failureReason = 'Should not have matched, but did'; + return false; + } + return true; + }); +} + +export type TypeValidator = (x: any) => x is T; + +/** + * Captures a value onto an object if it matches a given inner matcher + * + * @example + * + * const someValue = Capture.aString(); + * expect(stack).toHaveResource({ + * // ... + * Value: someValue.capture(stringMatching('*a*')), + * }); + * console.log(someValue.capturedValue); + */ +export class Capture { + /** + * A Capture object that captures any type + */ + public static anyType(): Capture { + return new Capture(); + } + + /** + * A Capture object that captures a string type + */ + public static aString(): Capture { + return new Capture((x: any): x is string => { + if (typeof x !== 'string') { + throw new Error(`Expected to capture a string, got '${x}'`); + } + return true; + }); + } + + /** + * A Capture object that captures a custom type + */ + // eslint-disable-next-line @typescript-eslint/no-shadow + public static a(validator: TypeValidator): Capture { + return new Capture(validator); + } + + private _value?: T; + private _didCapture = false; + private _wasInvoked = false; + + protected constructor(private readonly typeValidator?: TypeValidator) { + } + + /** + * Capture the value if the inner matcher successfully matches it + * + * If no matcher is given, `anything()` is assumed. + * + * And exception will be thrown if the inner matcher returns `true` and + * the value turns out to be of a different type than the `Capture` object + * is expecting. + */ + public capture(matcher?: any): PropertyMatcher { + if (matcher === undefined) { + matcher = anything(); + } + + return annotateMatcher({ $capture: matcher }, (value: any, failure: InspectionFailure) => { + this._wasInvoked = true; + const result = matcherFrom(matcher)(value, failure); + if (result) { + if (this.typeValidator && !this.typeValidator(value)) { + throw new Error(`Value not of the expected type: ${value}`); + } + this._didCapture = true; + this._value = value; + } + return result; + }); + } + + /** + * Whether a value was successfully captured + */ + public get didCapture() { + return this._didCapture; + } + + /** + * Return the value that was captured + * + * Throws an exception if now value was captured + */ + public get capturedValue(): T { + // When this module is ported to jsii, the type parameter will obviously + // have to be dropped and this will have to turn into an `any`. + if (!this.didCapture) { + throw new Error(`Did not capture a value: ${this._wasInvoked ? 'inner matcher failed' : 'never invoked'}`); + } + return this._value!; + } +} + +/** + * Match on the innards of a JSON string, instead of the complete string + */ +export function encodedJson(matcher: any): PropertyMatcher { + return annotateMatcher({ $encodedJson: matcher }, (value: any, failure: InspectionFailure) => { + if (typeof value !== 'string') { + failure.failureReason = `Expected a string, but got '${typeof value}'`; + return false; + } + + let decoded; + try { + decoded = JSON.parse(value); + } catch (e) { + failure.failureReason = `String is not JSON: ${e}`; + return false; + } + + return matcherFrom(matcher)(decoded, failure); + }); +} + +function escapeRegex(s: string) { + return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Make a matcher out of the given argument if it's not a matcher already + * + * If it's not a matcher, it will be treated as a literal. + */ +export function matcherFrom(matcher: any): PropertyMatcher { + return isCallable(matcher) ? matcher : exactValue(matcher); +} + +/** + * Annotate a matcher with toJSON + * + * We will JSON.stringify() values if we have a match failure, but for matchers this + * would show (in traditional JS fashion) something like '[function Function]', or more + * accurately nothing at all since functions cannot be JSONified. + * + * We override to JSON() in order to produce a readadable version of the matcher. + */ +export function annotateMatcher(how: A, matcher: PropertyMatcher): PropertyMatcher { + (matcher as any).toJSON = () => how; + return matcher; +} diff --git a/packages/@aws-cdk/assert-internal/lib/assertions/have-resource.ts b/packages/@aws-cdk/assert-internal/lib/assertions/have-resource.ts new file mode 100644 index 0000000000000..5a977d8252fd4 --- /dev/null +++ b/packages/@aws-cdk/assert-internal/lib/assertions/have-resource.ts @@ -0,0 +1,163 @@ +import { Assertion, JestFriendlyAssertion } from '../assertion'; +import { StackInspector } from '../inspector'; +import { anything, deepObjectLike, match, objectLike } from './have-resource-matchers'; + +/** + * Magic value to signify that a certain key should be absent from the property bag. + * + * The property is either not present or set to `undefined. + * + * NOTE: `ABSENT` only works with the `haveResource()` and `haveResourceLike()` + * assertions. + */ +export const ABSENT = '{{ABSENT}}'; + +/** + * An assertion to check whether a resource of a given type and with the given properties exists, disregarding properties + * + * @param resourceType the type of the resource that is expected to be present. + * @param properties the properties that the resource is expected to have. A function may be provided, in which case + * it will be called with the properties of candidate resources and an ``InspectionFailure`` + * instance on which errors should be appended, and should return a truthy value to denote a match. + * @param comparison the entity that is being asserted against. + * @param allowValueExtension if properties is an object, tells whether values must match exactly, or if they are + * allowed to be supersets of the reference values. Meaningless if properties is a function. + */ +export function haveResource( + resourceType: string, + properties?: any, + comparison?: ResourcePart, + allowValueExtension: boolean = false): Assertion { + return new HaveResourceAssertion(resourceType, properties, comparison, allowValueExtension); +} + +/** + * Sugar for calling ``haveResource`` with ``allowValueExtension`` set to ``true``. + */ +export function haveResourceLike( + resourceType: string, + properties?: any, + comparison?: ResourcePart) { + return haveResource(resourceType, properties, comparison, true); +} + +export type PropertyMatcher = (props: any, inspection: InspectionFailure) => boolean; + +export class HaveResourceAssertion extends JestFriendlyAssertion { + private readonly inspected: InspectionFailure[] = []; + private readonly part: ResourcePart; + private readonly matcher: any; + + constructor( + private readonly resourceType: string, + properties?: any, + part?: ResourcePart, + allowValueExtension: boolean = false) { + super(); + + this.matcher = isCallable(properties) ? properties : + properties === undefined ? anything() : + allowValueExtension ? deepObjectLike(properties) : + objectLike(properties); + this.part = part ?? ResourcePart.Properties; + } + + public assertUsing(inspector: StackInspector): boolean { + for (const logicalId of Object.keys(inspector.value.Resources || {})) { + const resource = inspector.value.Resources[logicalId]; + if (resource.Type === this.resourceType) { + const propsToCheck = this.part === ResourcePart.Properties ? (resource.Properties ?? {}) : resource; + + // Pass inspection object as 2nd argument, initialize failure with default string, + // to maintain backwards compatibility with old predicate API. + const inspection = { resource, failureReason: 'Object did not match predicate' }; + + if (match(propsToCheck, this.matcher, inspection)) { + return true; + } + + this.inspected.push(inspection); + } + } + + return false; + } + + public generateErrorMessage() { + const lines: string[] = []; + lines.push(`None of ${this.inspected.length} resources matches ${this.description}.`); + + for (const inspected of this.inspected) { + lines.push(`- ${inspected.failureReason} in:`); + lines.push(indent(4, JSON.stringify(inspected.resource, null, 2))); + } + + return lines.join('\n'); + } + + public assertOrThrow(inspector: StackInspector) { + if (!this.assertUsing(inspector)) { + throw new Error(this.generateErrorMessage()); + } + } + + public get description(): string { + // eslint-disable-next-line max-len + return `resource '${this.resourceType}' with ${JSON.stringify(this.matcher, undefined, 2)}`; + } +} + +function indent(n: number, s: string) { + const prefix = ' '.repeat(n); + return prefix + s.replace(/\n/g, '\n' + prefix); +} + +export interface InspectionFailure { + resource: any; + failureReason: string; +} + +/** + * What part of the resource to compare + */ +export enum ResourcePart { + /** + * Only compare the resource's properties + */ + Properties, + + /** + * Check the entire CloudFormation config + * + * (including UpdateConfig, DependsOn, etc.) + */ + CompleteDefinition +} + +/** + * Whether a value is a callable + */ +function isCallable(x: any): x is ((...args: any[]) => any) { + return x && {}.toString.call(x) === '[object Function]'; +} + +/** + * Return whether `superObj` is a super-object of `obj`. + * + * A super-object has the same or more property values, recursing into sub properties if ``allowValueExtension`` is true. + * + * At any point in the object, a value may be replaced with a function which will be used to check that particular field. + * The type of a matcher function is expected to be of type PropertyMatcher. + * + * @deprecated - Use `objectLike` or a literal object instead. + */ +export function isSuperObject(superObj: any, pattern: any, errors: string[] = [], allowValueExtension: boolean = false): boolean { + const matcher = allowValueExtension ? deepObjectLike(pattern) : objectLike(pattern); + + const inspection: InspectionFailure = { resource: superObj, failureReason: '' }; + const ret = match(superObj, matcher, inspection); + if (!ret) { + errors.push(inspection.failureReason); + } + return ret; +} diff --git a/packages/@aws-cdk/assert-internal/lib/assertions/have-type.ts b/packages/@aws-cdk/assert-internal/lib/assertions/have-type.ts new file mode 100644 index 0000000000000..a04d8a450a338 --- /dev/null +++ b/packages/@aws-cdk/assert-internal/lib/assertions/have-type.ts @@ -0,0 +1,21 @@ +import { Assertion } from '../assertion'; +import { StackPathInspector } from '../inspector'; + +export function haveType(type: string): Assertion { + return new StackPathHasTypeAssertion(type); +} + +class StackPathHasTypeAssertion extends Assertion { + constructor(private readonly type: string) { + super(); + } + + public assertUsing(inspector: StackPathInspector): boolean { + const resource = inspector.value; + return resource !== undefined && resource.Type === this.type; + } + + public get description(): string { + return `resource of type ${this.type}`; + } +} diff --git a/packages/@aws-cdk/assert-internal/lib/assertions/match-template.ts b/packages/@aws-cdk/assert-internal/lib/assertions/match-template.ts new file mode 100644 index 0000000000000..e668466d12416 --- /dev/null +++ b/packages/@aws-cdk/assert-internal/lib/assertions/match-template.ts @@ -0,0 +1,96 @@ +import * as cfnDiff from '@aws-cdk/cloudformation-diff'; +import { Assertion } from '../assertion'; +import { StackInspector } from '../inspector'; + +export enum MatchStyle { + /** Requires an exact match */ + EXACT = 'exactly', + /** Allows any change that does not cause a resource replacement */ + NO_REPLACES = 'no replaces', + /** Allows additions, but no updates */ + SUPERSET = 'superset' +} + +export function exactlyMatchTemplate(template: { [key: string]: any }) { + return matchTemplate(template, MatchStyle.EXACT); +} + +export function beASupersetOfTemplate(template: { [key: string]: any }) { + return matchTemplate(template, MatchStyle.SUPERSET); +} + +export function matchTemplate( + template: { [key: string]: any }, + matchStyle: MatchStyle = MatchStyle.EXACT): Assertion { + return new StackMatchesTemplateAssertion(template, matchStyle); +} + +class StackMatchesTemplateAssertion extends Assertion { + constructor( + private readonly template: { [key: string]: any }, + private readonly matchStyle: MatchStyle) { + super(); + } + + public assertOrThrow(inspector: StackInspector) { + if (!this.assertUsing(inspector)) { + // The details have already been printed, so don't generate a huge error message + throw new Error('Template comparison produced unacceptable match'); + } + } + + public assertUsing(inspector: StackInspector): boolean { + const diff = cfnDiff.diffTemplate(this.template, inspector.value); + const acceptable = this.isDiffAcceptable(diff); + if (!acceptable) { + // Print the diff + cfnDiff.formatDifferences(process.stderr, diff); + + // Print the actual template + process.stdout.write('--------------------------------------------------------------------------------------\n'); + process.stdout.write(JSON.stringify(inspector.value, undefined, 2) + '\n'); + } + + return acceptable; + } + + private isDiffAcceptable(diff: cfnDiff.TemplateDiff): boolean { + switch (this.matchStyle) { + case MatchStyle.EXACT: + return diff.differenceCount === 0; + case MatchStyle.NO_REPLACES: + for (const change of Object.values(diff.resources.changes)) { + if (change.changeImpact === cfnDiff.ResourceImpact.MAY_REPLACE) { return false; } + if (change.changeImpact === cfnDiff.ResourceImpact.WILL_REPLACE) { return false; } + } + + for (const change of Object.values(diff.parameters.changes)) { + if (change.isUpdate) { return false; } + } + + for (const change of Object.values(diff.outputs.changes)) { + if (change.isUpdate) { return false; } + } + return true; + case MatchStyle.SUPERSET: + for (const change of Object.values(diff.resources.changes)) { + if (change.changeImpact !== cfnDiff.ResourceImpact.WILL_CREATE) { return false; } + } + + for (const change of Object.values(diff.parameters.changes)) { + if (change.isAddition) { return false; } + } + + for (const change of Object.values(diff.outputs.changes)) { + if (change.isAddition || change.isUpdate) { return false; } + } + + return true; + } + throw new Error(`Unsupported match style: ${this.matchStyle}`); + } + + public get description(): string { + return `template (${this.matchStyle}): ${JSON.stringify(this.template, null, 2)}`; + } +} diff --git a/packages/@aws-cdk/assert-internal/lib/assertions/negated-assertion.ts b/packages/@aws-cdk/assert-internal/lib/assertions/negated-assertion.ts new file mode 100644 index 0000000000000..4c62225ee48a9 --- /dev/null +++ b/packages/@aws-cdk/assert-internal/lib/assertions/negated-assertion.ts @@ -0,0 +1,16 @@ +import { Assertion } from '../assertion'; +import { Inspector } from '../inspector'; + +export class NegatedAssertion extends Assertion { + constructor(private readonly negated: Assertion) { + super(); + } + + public assertUsing(inspector: I): boolean { + return !this.negated.assertUsing(inspector); + } + + public get description(): string { + return `not ${this.negated.description}`; + } +} diff --git a/packages/@aws-cdk/assert-internal/lib/canonicalize-assets.ts b/packages/@aws-cdk/assert-internal/lib/canonicalize-assets.ts new file mode 100644 index 0000000000000..9cee3d4742b3c --- /dev/null +++ b/packages/@aws-cdk/assert-internal/lib/canonicalize-assets.ts @@ -0,0 +1,71 @@ +/** + * Reduce template to a normal form where asset references have been normalized + * + * This makes it possible to compare templates if all that's different between + * them is the hashes of the asset values. + * + * Currently only handles parameterized assets, but can (and should) + * be adapted to handle convention-mode assets as well when we start using + * more of those. + */ +export function canonicalizeTemplate(template: any): any { + // For the weird case where we have an array of templates... + if (Array.isArray(template)) { + return template.map(canonicalizeTemplate); + } + + // Find assets via parameters + const stringSubstitutions = new Array<[RegExp, string]>(); + const paramRe = /^AssetParameters([a-zA-Z0-9]{64})(S3Bucket|S3VersionKey|ArtifactHash)([a-zA-Z0-9]{8})$/; + + const assetsSeen = new Set(); + for (const paramName of Object.keys(template?.Parameters || {})) { + const m = paramRe.exec(paramName); + if (!m) { continue; } + if (assetsSeen.has(m[1])) { continue; } + + assetsSeen.add(m[1]); + const ix = assetsSeen.size; + + // Full parameter reference + stringSubstitutions.push([ + new RegExp(`AssetParameters${m[1]}(S3Bucket|S3VersionKey|ArtifactHash)([a-zA-Z0-9]{8})`), + `Asset${ix}$1`, + ]); + // Substring asset hash reference + stringSubstitutions.push([ + new RegExp(`${m[1]}`), + `Asset${ix}Hash`, + ]); + } + + // Substitute them out + return substitute(template); + + function substitute(what: any): any { + if (Array.isArray(what)) { + return what.map(substitute); + } + + if (typeof what === 'object' && what !== null) { + const ret: any = {}; + for (const [k, v] of Object.entries(what)) { + ret[stringSub(k)] = substitute(v); + } + return ret; + } + + if (typeof what === 'string') { + return stringSub(what); + } + + return what; + } + + function stringSub(x: string) { + for (const [re, replacement] of stringSubstitutions) { + x = x.replace(re, replacement); + } + return x; + } +} diff --git a/packages/@aws-cdk/assert-internal/lib/expect.ts b/packages/@aws-cdk/assert-internal/lib/expect.ts new file mode 100644 index 0000000000000..21dd7e011c826 --- /dev/null +++ b/packages/@aws-cdk/assert-internal/lib/expect.ts @@ -0,0 +1,12 @@ +import * as cdk from '@aws-cdk/core'; +import * as api from '@aws-cdk/cx-api'; +import { StackInspector } from './inspector'; +import { SynthUtils } from './synth-utils'; + +export function expect(stack: api.CloudFormationStackArtifact | cdk.Stack | Record, skipValidation = false): StackInspector { + // if this is already a synthesized stack, then just inspect it. + const artifact = stack instanceof api.CloudFormationStackArtifact ? stack + : cdk.Stack.isStack(stack) ? SynthUtils._synthesizeWithNested(stack, { skipValidation }) + : stack; // This is a template already + return new StackInspector(artifact); +} diff --git a/packages/@aws-cdk/assert-internal/lib/index.ts b/packages/@aws-cdk/assert-internal/lib/index.ts new file mode 100644 index 0000000000000..902a5c222f003 --- /dev/null +++ b/packages/@aws-cdk/assert-internal/lib/index.ts @@ -0,0 +1,15 @@ +export * from './assertion'; +export * from './canonicalize-assets'; +export * from './expect'; +export * from './inspector'; +export * from './synth-utils'; + +export * from './assertions/exist'; +export * from './assertions/have-output'; +export * from './assertions/have-resource'; +export * from './assertions/have-resource-matchers'; +export * from './assertions/have-type'; +export * from './assertions/match-template'; +export * from './assertions/and-assertion'; +export * from './assertions/negated-assertion'; +export * from './assertions/count-resources'; diff --git a/packages/@aws-cdk/assert-internal/lib/inspector.ts b/packages/@aws-cdk/assert-internal/lib/inspector.ts new file mode 100644 index 0000000000000..f633de428f4f2 --- /dev/null +++ b/packages/@aws-cdk/assert-internal/lib/inspector.ts @@ -0,0 +1,74 @@ +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import * as api from '@aws-cdk/cx-api'; +import { Assertion, not } from './assertion'; +import { MatchStyle, matchTemplate } from './assertions/match-template'; + +export abstract class Inspector { + public aroundAssert?: (cb: () => void) => any; + + constructor() { + this.aroundAssert = undefined; + } + + public to(assertion: Assertion): any { + return this.aroundAssert ? this.aroundAssert(() => this._to(assertion)) + : this._to(assertion); + } + + public notTo(assertion: Assertion): any { + return this.to(not(assertion)); + } + + abstract get value(): any; + + private _to(assertion: Assertion): any { + assertion.assertOrThrow(this); + } +} + +export class StackInspector extends Inspector { + + private readonly template: { [key: string]: any }; + + constructor(public readonly stack: api.CloudFormationStackArtifact | object) { + super(); + + this.template = stack instanceof api.CloudFormationStackArtifact ? stack.template : stack; + } + + public at(path: string | string[]): StackPathInspector { + if (!(this.stack instanceof api.CloudFormationStackArtifact)) { + throw new Error('Cannot use "expect(stack).at(path)" for a raw template, only CloudFormationStackArtifact'); + } + + const strPath = typeof path === 'string' ? path : path.join('/'); + return new StackPathInspector(this.stack, strPath); + } + + public toMatch(template: { [key: string]: any }, matchStyle = MatchStyle.EXACT) { + return this.to(matchTemplate(template, matchStyle)); + } + + public get value(): { [key: string]: any } { + return this.template; + } +} + +export class StackPathInspector extends Inspector { + constructor(public readonly stack: api.CloudFormationStackArtifact, public readonly path: string) { + super(); + } + + public get value(): { [key: string]: any } | undefined { + // The names of paths in metadata in tests are very ill-defined. Try with the full path first, + // then try with the stack name preprended for backwards compat with most tests that happen to give + // their stack an ID that's the same as the stack name. + const metadata = this.stack.manifest.metadata || {}; + const md = metadata[this.path] || metadata[`/${this.stack.id}${this.path}`]; + if (md === undefined) { return undefined; } + const resourceMd = md.find(entry => entry.type === cxschema.ArtifactMetadataEntryType.LOGICAL_ID); + if (resourceMd === undefined) { return undefined; } + const logicalId = resourceMd.data as cxschema.LogMessageMetadataEntry; + return this.stack.template.Resources[logicalId]; + } +} diff --git a/packages/@aws-cdk/assert-internal/lib/synth-utils.ts b/packages/@aws-cdk/assert-internal/lib/synth-utils.ts new file mode 100644 index 0000000000000..bb8d9a437afd9 --- /dev/null +++ b/packages/@aws-cdk/assert-internal/lib/synth-utils.ts @@ -0,0 +1,87 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as core from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; + +export class SynthUtils { + /** + * Returns the cloud assembly template artifact for a stack. + */ + public static synthesize(stack: core.Stack, options: core.SynthesisOptions = { }): cxapi.CloudFormationStackArtifact { + // always synthesize against the root (be it an App or whatever) so all artifacts will be included + const assembly = synthesizeApp(stack, options); + return assembly.getStackArtifact(stack.artifactId); + } + + /** + * Synthesizes the stack and returns the resulting CloudFormation template. + */ + public static toCloudFormation(stack: core.Stack, options: core.SynthesisOptions = { }): any { + const synth = this._synthesizeWithNested(stack, options); + if (synth instanceof cxapi.CloudFormationStackArtifact) { + return synth.template; + } else { + return synth; + } + } + + /** + * @returns Returns a subset of the synthesized CloudFormation template (only specific resource types). + */ + public static subset(stack: core.Stack, options: SubsetOptions): any { + const template = this.toCloudFormation(stack); + if (template.Resources) { + for (const [key, resource] of Object.entries(template.Resources)) { + if (options.resourceTypes && !options.resourceTypes.includes((resource as any).Type)) { + delete template.Resources[key]; + } + } + } + + return template; + } + + /** + * Synthesizes the stack and returns a `CloudFormationStackArtifact` which can be inspected. + * Supports nested stacks as well as normal stacks. + * + * @return CloudFormationStackArtifact for normal stacks or the actual template for nested stacks + * @internal + */ + public static _synthesizeWithNested(stack: core.Stack, options: core.SynthesisOptions = { }): cxapi.CloudFormationStackArtifact | object { + // always synthesize against the root (be it an App or whatever) so all artifacts will be included + const assembly = synthesizeApp(stack, options); + + // if this is a nested stack (it has a parent), then just read the template as a string + if (stack.nestedStackParent) { + return JSON.parse(fs.readFileSync(path.join(assembly.directory, stack.templateFile)).toString('utf-8')); + } + + return assembly.getStackArtifact(stack.artifactId); + } +} + +/** + * Synthesizes the app in which a stack resides and returns the cloud assembly object. + */ +function synthesizeApp(stack: core.Stack, options: core.SynthesisOptions) { + const root = stack.node.root; + if (!core.Stage.isStage(root)) { + throw new Error('unexpected: all stacks must be part of a Stage or an App'); + } + + // to support incremental assertions (i.e. "expect(stack).toNotContainSomething(); doSomething(); expect(stack).toContainSomthing()") + const force = true; + + return root.synth({ + force, + ...options, + }); +} + +export interface SubsetOptions { + /** + * Match all resources of the given type + */ + resourceTypes?: string[]; +} diff --git a/packages/@aws-cdk/assert-internal/package.json b/packages/@aws-cdk/assert-internal/package.json new file mode 100644 index 0000000000000..e707c61d7f13b --- /dev/null +++ b/packages/@aws-cdk/assert-internal/package.json @@ -0,0 +1,67 @@ +{ + "name": "@aws-cdk/assert-internal", + "private": true, + "version": "0.0.0", + "description": "An assertion library for use with CDK Apps", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "pkglint": "pkglint -f", + "package": "cdk-package", + "build+test+package": "yarn build+test && yarn package", + "build+test": "yarn build && yarn test" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@types/jest": "^26.0.20", + "cdk-build-tools": "0.0.0", + "jest": "^26.6.3", + "pkglint": "0.0.0", + "ts-jest": "^26.5.3" + }, + "dependencies": { + "@aws-cdk/cloud-assembly-schema": "0.0.0", + "@aws-cdk/cloudformation-diff": "0.0.0", + "@aws-cdk/core": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", + "constructs": "^3.3.69" + }, + "peerDependencies": { + "@aws-cdk/core": "0.0.0", + "constructs": "^3.3.69", + "jest": "^26.6.3" + }, + "repository": { + "url": "https://github.com/aws/aws-cdk.git", + "type": "git", + "directory": "packages/@aws-cdk/assert-internal" + }, + "keywords": [ + "aws", + "cdk" + ], + "homepage": "https://github.com/aws/aws-cdk", + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "experimental", + "cdk-build": { + "jest": true + }, + "publishConfig": { + "tag": "latest" + }, + "ubergen": { + "exclude": true + } +} diff --git a/packages/@aws-cdk/assert-internal/test/assertions.test.ts b/packages/@aws-cdk/assert-internal/test/assertions.test.ts new file mode 100644 index 0000000000000..bd20d60032d76 --- /dev/null +++ b/packages/@aws-cdk/assert-internal/test/assertions.test.ts @@ -0,0 +1,349 @@ +import * as cdk from '@aws-cdk/core'; +import * as cx from '@aws-cdk/cx-api'; +import * as constructs from 'constructs'; + +import { countResources, countResourcesLike, exist, expect as cdkExpect, haveType, MatchStyle, matchTemplate } from '../lib/index'; + +passingExample('expect at to have ', () => { + const resourceType = 'Test::Resource'; + const synthStack = synthesizedStack(stack => { + new TestResource(stack, 'TestResource', { type: resourceType }); + }); + cdkExpect(synthStack).at('/TestResource').to(haveType(resourceType)); +}); +passingExample('expect non-synthesized stack at to have ', () => { + const resourceType = 'Test::Resource'; + const stack = new cdk.Stack(); + new TestResource(stack, 'TestResource', { type: resourceType }); + cdkExpect(stack).at('/TestResource').to(haveType(resourceType)); +}); +passingExample('expect at *not* to have ', () => { + const resourceType = 'Test::Resource'; + const synthStack = synthesizedStack(stack => { + new TestResource(stack, 'TestResource', { type: resourceType }); + }); + cdkExpect(synthStack).at('/TestResource').notTo(haveType('Foo::Bar')); +}); +passingExample('expect at to exist', () => { + const resourceType = 'Test::Resource'; + const synthStack = synthesizedStack(stack => { + new TestResource(stack, 'TestResource', { type: resourceType }); + }); + cdkExpect(synthStack).at('/TestResource').to(exist()); +}); +passingExample('expect to match (exactly)