From f4552733909cd0734a7d829a35d0c1277b2ca4fc Mon Sep 17 00:00:00 2001 From: DRNagar <62946759+DRNagar@users.noreply.github.com> Date: Thu, 4 Jun 2020 17:40:38 +0200 Subject: [PATCH 01/32] fix(apigateway): authorizerUri does not resolve to the correct partition (#8152) Add that the authorizerURI includes the correct partition. Previously, it always used the aws partition. fixes #8098 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-apigateway/lib/authorizers/lambda.ts | 11 +- .../aws-apigateway/lib/integration.ts | 4 +- .../integ.request-authorizer.expected.json | 54 ++++---- ...eg.token-authorizer-iam-role.expected.json | 54 ++++---- .../integ.token-authorizer.expected.json | 54 ++++---- .../test/authorizers/test.lambda.ts | 120 ++++++++++++++++++ .../test/integ.cors.expected.json | 56 ++++---- .../test/integ.restapi.books.expected.json | 60 ++++----- .../test/integ.restapi.defaults.expected.json | 48 +++---- .../test/integ.restapi.expected.json | 90 ++++++------- .../integ.restapi.multistack.expected.json | 52 ++++---- .../test/integ.restapi.multiuse.expected.json | 100 +++++++-------- .../integ.restapi.vpc-endpoint.expected.json | 48 +++---- 13 files changed, 445 insertions(+), 306 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts index 70d5408009700..9215c28de1e61 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts @@ -170,7 +170,7 @@ export class TokenAuthorizer extends LambdaAuthorizer { name: props.authorizerName ?? this.node.uniqueId, restApiId, type: 'TOKEN', - authorizerUri: `arn:aws:apigateway:${Stack.of(this).region}:lambda:path/2015-03-31/functions/${props.handler.functionArn}/invocations`, + authorizerUri: lambdaAuthorizerArn(props.handler), authorizerCredentials: props.assumeRole?.roleArn, authorizerResultTtlInSeconds: props.resultsCacheTtl?.toSeconds(), identitySource: props.identitySource || 'method.request.header.Authorization', @@ -232,7 +232,7 @@ export class RequestAuthorizer extends LambdaAuthorizer { name: props.authorizerName ?? this.node.uniqueId, restApiId, type: 'REQUEST', - authorizerUri: `arn:aws:apigateway:${Stack.of(this).region}:lambda:path/2015-03-31/functions/${props.handler.functionArn}/invocations`, + authorizerUri: lambdaAuthorizerArn(props.handler), authorizerCredentials: props.assumeRole?.roleArn, authorizerResultTtlInSeconds: props.resultsCacheTtl?.toSeconds(), identitySource: props.identitySources.map(is => is.toString()).join(','), @@ -248,3 +248,10 @@ export class RequestAuthorizer extends LambdaAuthorizer { this.setupPermissions(); } } + +/** + * constructs the authorizerURIArn. + */ +function lambdaAuthorizerArn(handler: lambda.IFunction) { + return `arn:${Stack.of(handler).partition}:apigateway:${Stack.of(handler).region}:lambda:path/2015-03-31/functions/${handler.functionArn}/invocations`; +} diff --git a/packages/@aws-cdk/aws-apigateway/lib/integration.ts b/packages/@aws-cdk/aws-apigateway/lib/integration.ts index 05356a57a861e..d7a9ec74f3b34 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integration.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integration.ts @@ -113,9 +113,9 @@ export interface IntegrationProps { * - If you specify HTTP for the `type` property, specify the API endpoint URL. * - If you specify MOCK for the `type` property, don't specify this property. * - If you specify AWS for the `type` property, specify an AWS service that - * follows this form: `arn:aws:apigateway:region:subdomain.service|service:path|action/service_api.` + * follows this form: `arn:partition:apigateway:region:subdomain.service|service:path|action/service_api.` * For example, a Lambda function URI follows this form: - * arn:aws:apigateway:region:lambda:path/path. The path is usually in the + * arn:partition:apigateway:region:lambda:path/path. The path is usually in the * form /2015-03-31/functions/LambdaFunctionARN/invocations. * * @see https://docs.aws.amazon.com/apigateway/api-reference/resource/integration/#uri diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.expected.json index 25995111b8677..89ab550818465 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.expected.json @@ -131,30 +131,6 @@ "Name": "MyRestApi" } }, - "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "MyRestApi2D1F47A9" - }, - "Description": "Automatically created by the RestApi construct" - }, - "DependsOn": [ - "MyRestApiANY05143F93" - ] - }, - "MyRestApiDeploymentStageprodC33B8E5F": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "MyRestApi2D1F47A9" - }, - "DeploymentId": { - "Ref": "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb" - }, - "StageName": "prod" - } - }, "MyRestApiCloudWatchRoleD4042E8E": { "Type": "AWS::IAM::Role", "Properties": { @@ -200,6 +176,30 @@ "MyRestApi2D1F47A9" ] }, + "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "MyRestApiANY05143F93" + ] + }, + "MyRestApiDeploymentStageprodC33B8E5F": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "DeploymentId": { + "Ref": "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb" + }, + "StageName": "prod" + } + }, "MyRestApiANY05143F93": { "Type": "AWS::ApiGateway::Method", "Properties": { @@ -247,7 +247,11 @@ "Fn::Join": [ "", [ - "arn:aws:apigateway:", + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", { "Ref": "AWS::Region" }, diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json index 97105a9490e83..339f10a1d17e0 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json @@ -119,7 +119,11 @@ "Fn::Join": [ "", [ - "arn:aws:apigateway:", + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", { "Ref": "AWS::Region" }, @@ -170,30 +174,6 @@ "Name": "MyRestApi" } }, - "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "MyRestApi2D1F47A9" - }, - "Description": "Automatically created by the RestApi construct" - }, - "DependsOn": [ - "MyRestApiANY05143F93" - ] - }, - "MyRestApiDeploymentStageprodC33B8E5F": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "MyRestApi2D1F47A9" - }, - "DeploymentId": { - "Ref": "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb" - }, - "StageName": "prod" - } - }, "MyRestApiCloudWatchRoleD4042E8E": { "Type": "AWS::IAM::Role", "Properties": { @@ -239,6 +219,30 @@ "MyRestApi2D1F47A9" ] }, + "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "MyRestApiANY05143F93" + ] + }, + "MyRestApiDeploymentStageprodC33B8E5F": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "DeploymentId": { + "Ref": "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb" + }, + "StageName": "prod" + } + }, "MyRestApiANY05143F93": { "Type": "AWS::ApiGateway::Method", "Properties": { diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.expected.json index 79102afef29f4..0d4f784d0362d 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.expected.json @@ -131,30 +131,6 @@ "Name": "MyRestApi" } }, - "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "MyRestApi2D1F47A9" - }, - "Description": "Automatically created by the RestApi construct" - }, - "DependsOn": [ - "MyRestApiANY05143F93" - ] - }, - "MyRestApiDeploymentStageprodC33B8E5F": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "MyRestApi2D1F47A9" - }, - "DeploymentId": { - "Ref": "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb" - }, - "StageName": "prod" - } - }, "MyRestApiCloudWatchRoleD4042E8E": { "Type": "AWS::IAM::Role", "Properties": { @@ -200,6 +176,30 @@ "MyRestApi2D1F47A9" ] }, + "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "MyRestApiANY05143F93" + ] + }, + "MyRestApiDeploymentStageprodC33B8E5F": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "DeploymentId": { + "Ref": "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb" + }, + "StageName": "prod" + } + }, "MyRestApiANY05143F93": { "Type": "AWS::ApiGateway::Method", "Properties": { @@ -247,7 +247,11 @@ "Fn::Join": [ "", [ - "arn:aws:apigateway:", + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", { "Ref": "AWS::Region" }, diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/test.lambda.ts b/packages/@aws-cdk/aws-apigateway/test/authorizers/test.lambda.ts index 83a2ff959d9be..4741647d25347 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/test.lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/test.lambda.ts @@ -29,6 +29,26 @@ export = { Type: 'TOKEN', RestApiId: stack.resolve(restApi.restApiId), IdentitySource: 'method.request.header.Authorization', + AuthorizerUri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':lambda:path/2015-03-31/functions/', + { + 'Fn::GetAtt': ['myfunction9B95E948', 'Arn'], + }, + '/invocations', + ], + ], + }, })); expect(stack).to(haveResource('AWS::Lambda::Permission', { @@ -65,6 +85,26 @@ export = { expect(stack).to(haveResource('AWS::ApiGateway::Authorizer', { Type: 'REQUEST', RestApiId: stack.resolve(restApi.restApiId), + AuthorizerUri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':lambda:path/2015-03-31/functions/', + { + 'Fn::GetAtt': ['myfunction9B95E948', 'Arn'], + }, + '/invocations', + ], + ], + }, })); expect(stack).to(haveResource('AWS::Lambda::Permission', { @@ -125,6 +165,26 @@ export = { IdentityValidationExpression: 'a-hacker', Name: 'myauthorizer', AuthorizerResultTtlInSeconds: 60, + AuthorizerUri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':lambda:path/2015-03-31/functions/', + { + 'Fn::GetAtt': ['myfunction9B95E948', 'Arn'], + }, + '/invocations', + ], + ], + }, })); test.done(); @@ -158,6 +218,26 @@ export = { IdentitySource: 'method.request.header.whoami', Name: 'myauthorizer', AuthorizerResultTtlInSeconds: 60, + AuthorizerUri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':lambda:path/2015-03-31/functions/', + { + 'Fn::GetAtt': ['myfunction9B95E948', 'Arn'], + }, + '/invocations', + ], + ], + }, })); test.done(); @@ -191,6 +271,26 @@ export = { expect(stack).to(haveResource('AWS::ApiGateway::Authorizer', { Type: 'TOKEN', RestApiId: stack.resolve(restApi.restApiId), + AuthorizerUri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':lambda:path/2015-03-31/functions/', + { + 'Fn::GetAtt': ['myfunction9B95E948', 'Arn'], + }, + '/invocations', + ], + ], + }, })); expect(stack).to(haveResource('AWS::IAM::Role')); @@ -245,6 +345,26 @@ export = { expect(stack).to(haveResource('AWS::ApiGateway::Authorizer', { Type: 'REQUEST', RestApiId: stack.resolve(restApi.restApiId), + AuthorizerUri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':lambda:path/2015-03-31/functions/', + { + 'Fn::GetAtt': ['myfunction9B95E948', 'Arn'], + }, + '/invocations', + ], + ], + }, })); expect(stack).to(haveResource('AWS::IAM::Role')); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json index 043b4d20bea46..2cbc9c1ebbbb8 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json @@ -6,34 +6,6 @@ "Name": "cors-api-test" } }, - "corsapitestDeployment2BF1633A228079ea05e5799220dd4ca13512b92d": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "corsapitest8682546E" - }, - "Description": "Automatically created by the RestApi construct" - }, - "DependsOn": [ - "corsapitesttwitchDELETEB4C94228", - "corsapitesttwitchGET4270341B", - "corsapitesttwitchOPTIONSE5EEB527", - "corsapitesttwitchPOSTB52CFB02", - "corsapitesttwitch0E3D1559" - ] - }, - "corsapitestDeploymentStageprod8F31F2AB": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "corsapitest8682546E" - }, - "DeploymentId": { - "Ref": "corsapitestDeployment2BF1633A228079ea05e5799220dd4ca13512b92d" - }, - "StageName": "prod" - } - }, "corsapitestCloudWatchRole9AF5A81A": { "Type": "AWS::IAM::Role", "Properties": { @@ -79,6 +51,34 @@ "corsapitest8682546E" ] }, + "corsapitestDeployment2BF1633A228079ea05e5799220dd4ca13512b92d": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "corsapitest8682546E" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "corsapitesttwitchDELETEB4C94228", + "corsapitesttwitchGET4270341B", + "corsapitesttwitchOPTIONSE5EEB527", + "corsapitesttwitchPOSTB52CFB02", + "corsapitesttwitch0E3D1559" + ] + }, + "corsapitestDeploymentStageprod8F31F2AB": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "corsapitest8682546E" + }, + "DeploymentId": { + "Ref": "corsapitestDeployment2BF1633A228079ea05e5799220dd4ca13512b92d" + }, + "StageName": "prod" + } + }, "corsapitesttwitch0E3D1559": { "Type": "AWS::ApiGateway::Resource", "Properties": { diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json index 0d471973c58ca..8b679bd6c6239 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json @@ -156,36 +156,6 @@ "Name": "books-api" } }, - "booksapiDeployment308B08F132cc25cf8168bd5e99b9e6d4915866b5": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "booksapiE1885304" - }, - "Description": "Automatically created by the RestApi construct" - }, - "DependsOn": [ - "booksapiANYF4F0CDEB", - "booksapibooksbookidDELETE214F4059", - "booksapibooksbookidGETCCE21986", - "booksapibooksbookid5264BCA2", - "booksapibooksGETA776447A", - "booksapibooksPOSTF6C6559D", - "booksapibooks97D84727" - ] - }, - "booksapiDeploymentStageprod55D8E03E": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "booksapiE1885304" - }, - "DeploymentId": { - "Ref": "booksapiDeployment308B08F132cc25cf8168bd5e99b9e6d4915866b5" - }, - "StageName": "prod" - } - }, "booksapiCloudWatchRole089CB225": { "Type": "AWS::IAM::Role", "Properties": { @@ -231,6 +201,36 @@ "booksapiE1885304" ] }, + "booksapiDeployment308B08F132cc25cf8168bd5e99b9e6d4915866b5": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "booksapiE1885304" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "booksapiANYF4F0CDEB", + "booksapibooksbookidDELETE214F4059", + "booksapibooksbookidGETCCE21986", + "booksapibooksbookid5264BCA2", + "booksapibooksGETA776447A", + "booksapibooksPOSTF6C6559D", + "booksapibooks97D84727" + ] + }, + "booksapiDeploymentStageprod55D8E03E": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "booksapiE1885304" + }, + "DeploymentId": { + "Ref": "booksapiDeployment308B08F132cc25cf8168bd5e99b9e6d4915866b5" + }, + "StageName": "prod" + } + }, "booksapiANYApiPermissionrestapibooksexamplebooksapi4538F335ANY73B3CDDC": { "Type": "AWS::Lambda::Permission", "Properties": { diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json index bf73644303e7d..ddc281809028d 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json @@ -6,30 +6,6 @@ "Name": "my-api" } }, - "myapiDeployment92F2CB4972a890db5063ec679071ba7eefc76f2a": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "myapi4C7BF186" - }, - "Description": "Automatically created by the RestApi construct" - }, - "DependsOn": [ - "myapiGETF990CE3C" - ] - }, - "myapiDeploymentStageprod298F01AF": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "myapi4C7BF186" - }, - "DeploymentId": { - "Ref": "myapiDeployment92F2CB4972a890db5063ec679071ba7eefc76f2a" - }, - "StageName": "prod" - } - }, "myapiCloudWatchRole095452E5": { "Type": "AWS::IAM::Role", "Properties": { @@ -75,6 +51,30 @@ "myapi4C7BF186" ] }, + "myapiDeployment92F2CB4972a890db5063ec679071ba7eefc76f2a": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "myapiGETF990CE3C" + ] + }, + "myapiDeploymentStageprod298F01AF": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "DeploymentId": { + "Ref": "myapiDeployment92F2CB4972a890db5063ec679071ba7eefc76f2a" + }, + "StageName": "prod" + } + }, "myapiGETF990CE3C": { "Type": "AWS::ApiGateway::Method", "Properties": { 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 9758c8c2e1b00..91af3471593eb 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json @@ -6,6 +6,51 @@ "Name": "my-api" } }, + "myapiCloudWatchRole095452E5": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + } + }, + "myapiAccountEC421A0A": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "myapiCloudWatchRole095452E5", + "Arn" + ] + } + }, + "DependsOn": [ + "myapi4C7BF186" + ] + }, "myapiDeployment92F2CB4963d40685c54c6f8da21d80a83f16d3d5": { "Type": "AWS::ApiGateway::Deployment", "Properties": { @@ -57,51 +102,6 @@ "StageName": "beta" } }, - "myapiCloudWatchRole095452E5": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "apigateway.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "ManagedPolicyArns": [ - { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" - ] - ] - } - ] - } - }, - "myapiAccountEC421A0A": { - "Type": "AWS::ApiGateway::Account", - "Properties": { - "CloudWatchRoleArn": { - "Fn::GetAtt": [ - "myapiCloudWatchRole095452E5", - "Arn" - ] - } - }, - "DependsOn": [ - "myapi4C7BF186" - ] - }, "myapiv113487378": { "Type": "AWS::ApiGateway::Resource", "Properties": { diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multistack.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multistack.expected.json index 23a4100da8156..3404f37880155 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multistack.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multistack.expected.json @@ -75,32 +75,6 @@ "Name": "SecondRestAPI" } }, - "BooksApiDeployment86CA39AF7e6c771d47a1a3777eba99bffc037822": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "BooksApi60AC975F" - }, - "Description": "Automatically created by the RestApi construct" - }, - "DependsOn": [ - "BooksApiANY0C4EABE3", - "BooksApibooksGET6066BF7E", - "BooksApibooks1F745538" - ] - }, - "BooksApiDeploymentStageprod0693B760": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "BooksApi60AC975F" - }, - "DeploymentId": { - "Ref": "BooksApiDeployment86CA39AF7e6c771d47a1a3777eba99bffc037822" - }, - "StageName": "prod" - } - }, "BooksApiCloudWatchRoleB120ADBA": { "Type": "AWS::IAM::Role", "Properties": { @@ -146,6 +120,32 @@ "BooksApi60AC975F" ] }, + "BooksApiDeployment86CA39AF7e6c771d47a1a3777eba99bffc037822": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "BooksApi60AC975F" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "BooksApiANY0C4EABE3", + "BooksApibooksGET6066BF7E", + "BooksApibooks1F745538" + ] + }, + "BooksApiDeploymentStageprod0693B760": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "BooksApi60AC975F" + }, + "DeploymentId": { + "Ref": "BooksApiDeployment86CA39AF7e6c771d47a1a3777eba99bffc037822" + }, + "StageName": "prod" + } + }, "BooksApiANY0C4EABE3": { "Type": "AWS::ApiGateway::Method", "Properties": { diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multiuse.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multiuse.expected.json index eb403f6d94d59..6a7cea680ef60 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multiuse.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multiuse.expected.json @@ -56,31 +56,6 @@ "Name": "hello-api" } }, - "helloapiDeploymentFA89AEEC3622d8c965f356a33fd95586d24bf138": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "helloapi4446A35B" - }, - "Description": "Automatically created by the RestApi construct" - }, - "DependsOn": [ - "helloapihelloGETE6A58337", - "helloapihello4AA00177" - ] - }, - "helloapiDeploymentStageprod677E2C4F": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "helloapi4446A35B" - }, - "DeploymentId": { - "Ref": "helloapiDeploymentFA89AEEC3622d8c965f356a33fd95586d24bf138" - }, - "StageName": "prod" - } - }, "helloapiCloudWatchRoleD13E913E": { "Type": "AWS::IAM::Role", "Properties": { @@ -126,6 +101,31 @@ "helloapi4446A35B" ] }, + "helloapiDeploymentFA89AEEC3622d8c965f356a33fd95586d24bf138": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "helloapi4446A35B" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "helloapihelloGETE6A58337", + "helloapihello4AA00177" + ] + }, + "helloapiDeploymentStageprod677E2C4F": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "helloapi4446A35B" + }, + "DeploymentId": { + "Ref": "helloapiDeploymentFA89AEEC3622d8c965f356a33fd95586d24bf138" + }, + "StageName": "prod" + } + }, "helloapihello4AA00177": { "Type": "AWS::ApiGateway::Resource", "Properties": { @@ -265,31 +265,6 @@ "Name": "second-api" } }, - "secondapiDeployment20F2C70088fa5a027620045bea3e5043c6d31f5a": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "secondapi730EF3C7" - }, - "Description": "Automatically created by the RestApi construct" - }, - "DependsOn": [ - "secondapihelloGETDC5BBB18", - "secondapihello7264EB69" - ] - }, - "secondapiDeploymentStageprod40491DF0": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "secondapi730EF3C7" - }, - "DeploymentId": { - "Ref": "secondapiDeployment20F2C70088fa5a027620045bea3e5043c6d31f5a" - }, - "StageName": "prod" - } - }, "secondapiCloudWatchRole7FEC1028": { "Type": "AWS::IAM::Role", "Properties": { @@ -335,6 +310,31 @@ "secondapi730EF3C7" ] }, + "secondapiDeployment20F2C70088fa5a027620045bea3e5043c6d31f5a": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "secondapi730EF3C7" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "secondapihelloGETDC5BBB18", + "secondapihello7264EB69" + ] + }, + "secondapiDeploymentStageprod40491DF0": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "secondapi730EF3C7" + }, + "DeploymentId": { + "Ref": "secondapiDeployment20F2C70088fa5a027620045bea3e5043c6d31f5a" + }, + "StageName": "prod" + } + }, "secondapihello7264EB69": { "Type": "AWS::ApiGateway::Resource", "Properties": { diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.vpc-endpoint.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.vpc-endpoint.expected.json index 21b7c8298d601..872513b9b89a2 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.vpc-endpoint.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.vpc-endpoint.expected.json @@ -631,30 +631,6 @@ } } }, - "MyApiDeploymentECB0D05E7a475a505b0c925e193030293593b6dc": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "MyApi49610EDF" - }, - "Description": "Automatically created by the RestApi construct" - }, - "DependsOn": [ - "MyApiGETD0C7AA0C" - ] - }, - "MyApiDeploymentStageprodE1054AF0": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "MyApi49610EDF" - }, - "DeploymentId": { - "Ref": "MyApiDeploymentECB0D05E7a475a505b0c925e193030293593b6dc" - }, - "StageName": "prod" - } - }, "MyApiCloudWatchRole2BEC1A9C": { "Type": "AWS::IAM::Role", "Properties": { @@ -700,6 +676,30 @@ "MyApi49610EDF" ] }, + "MyApiDeploymentECB0D05E7a475a505b0c925e193030293593b6dc": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "MyApi49610EDF" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "MyApiGETD0C7AA0C" + ] + }, + "MyApiDeploymentStageprodE1054AF0": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "MyApi49610EDF" + }, + "DeploymentId": { + "Ref": "MyApiDeploymentECB0D05E7a475a505b0c925e193030293593b6dc" + }, + "StageName": "prod" + } + }, "MyApiGETD0C7AA0C": { "Type": "AWS::ApiGateway::Method", "Properties": { From 4c6a4134f2d8e119533fc67043b33cc1129d3e15 Mon Sep 17 00:00:00 2001 From: Romain Marcadier Date: Thu, 4 Jun 2020 19:00:09 +0200 Subject: [PATCH 02/32] chore: remove monocdk-experiment build exceptions (#8366) MonoCDK was previously using a special packaging process, but this was changed to use the standard packaging process used by any other CDK library. It is thus no longer necessary to apply those exceptions which risk making the build slower. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- pack.sh | 3 +-- packages/@monocdk-experiment/assert/clone.sh | 5 ----- scripts/list-packages | 3 +-- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/pack.sh b/pack.sh index 596e81c9bd65d..02b901f141273 100755 --- a/pack.sh +++ b/pack.sh @@ -15,8 +15,7 @@ rm -fr ${distdir} mkdir -p ${distdir} # Split out jsii and non-jsii packages. Jsii packages will be built all at once. -# Non-jsii packages will be run individually. Note that currently the monoCDK -# package is handled as non-jsii because of the way it is packaged. +# Non-jsii packages will be run individually. echo "Collecting package list..." >&2 scripts/list-packages $TMPDIR/jsii.txt $TMPDIR/nonjsii.txt diff --git a/packages/@monocdk-experiment/assert/clone.sh b/packages/@monocdk-experiment/assert/clone.sh index 6588be2545a76..9cf4731c69f64 100755 --- a/packages/@monocdk-experiment/assert/clone.sh +++ b/packages/@monocdk-experiment/assert/clone.sh @@ -14,8 +14,3 @@ for file in ${files}; do done npx rewrite-imports "**/*.ts" - -# symlink the full staged monocdk from the staging directory to node_modules -rm -fr node_modules/monocdk-experiment -mkdir -p node_modules -ln -s $PWD/../../monocdk-experiment/staging node_modules/monocdk-experiment diff --git a/scripts/list-packages b/scripts/list-packages index b4d16d6d8fc52..95a2c5ecc0379 100755 --- a/scripts/list-packages +++ b/scripts/list-packages @@ -24,8 +24,7 @@ child_process.exec('lerna ls --toposort --json', { shell: true }, (error, stdout for (const module of modules) { const pkgJson = require(path.join(module.location, 'package.json')); - // MonoCDK-Experiment does its own packaging, should be handled "non-JSII style" - if (pkgJson.jsii && pkgJson.name !== 'monocdk-experiment') { + if (pkgJson.jsii) { jsiiDirectories.push(module.location); } else { nonJsiiNames.push(pkgJson.name); From 495c082b0cf9ab65830df4d25b9ecee687d325ca Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2020 18:13:01 +0000 Subject: [PATCH 03/32] chore(deps): bump ts-jest from 26.0.0 to 26.1.0 (#8378) Bumps [ts-jest](https://github.com/kulshekhar/ts-jest) from 26.0.0 to 26.1.0. - [Release notes](https://github.com/kulshekhar/ts-jest/releases) - [Changelog](https://github.com/kulshekhar/ts-jest/blob/master/CHANGELOG.md) - [Commits](https://github.com/kulshekhar/ts-jest/compare/v26.0.0...v26.1.0) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- packages/@aws-cdk/assert/package.json | 2 +- packages/@aws-cdk/aws-dynamodb/package.json | 2 +- packages/@aws-cdk/aws-sam/package.json | 2 +- packages/@aws-cdk/cloudformation-diff/package.json | 2 +- packages/@aws-cdk/cloudformation-include/package.json | 2 +- packages/@monocdk-experiment/assert/package.json | 2 +- packages/aws-cdk/package.json | 2 +- tools/cdk-build-tools/package.json | 2 +- yarn.lock | 8 ++++---- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/@aws-cdk/assert/package.json b/packages/@aws-cdk/assert/package.json index bab4a3d1dd077..1ad7d397c5c6f 100644 --- a/packages/@aws-cdk/assert/package.json +++ b/packages/@aws-cdk/assert/package.json @@ -25,7 +25,7 @@ "cdk-build-tools": "0.0.0", "jest": "^25.5.4", "pkglint": "0.0.0", - "ts-jest": "^26.0.0" + "ts-jest": "^26.1.0" }, "dependencies": { "@aws-cdk/cloudformation-diff": "0.0.0", diff --git a/packages/@aws-cdk/aws-dynamodb/package.json b/packages/@aws-cdk/aws-dynamodb/package.json index 2b06e10f822bc..65075f253bcff 100644 --- a/packages/@aws-cdk/aws-dynamodb/package.json +++ b/packages/@aws-cdk/aws-dynamodb/package.json @@ -73,7 +73,7 @@ "jest": "^25.5.4", "pkglint": "0.0.0", "sinon": "^9.0.2", - "ts-jest": "^26.0.0" + "ts-jest": "^26.1.0" }, "dependencies": { "@aws-cdk/aws-applicationautoscaling": "0.0.0", diff --git a/packages/@aws-cdk/aws-sam/package.json b/packages/@aws-cdk/aws-sam/package.json index 9f13bd373b5d0..e61c29fcf3d35 100644 --- a/packages/@aws-cdk/aws-sam/package.json +++ b/packages/@aws-cdk/aws-sam/package.json @@ -70,7 +70,7 @@ "cfn2ts": "0.0.0", "jest": "^25.5.4", "pkglint": "0.0.0", - "ts-jest": "^26.0.0" + "ts-jest": "^26.1.0" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/cloudformation-diff/package.json b/packages/@aws-cdk/cloudformation-diff/package.json index f54b98f3d193b..bb31931b64aac 100644 --- a/packages/@aws-cdk/cloudformation-diff/package.json +++ b/packages/@aws-cdk/cloudformation-diff/package.json @@ -36,7 +36,7 @@ "fast-check": "^1.24.2", "jest": "^25.5.4", "pkglint": "0.0.0", - "ts-jest": "^26.0.0" + "ts-jest": "^26.1.0" }, "repository": { "url": "https://github.com/aws/aws-cdk.git", diff --git a/packages/@aws-cdk/cloudformation-include/package.json b/packages/@aws-cdk/cloudformation-include/package.json index 5389145c4941a..bf3be1afd8d19 100644 --- a/packages/@aws-cdk/cloudformation-include/package.json +++ b/packages/@aws-cdk/cloudformation-include/package.json @@ -307,7 +307,7 @@ "cdk-build-tools": "0.0.0", "jest": "^25.4.0", "pkglint": "0.0.0", - "ts-jest": "^26.0.0" + "ts-jest": "^26.1.0" }, "bundledDependencies": [ "yaml" diff --git a/packages/@monocdk-experiment/assert/package.json b/packages/@monocdk-experiment/assert/package.json index ebd3b55c4681b..5da40f1a0097f 100644 --- a/packages/@monocdk-experiment/assert/package.json +++ b/packages/@monocdk-experiment/assert/package.json @@ -41,7 +41,7 @@ "cdk-build-tools": "0.0.0", "jest": "^25.5.4", "pkglint": "0.0.0", - "ts-jest": "^26.0.0", + "ts-jest": "^26.1.0", "@monocdk-experiment/rewrite-imports": "0.0.0", "monocdk-experiment": "0.0.0", "constructs": "^3.0.2" diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 79c5d949040ae..cec9c904d18d9 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -61,7 +61,7 @@ "mockery": "^2.1.0", "pkglint": "0.0.0", "sinon": "^9.0.2", - "ts-jest": "^26.0.0", + "ts-jest": "^26.1.0", "ts-mock-imports": "^1.2.6" }, "dependencies": { diff --git a/tools/cdk-build-tools/package.json b/tools/cdk-build-tools/package.json index f66bf5459c3ea..b8ff407bc883f 100644 --- a/tools/cdk-build-tools/package.json +++ b/tools/cdk-build-tools/package.json @@ -53,7 +53,7 @@ "jsii-pacmak": "^1.6.0", "nodeunit": "^0.11.3", "nyc": "^15.0.1", - "ts-jest": "^26.0.0", + "ts-jest": "^26.1.0", "tslint": "^5.20.1", "typescript": "~3.8.3", "yargs": "^15.3.1", diff --git a/yarn.lock b/yarn.lock index 4c56b7dbcbe35..104cf74c0ae1c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9437,10 +9437,10 @@ trivial-deferred@^1.0.1: resolved "https://registry.yarnpkg.com/trivial-deferred/-/trivial-deferred-1.0.1.tgz#376d4d29d951d6368a6f7a0ae85c2f4d5e0658f3" integrity sha1-N21NKdlR1jaKb3oK6FwvTV4GWPM= -ts-jest@^26.0.0: - version "26.0.0" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.0.0.tgz#957b802978249aaf74180b9dcb17b4fd787ad6f3" - integrity sha512-eBpWH65mGgzobuw7UZy+uPP9lwu+tPp60o324ASRX4Ijg8UC5dl2zcge4kkmqr2Zeuk9FwIjvCTOPuNMEyGWWw== +ts-jest@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.1.0.tgz#e9070fc97b3ea5557a48b67c631c74eb35e15417" + integrity sha512-JbhQdyDMYN5nfKXaAwCIyaWLGwevcT2/dbqRPsQeh6NZPUuXjZQZEfeLb75tz0ubCIgEELNm6xAzTe5NXs5Y4Q== dependencies: bs-logger "0.x" buffer-from "1.x" From 8aca8558c77aac279ee4cc660a7223d7f8f10c64 Mon Sep 17 00:00:00 2001 From: Meng Xin Zhu Date: Fri, 5 Jun 2020 03:06:32 +0800 Subject: [PATCH 04/32] docs(efs): fix example code of efs (#8364) The example of EFS doc is using older naming of EFS L2 API, which can not be compiled any more. ---- *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-efs/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/@aws-cdk/aws-efs/README.md b/packages/@aws-cdk/aws-efs/README.md index 5142c02259b1c..dbbc97d8f9a07 100644 --- a/packages/@aws-cdk/aws-efs/README.md +++ b/packages/@aws-cdk/aws-efs/README.md @@ -19,12 +19,12 @@ This construct library allows you to set up AWS Elastic File System (EFS). import * as efs from '@aws-cdk/aws-efs'; const myVpc = new ec2.Vpc(this, 'VPC'); -const fileSystem = new efs.EfsFileSystem(this, 'MyEfsFileSystem', { +const fileSystem = new efs.FileSystem(this, 'MyEfsFileSystem', { vpc: myVpc, encrypted: true, - lifecyclePolicy: EfsLifecyclePolicyProperty.AFTER_14_DAYS, - performanceMode: EfsPerformanceMode.GENERAL_PURPOSE, - throughputMode: EfsThroughputMode.BURSTING + lifecyclePolicy: efs.LifecyclePolicy.AFTER_14_DAYS, + performanceMode: efs.PerformanceMode.GENERAL_PURPOSE, + throughputMode: efs.ThroughputMode.BURSTING }); ``` @@ -43,12 +43,12 @@ following code can be used as reference: ``` const vpc = new ec2.Vpc(this, 'VPC'); -const fileSystem = new efs.EfsFileSystem(this, 'EfsFileSystem', { +const fileSystem = new efs.FileSystem(this, 'MyEfsFileSystem', { vpc, encrypted: true, - lifecyclePolicy: efs.EfsLifecyclePolicyProperty.AFTER_14_DAYS, - performanceMode: efs.EfsPerformanceMode.GENERAL_PURPOSE, - throughputMode: efs.EfsThroughputMode.BURSTING + lifecyclePolicy: efs.LifecyclePolicy.AFTER_14_DAYS, + performanceMode: efs.PerformanceMode.GENERAL_PURPOSE, + throughputMode: efs.ThroughputMode.BURSTING }); const inst = new Instance(this, 'inst', { From a3e23cdd16560f9d5e57e6f5d15978a08cf5644d Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 4 Jun 2020 16:56:29 -0500 Subject: [PATCH 05/32] chore(secretsmanager): correct case typo in rotation-schedule.ts (#8384) ---- *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-secretsmanager/lib/rotation-schedule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts b/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts index 913db1ee118fa..ada35925d8b3a 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts @@ -8,7 +8,7 @@ import { CfnRotationSchedule } from './secretsmanager.generated'; */ export interface RotationScheduleOptions { /** - * THe Lambda function that can rotate the secret. + * The Lambda function that can rotate the secret. */ readonly rotationLambda: lambda.IFunction; From 729cc0b1705cab64696682f21985d97ce6c41607 Mon Sep 17 00:00:00 2001 From: kaizen3031593 <36202692+kaizen3031593@users.noreply.github.com> Date: Fri, 5 Jun 2020 04:50:32 -0400 Subject: [PATCH 06/32] fix(elbv2): addAction ignores conditions (#8385) Elastic Load Balancer's ApplicationListener.addAction does not pass on conditions array to ApplicationListenerRule. This PR adds a line that passes on the conditions in the addAction function. fixes #8328 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/alb/application-listener.ts | 1 + .../test/alb/test.actions.ts | 49 ++ .../test/integ.alb2.expected.json | 664 ++++++++++++++++++ .../test/integ.alb2.ts | 52 ++ 4 files changed, 766 insertions(+) create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb2.expected.json create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb2.ts diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index c087f690f70e5..f671e705ec78b 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -244,6 +244,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis // TargetGroup.registerListener is called inside ApplicationListenerRule. new ApplicationListenerRule(this, id + 'Rule', { listener: this, + conditions: props.conditions, hostHeader: props.hostHeader, pathPattern: props.pathPattern, pathPatterns: props.pathPatterns, diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.actions.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.actions.ts index 1921303469968..62a1c378bf067 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.actions.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.actions.ts @@ -182,4 +182,53 @@ export = { test.done(); }, + + 'Add Action with multiple Conditions'(test: Test) { + // GIVEN + const listener = lb.addListener('Listener', { port: 80 }); + + // WHEN + listener.addAction('Action1', { + action: elbv2.ListenerAction.forward([group1]), + }); + + listener.addAction('Action2', { + conditions: [ + elbv2.ListenerCondition.hostHeaders(['example.com']), + elbv2.ListenerCondition.sourceIps(['1.1.1.1/32']), + ], + priority: 10, + action: elbv2.ListenerAction.forward([group2]), + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Actions: [ + { + TargetGroupArn: { Ref: 'TargetGroup2D571E5D7' }, + Type: 'forward', + }, + ], + Conditions: [ + { + Field: 'host-header', + HostHeaderConfig: { + Values: [ + 'example.com', + ], + }, + }, + { + Field: 'source-ip', + SourceIpConfig: { + Values: [ + '1.1.1.1/32', + ], + }, + }, + ], + })); + + test.done(); + }, }; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb2.expected.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb2.expected.json new file mode 100644 index 0000000000000..2639b8a9b138e --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb2.expected.json @@ -0,0 +1,664 @@ +{ + "Resources": { + "VPCB9E5F0B4": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC" + } + ] + } + }, + "VPCPublicSubnet1SubnetB4246D30": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableAssociation0B0896DC": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + } + } + }, + "VPCPublicSubnet1DefaultRoute91CEF279": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet1EIP6AD938E8": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet2Subnet74179F39": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTableAssociation5A808732": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + } + }, + "VPCPublicSubnet2DefaultRouteB7481BBA": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet2EIP4947BC00": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2NATGateway3C070193": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPrivateSubnet1Subnet8BCA10E0": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableAssociation347902D1": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + } + } + }, + "VPCPrivateSubnet1DefaultRouteAE1D6490": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet1NATGatewayE0556630" + } + } + }, + "VPCPrivateSubnet2SubnetCFCDAA7A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.192.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTableAssociation0C73D413": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + } + }, + "VPCPrivateSubnet2DefaultRouteF4F5CFD2": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet2NATGateway3C070193" + } + } + }, + "VPCIGWB7E252D3": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC" + } + ] + } + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "LB8A12904C": { + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + "Properties": { + "Scheme": "internet-facing", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "LBSecurityGroup8A41EA2B", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + ], + "Type": "application" + }, + "DependsOn": [ + "VPCPublicSubnet1DefaultRoute91CEF279", + "VPCPublicSubnet2DefaultRouteB7481BBA" + ] + }, + "LBSecurityGroup8A41EA2B": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Automatically created Security Group for ELB awscdkelbv2integLB9950B1E4", + "SecurityGroupEgress": [ + { + "CidrIp": "255.255.255.255/32", + "Description": "Disallow all traffic", + "FromPort": 252, + "IpProtocol": "icmp", + "ToPort": 86 + } + ], + "SecurityGroupIngress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow from anyone on port 80", + "FromPort": 80, + "IpProtocol": "tcp", + "ToPort": 80 + } + ], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "LBListener49E825B4": { + "Type": "AWS::ElasticLoadBalancingV2::Listener", + "Properties": { + "DefaultActions": [ + { + "TargetGroupArn": { + "Ref": "LBListenerTargetGroupF04FCF6D" + }, + "Type": "forward" + } + ], + "LoadBalancerArn": { + "Ref": "LB8A12904C" + }, + "Port": 80, + "Protocol": "HTTP" + } + }, + "LBListenerTargetGroupF04FCF6D": { + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + "Properties": { + "Port": 80, + "Protocol": "HTTP", + "Targets": [ + { + "Id": "10.0.128.4" + } + ], + "TargetType": "ip", + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "LBListenerConditionalTargetGroupA75CCCD9": { + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + "Properties": { + "Port": 80, + "Protocol": "HTTP", + "Targets": [ + { + "Id": "10.0.128.5" + } + ], + "TargetType": "ip", + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "LBListenerConditionalTargetRule91FA260F": { + "Type": "AWS::ElasticLoadBalancingV2::ListenerRule", + "Properties": { + "Actions": [ + { + "TargetGroupArn": { + "Ref": "LBListenerConditionalTargetGroupA75CCCD9" + }, + "Type": "forward" + } + ], + "Conditions": [ + { + "Field": "host-header", + "Values": [ + "example.com" + ] + } + ], + "ListenerArn": { + "Ref": "LBListener49E825B4" + }, + "Priority": 10 + } + }, + "LBListeneraction1Rule86E405BB": { + "Type": "AWS::ElasticLoadBalancingV2::ListenerRule", + "Properties": { + "Actions": [ + { + "FixedResponseConfig": { + "MessageBody": "success", + "StatusCode": "200" + }, + "Type": "fixed-response" + } + ], + "Conditions": [ + { + "Field": "host-header", + "HostHeaderConfig": { + "Values": [ + "example.com" + ] + } + } + ], + "ListenerArn": { + "Ref": "LBListener49E825B4" + }, + "Priority": 1 + } + }, + "ResponseTimeHigh1D16E109F": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 2, + "Dimensions": [ + { + "Name": "LoadBalancer", + "Value": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Ref": "LBListener49E825B4" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 2, + { + "Fn::Split": [ + "/", + { + "Ref": "LBListener49E825B4" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + "/", + { + "Ref": "LBListener49E825B4" + } + ] + } + ] + } + ] + ] + } + }, + { + "Name": "TargetGroup", + "Value": { + "Fn::GetAtt": [ + "LBListenerTargetGroupF04FCF6D", + "TargetGroupFullName" + ] + } + } + ], + "MetricName": "TargetResponseTime", + "Namespace": "AWS/ApplicationELB", + "Period": 300, + "Statistic": "Average", + "Threshold": 5 + } + }, + "ResponseTimeHigh2FFCF1FE1": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 2, + "Dimensions": [ + { + "Name": "LoadBalancer", + "Value": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Ref": "LBListener49E825B4" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 2, + { + "Fn::Split": [ + "/", + { + "Ref": "LBListener49E825B4" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + "/", + { + "Ref": "LBListener49E825B4" + } + ] + } + ] + } + ] + ] + } + }, + { + "Name": "TargetGroup", + "Value": { + "Fn::GetAtt": [ + "LBListenerConditionalTargetGroupA75CCCD9", + "TargetGroupFullName" + ] + } + } + ], + "MetricName": "TargetResponseTime", + "Namespace": "AWS/ApplicationELB", + "Period": 300, + "Statistic": "Average", + "Threshold": 5 + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb2.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb2.ts new file mode 100644 index 0000000000000..fa586fc7bfcf4 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb2.ts @@ -0,0 +1,52 @@ +#!/usr/bin/env node +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import * as elbv2 from '../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-cdk-elbv2-integ'); + +const vpc = new ec2.Vpc(stack, 'VPC', { + maxAzs: 2, +}); + +const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { + vpc, + internetFacing: true, +}); + +const listener = lb.addListener('Listener', { + port: 80, +}); + +const group1 = listener.addTargets('Target', { + port: 80, + targets: [new elbv2.IpTarget('10.0.128.4')], +}); + +const group2 = listener.addTargets('ConditionalTarget', { + priority: 10, + hostHeader: 'example.com', + port: 80, + targets: [new elbv2.IpTarget('10.0.128.5')], +}); + +listener.addAction('action1', { + priority: 1, + conditions: [ + elbv2.ListenerCondition.hostHeaders(['example.com']), + ], + action: elbv2.ListenerAction.fixedResponse(200, {messageBody: 'success'}), +}); + +group1.metricTargetResponseTime().createAlarm(stack, 'ResponseTimeHigh1', { + threshold: 5, + evaluationPeriods: 2, +}); + +group2.metricTargetResponseTime().createAlarm(stack, 'ResponseTimeHigh2', { + threshold: 5, + evaluationPeriods: 2, +}); + +app.synth(); From 4eefbbe612d4bd643bffd4dee525d88a921439cb Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Fri, 5 Jun 2020 11:12:20 -0700 Subject: [PATCH 07/32] feat(rds): rename 'kmsKey' properties to 'encryptionKey' (#8324) The conventional CDK name for properties that hold KMS Keys is 'encryptionKey', not 'kmsKey' (we don't use the service name as part of the class or property name). BREAKING CHANGE: DatabaseClusterProps.kmsKey has been renamed to storageEncryptionKey * **rds**: DatabaseInstanceNewProps.performanceInsightKmsKey has been renamed to performanceInsightEncryptionKey * **rds**: DatabaseInstanceSourceProps.secretKmsKey has been renamed to masterUserPasswordEncryptionKey * **rds**: DatabaseInstanceProps.kmsKey has been renamed to storageEncryptionKey * **rds**: DatabaseInstanceReadReplicaProps.kmsKey has been renamed to storageEncryptionKey * **rds**: Login.kmsKey has been renamed to encryptionKey ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-rds/lib/cluster.ts | 18 +++++----- packages/@aws-cdk/aws-rds/lib/instance.ts | 36 +++++++++---------- packages/@aws-cdk/aws-rds/lib/props.ts | 2 +- .../@aws-cdk/aws-rds/test/integ.cluster-s3.ts | 2 +- .../@aws-cdk/aws-rds/test/integ.cluster.ts | 2 +- .../@aws-cdk/aws-rds/test/test.cluster.ts | 2 +- 6 files changed, 31 insertions(+), 31 deletions(-) diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index 6f75c650c3fce..577a5420d0632 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -88,19 +88,19 @@ export interface DatabaseClusterProps { readonly defaultDatabaseName?: string; /** - * Whether to enable storage encryption + * Whether to enable storage encryption. * - * @default false + * @default - true if storageEncryptionKey is provided, false otherwise */ readonly storageEncrypted?: boolean /** - * The KMS key for storage encryption. If specified `storageEncrypted` - * will be set to `true`. + * The KMS key for storage encryption. + * If specified, {@link storageEncrypted} will be set to `true`. * - * @default - default master key. + * @default - if storageEncrypted is true then the default master key, no key otherwise */ - readonly kmsKey?: kms.IKey; + readonly storageEncryptionKey?: kms.IKey; /** * A preferred maintenance window day/time range. Should be specified as a range ddd:hh24:mi-ddd:hh24:mi (24H Clock UTC). @@ -369,7 +369,7 @@ export class DatabaseCluster extends DatabaseClusterBase { if (!props.masterUser.password) { secret = new DatabaseSecret(this, 'Secret', { username: props.masterUser.username, - encryptionKey: props.masterUser.kmsKey, + encryptionKey: props.masterUser.encryptionKey, }); } @@ -460,8 +460,8 @@ export class DatabaseCluster extends DatabaseClusterBase { preferredMaintenanceWindow: props.preferredMaintenanceWindow, databaseName: props.defaultDatabaseName, // Encryption - kmsKeyId: props.kmsKey && props.kmsKey.keyArn, - storageEncrypted: props.kmsKey ? true : props.storageEncrypted, + kmsKeyId: props.storageEncryptionKey && props.storageEncryptionKey.keyArn, + storageEncrypted: props.storageEncryptionKey ? true : props.storageEncrypted, }); // if removalPolicy was not specified, diff --git a/packages/@aws-cdk/aws-rds/lib/instance.ts b/packages/@aws-cdk/aws-rds/lib/instance.ts index 103ecc5df17bf..5ed0925bf5d7d 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance.ts @@ -476,7 +476,7 @@ export interface DatabaseInstanceNewProps { * * @default - default master key */ - readonly performanceInsightKmsKey?: kms.IKey; + readonly performanceInsightEncryptionKey?: kms.IKey; /** * The list of log types that need to be enabled for exporting to @@ -624,7 +624,7 @@ abstract class DatabaseInstanceNew extends DatabaseInstanceBase implements IData multiAz: props.multiAz, optionGroupName: props.optionGroup && props.optionGroup.optionGroupName, performanceInsightsKmsKeyId: props.enablePerformanceInsights - ? props.performanceInsightKmsKey && props.performanceInsightKmsKey.keyArn + ? props.performanceInsightEncryptionKey && props.performanceInsightEncryptionKey.keyArn : undefined, performanceInsightsRetentionPeriod: props.enablePerformanceInsights ? (props.performanceInsightRetention || PerformanceInsightRetention.DEFAULT) @@ -706,11 +706,11 @@ export interface DatabaseInstanceSourceProps extends DatabaseInstanceNewProps { readonly masterUserPassword?: SecretValue; /** - * The KMS key to use to encrypt the secret for the master user password. + * The KMS key used to encrypt the secret for the master user password. * * @default - default master key */ - readonly secretKmsKey?: kms.IKey; + readonly masterUserPasswordEncryptionKey?: kms.IKey; /** * The name of the database. @@ -832,16 +832,16 @@ export interface DatabaseInstanceProps extends DatabaseInstanceSourceProps { /** * Indicates whether the DB instance is encrypted. * - * @default false + * @default - true if storageEncryptionKey has been provided, false otherwise */ readonly storageEncrypted?: boolean; /** - * The master key that's used to encrypt the DB instance. + * The KMS key that's used to encrypt the DB instance. * - * @default - default master key + * @default - default master key if storageEncrypted is true, no key otherwise */ - readonly kmsKey?: kms.IKey; + readonly storageEncryptionKey?: kms.IKey; } /** @@ -863,19 +863,19 @@ export class DatabaseInstance extends DatabaseInstanceSource implements IDatabas if (!props.masterUserPassword) { secret = new DatabaseSecret(this, 'Secret', { username: props.masterUsername, - encryptionKey: props.secretKmsKey, + encryptionKey: props.masterUserPasswordEncryptionKey, }); } const instance = new CfnDBInstance(this, 'Resource', { ...this.sourceCfnProps, characterSetName: props.characterSetName, - kmsKeyId: props.kmsKey && props.kmsKey.keyArn, + kmsKeyId: props.storageEncryptionKey && props.storageEncryptionKey.keyArn, masterUsername: secret ? secret.secretValueFromJson('username').toString() : props.masterUsername, masterUserPassword: secret ? secret.secretValueFromJson('password').toString() : props.masterUserPassword && props.masterUserPassword.toString(), - storageEncrypted: props.kmsKey ? true : props.storageEncrypted, + storageEncrypted: props.storageEncryptionKey ? true : props.storageEncrypted, }); this.instanceIdentifier = instance.ref; @@ -958,7 +958,7 @@ export class DatabaseInstanceFromSnapshot extends DatabaseInstanceSource impleme secret = new DatabaseSecret(this, 'Secret', { username: props.masterUsername, - encryptionKey: props.secretKmsKey, + encryptionKey: props.masterUserPasswordEncryptionKey, }); } else { if (props.masterUsername) { // It's not possible to change the master username of a RDS instance @@ -1008,16 +1008,16 @@ export interface DatabaseInstanceReadReplicaProps extends DatabaseInstanceSource /** * Indicates whether the DB instance is encrypted. * - * @default false + * @default - true if storageEncryptionKey has been provided, false otherwise */ readonly storageEncrypted?: boolean; /** - * The master key that's used to encrypt the DB instance. + * The KMS key that's used to encrypt the DB instance. * - * @default - default master key + * @default - default master key if storageEncrypted is true, no key otherwise */ - readonly kmsKey?: kms.IKey; + readonly storageEncryptionKey?: kms.IKey; } /** @@ -1038,8 +1038,8 @@ export class DatabaseInstanceReadReplica extends DatabaseInstanceNew implements ...this.newCfnProps, // this must be ARN, not ID, because of https://github.com/terraform-providers/terraform-provider-aws/issues/528#issuecomment-391169012 sourceDbInstanceIdentifier: props.sourceDatabaseInstance.instanceArn, - kmsKeyId: props.kmsKey && props.kmsKey.keyArn, - storageEncrypted: props.kmsKey ? true : props.storageEncrypted, + kmsKeyId: props.storageEncryptionKey && props.storageEncryptionKey.keyArn, + storageEncrypted: props.storageEncryptionKey ? true : props.storageEncrypted, }); this.instanceIdentifier = instance.ref; diff --git a/packages/@aws-cdk/aws-rds/lib/props.ts b/packages/@aws-cdk/aws-rds/lib/props.ts index 5f198a2214e94..95e04ec684069 100644 --- a/packages/@aws-cdk/aws-rds/lib/props.ts +++ b/packages/@aws-cdk/aws-rds/lib/props.ts @@ -178,7 +178,7 @@ export interface Login { * * @default default master key */ - readonly kmsKey?: kms.IKey; + readonly encryptionKey?: kms.IKey; } /** diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.ts b/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.ts index 4b9eb089715a2..2153d8ea95410 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.ts +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.ts @@ -25,7 +25,7 @@ const cluster = new DatabaseCluster(stack, 'Database', { vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, vpc, }, - kmsKey, + storageEncryptionKey: kmsKey, s3ImportBuckets: [importBucket], s3ExportBuckets: [exportBucket], }); diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster.ts b/packages/@aws-cdk/aws-rds/test/integ.cluster.ts index b5517f4b4048b..590a32fd7afb1 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster.ts +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster.ts @@ -31,7 +31,7 @@ const cluster = new DatabaseCluster(stack, 'Database', { vpc, }, parameterGroup: params, - kmsKey, + storageEncryptionKey: kmsKey, }); cluster.connections.allowDefaultPortFromAnyIpv4('Open to the world'); diff --git a/packages/@aws-cdk/aws-rds/test/test.cluster.ts b/packages/@aws-cdk/aws-rds/test/test.cluster.ts index 5293cf2f0dd1d..597027e267f2e 100644 --- a/packages/@aws-cdk/aws-rds/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-rds/test/test.cluster.ts @@ -242,7 +242,7 @@ export = { instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), vpc, }, - kmsKey: new kms.Key(stack, 'Key'), + storageEncryptionKey: new kms.Key(stack, 'Key'), }); // THEN From 6c3545af8c0175a9347caa6012cd25eb1cb04b84 Mon Sep 17 00:00:00 2001 From: comcalvi <66279577+comcalvi@users.noreply.github.com> Date: Fri, 5 Jun 2020 16:42:23 -0400 Subject: [PATCH 08/32] docs: add Slack link to the main ReadMe (#8388) Fixes #6669 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ba7a422bb528f..9b51cbaba4118 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ for tracking bugs and feature requests. * Ask a question on [Stack Overflow](https://stackoverflow.com/questions/tagged/aws-cdk) and tag it with `aws-cdk` * Come join the AWS CDK community on [Gitter](https://gitter.im/awslabs/aws-cdk) +* Talk in the CDK channel of the [AWS Developers Slack workspace](https://awsdevelopers.slack.com) (invite required) * Open a support ticket with [AWS Support](https://console.aws.amazon.com/support/home#/) * If it turns out that you may have found a bug, please open an [issue](https://github.com/aws/aws-cdk/issues/new) From f44ae607670bccee21dfd390effa7d0e8701efd4 Mon Sep 17 00:00:00 2001 From: comcalvi <66279577+comcalvi@users.noreply.github.com> Date: Fri, 5 Jun 2020 21:51:43 -0400 Subject: [PATCH 09/32] feat(secretsmanager): Secret.grantRead() also gives DescribeSecret permissions (#8409) `Secret.grantRead()` now gives permission for `secretmanager:DescribeSecret` and `secretmanager:GetSecretValue`, instead of only `secretmanager:GetSecretValue`. Fixes #6444 Fixes #7953 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../test/ec2/integ.secret-json-field.expected.json | 7 +++++-- .../aws-ecs/test/fargate/integ.secret.expected.json | 7 +++++-- .../@aws-cdk/aws-ecs/test/test.container-definition.ts | 10 ++++++++-- packages/@aws-cdk/aws-secretsmanager/lib/secret.ts | 2 +- .../test/integ.secret.lit.expected.json | 7 +++++-- .../@aws-cdk/aws-secretsmanager/test/test.secret.ts | 10 ++++++++-- 6 files changed, 32 insertions(+), 11 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.secret-json-field.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.secret-json-field.expected.json index 5378fdbb03212..f214a22fea2cb 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.secret-json-field.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.secret-json-field.expected.json @@ -95,7 +95,10 @@ "PolicyDocument": { "Statement": [ { - "Action": "secretsmanager:GetSecretValue", + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ], "Effect": "Allow", "Resource": { "Ref": "SecretA720EF05" @@ -113,4 +116,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.secret.expected.json b/packages/@aws-cdk/aws-ecs/test/fargate/integ.secret.expected.json index 919ea2bbf03d8..39896001c0e67 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.secret.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.secret.expected.json @@ -88,7 +88,10 @@ "PolicyDocument": { "Statement": [ { - "Action": "secretsmanager:GetSecretValue", + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ], "Effect": "Allow", "Resource": { "Ref": "SecretA720EF05" @@ -106,4 +109,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts index cccb2e9efdefb..934f195e654e2 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts @@ -755,7 +755,10 @@ export = { PolicyDocument: { Statement: [ { - Action: 'secretsmanager:GetSecretValue', + Action: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], Effect: 'Allow', Resource: { Ref: 'SecretA720EF05', @@ -1111,7 +1114,10 @@ export = { PolicyDocument: { Statement: [ { - Action: 'secretsmanager:GetSecretValue', + Action: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], Effect: 'Allow', Resource: mySecretArn, }, diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts index 4bb50b68aa684..b44c44206a0b3 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts @@ -135,7 +135,7 @@ abstract class SecretBase extends Resource implements ISecret { const result = iam.Grant.addToPrincipal({ grantee, - actions: ['secretsmanager:GetSecretValue'], + actions: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'], resourceArns: [this.secretArn], scope: this, }); diff --git a/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.expected.json b/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.expected.json index b09235155139e..5411df31be1ba 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.expected.json +++ b/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.expected.json @@ -38,7 +38,10 @@ "PolicyDocument": { "Statement": [ { - "Action": "secretsmanager:GetSecretValue", + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ], "Effect": "Allow", "Resource": { "Ref": "SecretA720EF05" @@ -121,4 +124,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts b/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts index 3b043eb562a97..606bc33d9ec8b 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts @@ -189,7 +189,10 @@ export = { PolicyDocument: { Version: '2012-10-17', Statement: [{ - Action: 'secretsmanager:GetSecretValue', + Action: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], Effect: 'Allow', Resource: { Ref: 'SecretA720EF05' }, }], @@ -252,7 +255,10 @@ export = { PolicyDocument: { Version: '2012-10-17', Statement: [{ - Action: 'secretsmanager:GetSecretValue', + Action: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], Effect: 'Allow', Resource: { Ref: 'SecretA720EF05' }, Condition: { From aa920afa8834d8e501448dda399bad2ed6f230be Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Sun, 7 Jun 2020 08:39:54 +0200 Subject: [PATCH 10/32] chore(s3-assets): use jest for tests (#8411) No new tests or expectations added. ---- *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-s3-assets/.gitignore | 1 + packages/@aws-cdk/aws-s3-assets/.npmignore | 1 + .../@aws-cdk/aws-s3-assets/jest.config.js | 2 + packages/@aws-cdk/aws-s3-assets/package.json | 14 +- .../@aws-cdk/aws-s3-assets/test/asset.test.ts | 328 ++++++++++++++++ .../@aws-cdk/aws-s3-assets/test/test.asset.ts | 355 ------------------ 6 files changed, 336 insertions(+), 365 deletions(-) create mode 100644 packages/@aws-cdk/aws-s3-assets/jest.config.js create mode 100644 packages/@aws-cdk/aws-s3-assets/test/asset.test.ts delete mode 100644 packages/@aws-cdk/aws-s3-assets/test/test.asset.ts diff --git a/packages/@aws-cdk/aws-s3-assets/.gitignore b/packages/@aws-cdk/aws-s3-assets/.gitignore index 84107ada8a317..743b39099999a 100644 --- a/packages/@aws-cdk/aws-s3-assets/.gitignore +++ b/packages/@aws-cdk/aws-s3-assets/.gitignore @@ -15,3 +15,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-s3-assets/.npmignore b/packages/@aws-cdk/aws-s3-assets/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-s3-assets/.npmignore +++ b/packages/@aws-cdk/aws-s3-assets/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-s3-assets/jest.config.js b/packages/@aws-cdk/aws-s3-assets/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-assets/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-s3-assets/package.json b/packages/@aws-cdk/aws-s3-assets/package.json index 3aea5a8f58626..3b8fe5bdebded 100644 --- a/packages/@aws-cdk/aws-s3-assets/package.json +++ b/packages/@aws-cdk/aws-s3-assets/package.json @@ -45,6 +45,9 @@ "build+test": "npm run build && npm test", "compat": "cdk-compat" }, + "cdk-build": { + "jest": true + }, "keywords": [ "aws", "cdk", @@ -60,16 +63,10 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.31", - "@types/sinon": "^9.0.4", - "aws-cdk": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "nodeunit": "^0.11.3", "pkglint": "0.0.0", - "sinon": "^9.0.2", - "@aws-cdk/cloud-assembly-schema": "0.0.0", - "ts-mock-imports": "^1.3.0" + "@aws-cdk/cloud-assembly-schema": "0.0.0" }, "dependencies": { "@aws-cdk/assets": "0.0.0", @@ -93,9 +90,6 @@ }, "stability": "experimental", "maturity": "experimental", - "nyc": { - "statements": 75 - }, "awslint": { "exclude": [ "docs-public-apis:@aws-cdk/aws-s3-assets.AssetOptions", diff --git a/packages/@aws-cdk/aws-s3-assets/test/asset.test.ts b/packages/@aws-cdk/aws-s3-assets/test/asset.test.ts new file mode 100644 index 0000000000000..4da45143c59f8 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-assets/test/asset.test.ts @@ -0,0 +1,328 @@ +import { ResourcePart, SynthUtils } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { Asset } from '../lib/asset'; + +const SAMPLE_ASSET_DIR = path.join(__dirname, 'sample-asset-directory'); + +test('simple use case', () => { + const app = new cdk.App({ + context: { + [cxapi.DISABLE_ASSET_STAGING_CONTEXT]: 'true', + }, + }); + const stack = new cdk.Stack(app, 'MyStack'); + new Asset(stack, 'MyAsset', { + path: SAMPLE_ASSET_DIR, + }); + + // verify that metadata contains an "aws:cdk:asset" entry with + // the correct information + const entry = stack.node.metadata.find(m => m.type === 'aws:cdk:asset'); + expect(entry).toBeTruthy(); + + // verify that now the template contains parameters for this asset + const session = app.synth(); + + expect(stack.resolve(entry!.data)).toEqual({ + path: SAMPLE_ASSET_DIR, + id: '6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', + packaging: 'zip', + sourceHash: '6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', + s3BucketParameter: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3Bucket50B5A10B', + s3KeyParameter: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3VersionKey1F7D75F9', + artifactHashParameter: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2ArtifactHash220DE9BD', + }); + + const template = JSON.parse(fs.readFileSync(path.join(session.directory, 'MyStack.template.json'), 'utf-8')); + + expect(template.Parameters.AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3Bucket50B5A10B.Type).toBe('String'); + expect(template.Parameters.AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3VersionKey1F7D75F9.Type).toBe('String'); +}); + +test('verify that the app resolves tokens in metadata', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'my-stack'); + const dirPath = path.resolve(__dirname, 'sample-asset-directory'); + + new Asset(stack, 'MyAsset', { + path: dirPath, + }); + + const synth = app.synth().getStackByName(stack.stackName); + const meta = synth.manifest.metadata || {}; + expect(meta['/my-stack']).toBeTruthy(); + expect(meta['/my-stack'][0]).toBeTruthy(); + expect(meta['/my-stack'][0].data).toEqual({ + path: 'asset.6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', + id: '6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', + packaging: 'zip', + sourceHash: '6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', + s3BucketParameter: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3Bucket50B5A10B', + s3KeyParameter: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3VersionKey1F7D75F9', + artifactHashParameter: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2ArtifactHash220DE9BD', + }); +}); + +test('"file" assets', () => { + const stack = new cdk.Stack(); + const filePath = path.join(__dirname, 'file-asset.txt'); + new Asset(stack, 'MyAsset', { path: filePath }); + const entry = stack.node.metadata.find(m => m.type === 'aws:cdk:asset'); + expect(entry).toBeTruthy(); + + // synthesize first so "prepare" is called + const template = SynthUtils.synthesize(stack).template; + + expect(stack.resolve(entry!.data)).toEqual({ + path: 'asset.78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197.txt', + packaging: 'file', + id: '78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197', + sourceHash: '78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197', + s3BucketParameter: 'AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197S3Bucket2C60F94A', + s3KeyParameter: 'AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197S3VersionKey9482DC35', + artifactHashParameter: 'AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197ArtifactHash22BFFA67', + }); + + // verify that now the template contains parameters for this asset + expect(template.Parameters.AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197S3Bucket2C60F94A.Type).toBe('String'); + expect(template.Parameters.AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197S3VersionKey9482DC35.Type).toBe('String'); +}); + +test('"readers" or "grantRead" can be used to grant read permissions on the asset to a principal', () => { + const stack = new cdk.Stack(); + const user = new iam.User(stack, 'MyUser'); + const group = new iam.Group(stack, 'MyGroup'); + + const asset = new Asset(stack, 'MyAsset', { + path: path.join(__dirname, 'sample-asset-directory'), + readers: [ user ], + }); + + asset.grantRead(group); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], + Effect: 'Allow', + Resource: [ + { 'Fn::Join': ['', ['arn:', {Ref: 'AWS::Partition'}, ':s3:::', {Ref: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3Bucket50B5A10B'} ] ] }, + { 'Fn::Join': ['', [ 'arn:', {Ref: 'AWS::Partition'}, ':s3:::', {Ref: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3Bucket50B5A10B'}, '/*' ] ] }, + ], + }, + ], + }, + }); +}); + +test('fails if directory not found', () => { + const stack = new cdk.Stack(); + expect(() => new Asset(stack, 'MyDirectory', { + path: '/path/not/found/' + Math.random() * 999999, + })).toThrow(); +}); + +test('multiple assets under the same parent', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + expect(() => new Asset(stack, 'MyDirectory1', { path: path.join(__dirname, 'sample-asset-directory') })).not.toThrow(); + expect(() => new Asset(stack, 'MyDirectory2', { path: path.join(__dirname, 'sample-asset-directory') })).not.toThrow(); +}); + +test('isZipArchive indicates if the asset represents a .zip file (either explicitly or via ZipDirectory packaging)', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const nonZipAsset = new Asset(stack, 'NonZipAsset', { + path: path.join(__dirname, 'sample-asset-directory', 'sample-asset-file.txt'), + }); + + const zipDirectoryAsset = new Asset(stack, 'ZipDirectoryAsset', { + path: path.join(__dirname, 'sample-asset-directory'), + }); + + const zipFileAsset = new Asset(stack, 'ZipFileAsset', { + path: path.join(__dirname, 'sample-asset-directory', 'sample-zip-asset.zip'), + }); + + const jarFileAsset = new Asset(stack, 'JarFileAsset', { + path: path.join(__dirname, 'sample-asset-directory', 'sample-jar-asset.jar'), + }); + + // THEN + expect(nonZipAsset.isZipArchive).toBe(false); + expect(zipDirectoryAsset.isZipArchive).toBe(true); + expect(zipFileAsset.isZipArchive).toBe(true); + expect(jarFileAsset.isZipArchive).toBe(true); +}); + +test('addResourceMetadata can be used to add CFN metadata to resources', () => { + // GIVEN + const stack = new cdk.Stack(); + stack.node.setContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT, true); + + const location = path.join(__dirname, 'sample-asset-directory'); + const resource = new cdk.CfnResource(stack, 'MyResource', { type: 'My::Resource::Type' }); + const asset = new Asset(stack, 'MyAsset', { path: location }); + + // WHEN + asset.addResourceMetadata(resource, 'PropName'); + + // THEN + expect(stack).toHaveResource('My::Resource::Type', { + Metadata: { + 'aws:asset:path': 'asset.6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', + 'aws:asset:property': 'PropName', + }, + }, ResourcePart.CompleteDefinition); +}); + +test('asset metadata is only emitted if ASSET_RESOURCE_METADATA_ENABLED_CONTEXT is defined', () => { + // GIVEN + const stack = new cdk.Stack(); + + const resource = new cdk.CfnResource(stack, 'MyResource', { type: 'My::Resource::Type' }); + const asset = new Asset(stack, 'MyAsset', { path: SAMPLE_ASSET_DIR }); + + // WHEN + asset.addResourceMetadata(resource, 'PropName'); + + // THEN + expect(stack).not.toHaveResource('My::Resource::Type', { + Metadata: { + 'aws:asset:path': SAMPLE_ASSET_DIR, + 'aws:asset:property': 'PropName', + }, + }, ResourcePart.CompleteDefinition); +}); + +describe('staging', () => { + test('copy file assets under /${fingerprint}.ext', () => { + const tempdir = mkdtempSync(); + process.chdir(tempdir); // change current directory to somewhere in /tmp + + // GIVEN + const app = new cdk.App({ outdir: tempdir }); + const stack = new cdk.Stack(app, 'stack'); + + // WHEN + new Asset(stack, 'ZipFile', { + path: path.join(SAMPLE_ASSET_DIR, 'sample-zip-asset.zip'), + }); + + new Asset(stack, 'TextFile', { + path: path.join(SAMPLE_ASSET_DIR, 'sample-asset-file.txt'), + }); + + // THEN + app.synth(); + expect(fs.existsSync(tempdir)).toBe(true); + expect(fs.existsSync(path.join(tempdir, 'asset.a7a79cdf84b802ea8b198059ff899cffc095a1b9606e919f98e05bf80779756b.zip'))).toBe(true); + }); + + test('copy directory under .assets/fingerprint/**', () => { + const tempdir = mkdtempSync(); + process.chdir(tempdir); // change current directory to somewhere in /tmp + + // GIVEN + const app = new cdk.App({ outdir: tempdir }); + const stack = new cdk.Stack(app, 'stack'); + + // WHEN + new Asset(stack, 'ZipDirectory', { + path: SAMPLE_ASSET_DIR, + }); + + // THEN + app.synth(); + expect(fs.existsSync(tempdir)).toBe(true); + const hash = 'asset.6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2'; + expect(fs.existsSync(path.join(tempdir, hash, 'sample-asset-file.txt'))).toBe(true); + expect(fs.existsSync(path.join(tempdir, hash, 'sample-jar-asset.jar'))).toBe(true); + expect(() => fs.readdirSync(tempdir)).not.toThrow(); + }); + + test('staging path is relative if the dir is below the working directory', () => { + // GIVEN + const tempdir = mkdtempSync(); + process.chdir(tempdir); // change current directory to somewhere in /tmp + + const staging = '.my-awesome-staging-directory'; + const app = new cdk.App({ + outdir: staging, + context: { + [cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT]: 'true', + }, + }); + + const stack = new cdk.Stack(app, 'stack'); + + const resource = new cdk.CfnResource(stack, 'MyResource', { type: 'My::Resource::Type' }); + const asset = new Asset(stack, 'MyAsset', { path: SAMPLE_ASSET_DIR }); + + // WHEN + asset.addResourceMetadata(resource, 'PropName'); + + const template = SynthUtils.synthesize(stack).template; + expect(template.Resources.MyResource.Metadata).toEqual({ + 'aws:asset:path': 'asset.6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', + 'aws:asset:property': 'PropName', + }); + }); + + test('if staging is disabled, asset path is absolute', () => { + // GIVEN + const staging = path.resolve(mkdtempSync()); + const app = new cdk.App({ + outdir: staging, + context: { + [cxapi.DISABLE_ASSET_STAGING_CONTEXT]: 'true', + [cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT]: 'true', + }, + }); + + const stack = new cdk.Stack(app, 'stack'); + + const resource = new cdk.CfnResource(stack, 'MyResource', { type: 'My::Resource::Type' }); + const asset = new Asset(stack, 'MyAsset', { path: SAMPLE_ASSET_DIR }); + + // WHEN + asset.addResourceMetadata(resource, 'PropName'); + + const template = SynthUtils.synthesize(stack).template; + expect(template.Resources.MyResource.Metadata).toEqual({ + 'aws:asset:path': SAMPLE_ASSET_DIR, + 'aws:asset:property': 'PropName', + }); + }); + + test('cdk metadata points to staged asset', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'stack'); + new Asset(stack, 'MyAsset', { path: SAMPLE_ASSET_DIR }); + + // WHEN + const session = app.synth(); + const artifact = session.getStackByName(stack.stackName); + const metadata = artifact.manifest.metadata || {}; + const md = Object.values(metadata)[0]![0]!.data as cxschema.AssetMetadataEntry; + expect(md.path).toBe('asset.6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2'); + }); +}); + +function mkdtempSync() { + return fs.mkdtempSync(path.join(os.tmpdir(), 'assets.test')); +} diff --git a/packages/@aws-cdk/aws-s3-assets/test/test.asset.ts b/packages/@aws-cdk/aws-s3-assets/test/test.asset.ts deleted file mode 100644 index 68ef08d863d76..0000000000000 --- a/packages/@aws-cdk/aws-s3-assets/test/test.asset.ts +++ /dev/null @@ -1,355 +0,0 @@ -import { expect, haveResource, ResourcePart, SynthUtils } from '@aws-cdk/assert'; -import * as iam from '@aws-cdk/aws-iam'; -import * as cxschema from '@aws-cdk/cloud-assembly-schema'; -import * as cdk from '@aws-cdk/core'; -import * as cxapi from '@aws-cdk/cx-api'; -import * as fs from 'fs'; -import { Test } from 'nodeunit'; -import * as os from 'os'; -import * as path from 'path'; -import { Asset } from '../lib/asset'; - -// tslint:disable:max-line-length - -const SAMPLE_ASSET_DIR = path.join(__dirname, 'sample-asset-directory'); - -export = { - 'simple use case'(test: Test) { - const app = new cdk.App({ - context: { - [cxapi.DISABLE_ASSET_STAGING_CONTEXT]: 'true', - }, - }); - const stack = new cdk.Stack(app, 'MyStack'); - new Asset(stack, 'MyAsset', { - path: SAMPLE_ASSET_DIR, - }); - - // verify that metadata contains an "aws:cdk:asset" entry with - // the correct information - const entry = stack.node.metadata.find(m => m.type === 'aws:cdk:asset'); - test.ok(entry, 'found metadata entry'); - - // verify that now the template contains parameters for this asset - const session = app.synth(); - - test.deepEqual(stack.resolve(entry!.data), { - path: SAMPLE_ASSET_DIR, - id: '6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', - packaging: 'zip', - sourceHash: '6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', - s3BucketParameter: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3Bucket50B5A10B', - s3KeyParameter: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3VersionKey1F7D75F9', - artifactHashParameter: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2ArtifactHash220DE9BD', - }); - - const template = JSON.parse(fs.readFileSync(path.join(session.directory, 'MyStack.template.json'), 'utf-8')); - - test.equal(template.Parameters.AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3Bucket50B5A10B.Type, 'String'); - test.equal(template.Parameters.AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3VersionKey1F7D75F9.Type, 'String'); - - test.done(); - }, - - 'verify that the app resolves tokens in metadata'(test: Test) { - const app = new cdk.App(); - const stack = new cdk.Stack(app, 'my-stack'); - const dirPath = path.resolve(__dirname, 'sample-asset-directory'); - - new Asset(stack, 'MyAsset', { - path: dirPath, - }); - - const synth = app.synth().getStackByName(stack.stackName); - const meta = synth.manifest.metadata || {}; - test.ok(meta['/my-stack']); - test.ok(meta['/my-stack'][0]); - test.deepEqual(meta['/my-stack'][0].data, { - path: 'asset.6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', - id: '6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', - packaging: 'zip', - sourceHash: '6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', - s3BucketParameter: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3Bucket50B5A10B', - s3KeyParameter: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3VersionKey1F7D75F9', - artifactHashParameter: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2ArtifactHash220DE9BD', - }); - - test.done(); - }, - - '"file" assets'(test: Test) { - const stack = new cdk.Stack(); - const filePath = path.join(__dirname, 'file-asset.txt'); - new Asset(stack, 'MyAsset', { path: filePath }); - const entry = stack.node.metadata.find(m => m.type === 'aws:cdk:asset'); - test.ok(entry, 'found metadata entry'); - - // synthesize first so "prepare" is called - const template = SynthUtils.synthesize(stack).template; - - test.deepEqual(stack.resolve(entry!.data), { - path: 'asset.78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197.txt', - packaging: 'file', - id: '78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197', - sourceHash: '78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197', - s3BucketParameter: 'AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197S3Bucket2C60F94A', - s3KeyParameter: 'AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197S3VersionKey9482DC35', - artifactHashParameter: 'AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197ArtifactHash22BFFA67', - }); - - // verify that now the template contains parameters for this asset - test.equal(template.Parameters.AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197S3Bucket2C60F94A.Type, 'String'); - test.equal(template.Parameters.AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197S3VersionKey9482DC35.Type, 'String'); - - test.done(); - }, - - '"readers" or "grantRead" can be used to grant read permissions on the asset to a principal'(test: Test) { - const stack = new cdk.Stack(); - const user = new iam.User(stack, 'MyUser'); - const group = new iam.Group(stack, 'MyGroup'); - - const asset = new Asset(stack, 'MyAsset', { - path: path.join(__dirname, 'sample-asset-directory'), - readers: [ user ], - }); - - asset.grantRead(group); - - expect(stack).to(haveResource('AWS::IAM::Policy', { - PolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], - Effect: 'Allow', - Resource: [ - { 'Fn::Join': ['', ['arn:', {Ref: 'AWS::Partition'}, ':s3:::', {Ref: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3Bucket50B5A10B'} ] ] }, - { 'Fn::Join': ['', [ 'arn:', {Ref: 'AWS::Partition'}, ':s3:::', {Ref: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3Bucket50B5A10B'}, '/*' ] ] }, - ], - }, - ], - }, - })); - - test.done(); - }, - 'fails if directory not found'(test: Test) { - const stack = new cdk.Stack(); - test.throws(() => new Asset(stack, 'MyDirectory', { - path: '/path/not/found/' + Math.random() * 999999, - })); - test.done(); - }, - - 'multiple assets under the same parent'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - new Asset(stack, 'MyDirectory1', { path: path.join(__dirname, 'sample-asset-directory') }); - new Asset(stack, 'MyDirectory2', { path: path.join(__dirname, 'sample-asset-directory') }); - - // THEN: no error - - test.done(); - }, - - 'isZipArchive indicates if the asset represents a .zip file (either explicitly or via ZipDirectory packaging)'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const nonZipAsset = new Asset(stack, 'NonZipAsset', { - path: path.join(__dirname, 'sample-asset-directory', 'sample-asset-file.txt'), - }); - - const zipDirectoryAsset = new Asset(stack, 'ZipDirectoryAsset', { - path: path.join(__dirname, 'sample-asset-directory'), - }); - - const zipFileAsset = new Asset(stack, 'ZipFileAsset', { - path: path.join(__dirname, 'sample-asset-directory', 'sample-zip-asset.zip'), - }); - - const jarFileAsset = new Asset(stack, 'JarFileAsset', { - path: path.join(__dirname, 'sample-asset-directory', 'sample-jar-asset.jar'), - }); - - // THEN - test.equal(nonZipAsset.isZipArchive, false); - test.equal(zipDirectoryAsset.isZipArchive, true); - test.equal(zipFileAsset.isZipArchive, true); - test.equal(jarFileAsset.isZipArchive, true); - test.done(); - }, - - 'addResourceMetadata can be used to add CFN metadata to resources'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - stack.node.setContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT, true); - - const location = path.join(__dirname, 'sample-asset-directory'); - const resource = new cdk.CfnResource(stack, 'MyResource', { type: 'My::Resource::Type' }); - const asset = new Asset(stack, 'MyAsset', { path: location }); - - // WHEN - asset.addResourceMetadata(resource, 'PropName'); - - // THEN - expect(stack).to(haveResource('My::Resource::Type', { - Metadata: { - 'aws:asset:path': 'asset.6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', - 'aws:asset:property': 'PropName', - }, - }, ResourcePart.CompleteDefinition)); - test.done(); - }, - - 'asset metadata is only emitted if ASSET_RESOURCE_METADATA_ENABLED_CONTEXT is defined'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const resource = new cdk.CfnResource(stack, 'MyResource', { type: 'My::Resource::Type' }); - const asset = new Asset(stack, 'MyAsset', { path: SAMPLE_ASSET_DIR }); - - // WHEN - asset.addResourceMetadata(resource, 'PropName'); - - // THEN - expect(stack).notTo(haveResource('My::Resource::Type', { - Metadata: { - 'aws:asset:path': SAMPLE_ASSET_DIR, - 'aws:asset:property': 'PropName', - }, - }, ResourcePart.CompleteDefinition)); - - test.done(); - }, - - 'staging': { - - 'copy file assets under /${fingerprint}.ext'(test: Test) { - const tempdir = mkdtempSync(); - process.chdir(tempdir); // change current directory to somewhere in /tmp - - // GIVEN - const app = new cdk.App({ outdir: tempdir }); - const stack = new cdk.Stack(app, 'stack'); - - // WHEN - new Asset(stack, 'ZipFile', { - path: path.join(SAMPLE_ASSET_DIR, 'sample-zip-asset.zip'), - }); - - new Asset(stack, 'TextFile', { - path: path.join(SAMPLE_ASSET_DIR, 'sample-asset-file.txt'), - }); - - // THEN - app.synth(); - test.ok(fs.existsSync(tempdir)); - test.ok(fs.existsSync(path.join(tempdir, 'asset.a7a79cdf84b802ea8b198059ff899cffc095a1b9606e919f98e05bf80779756b.zip'))); - test.done(); - }, - - 'copy directory under .assets/fingerprint/**'(test: Test) { - const tempdir = mkdtempSync(); - process.chdir(tempdir); // change current directory to somewhere in /tmp - - // GIVEN - const app = new cdk.App({ outdir: tempdir }); - const stack = new cdk.Stack(app, 'stack'); - - // WHEN - new Asset(stack, 'ZipDirectory', { - path: SAMPLE_ASSET_DIR, - }); - - // THEN - app.synth(); - test.ok(fs.existsSync(tempdir)); - const hash = 'asset.6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2'; - test.ok(fs.existsSync(path.join(tempdir, hash, 'sample-asset-file.txt'))); - test.ok(fs.existsSync(path.join(tempdir, hash, 'sample-jar-asset.jar'))); - fs.readdirSync(tempdir); - test.done(); - }, - - 'staging path is relative if the dir is below the working directory'(test: Test) { - // GIVEN - const tempdir = mkdtempSync(); - process.chdir(tempdir); // change current directory to somewhere in /tmp - - const staging = '.my-awesome-staging-directory'; - const app = new cdk.App({ - outdir: staging, - context: { - [cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT]: 'true', - }, - }); - - const stack = new cdk.Stack(app, 'stack'); - - const resource = new cdk.CfnResource(stack, 'MyResource', { type: 'My::Resource::Type' }); - const asset = new Asset(stack, 'MyAsset', { path: SAMPLE_ASSET_DIR }); - - // WHEN - asset.addResourceMetadata(resource, 'PropName'); - - const template = SynthUtils.synthesize(stack).template; - test.deepEqual(template.Resources.MyResource.Metadata, { - 'aws:asset:path': 'asset.6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', - 'aws:asset:property': 'PropName', - }); - test.done(); - }, - - 'if staging is disabled, asset path is absolute'(test: Test) { - // GIVEN - const staging = path.resolve(mkdtempSync()); - const app = new cdk.App({ - outdir: staging, - context: { - [cxapi.DISABLE_ASSET_STAGING_CONTEXT]: 'true', - [cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT]: 'true', - }, - }); - - const stack = new cdk.Stack(app, 'stack'); - - const resource = new cdk.CfnResource(stack, 'MyResource', { type: 'My::Resource::Type' }); - const asset = new Asset(stack, 'MyAsset', { path: SAMPLE_ASSET_DIR }); - - // WHEN - asset.addResourceMetadata(resource, 'PropName'); - - const template = SynthUtils.synthesize(stack).template; - test.deepEqual(template.Resources.MyResource.Metadata, { - 'aws:asset:path': SAMPLE_ASSET_DIR, - 'aws:asset:property': 'PropName', - }); - test.done(); - }, - - 'cdk metadata points to staged asset'(test: Test) { - // GIVEN - const app = new cdk.App(); - const stack = new cdk.Stack(app, 'stack'); - new Asset(stack, 'MyAsset', { path: SAMPLE_ASSET_DIR }); - - // WHEN - const session = app.synth(); - const artifact = session.getStackByName(stack.stackName); - const metadata = artifact.manifest.metadata || {}; - const md = Object.values(metadata)[0]![0]!.data as cxschema.AssetMetadataEntry; - test.deepEqual(md.path, 'asset.6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2'); - test.done(); - }, - - }, -}; - -function mkdtempSync() { - return fs.mkdtempSync(path.join(os.tmpdir(), 'test.assets')); -} From 8038dacba26b29a8839402420ac31ae7b1b724f2 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Sun, 7 Jun 2020 09:26:30 +0200 Subject: [PATCH 11/32] chore(bootstrap): split file/image publishing roles (#8403) For security purposes, we decided that it would be lower risk to assume a different role when we publish S3 assets and when we publish ECR assets. The reason is that ECR publishers execute `docker build` which can potentially execute 3rd party code (via a base docker image). This change modifies the conventional name for the publishing roles as well as adds a set of properties to the `DefaultStackSynthesizer` to allow customization as needed. This is a resubmission of #8319. That one was failing backwards regression tests... and for good reason! However in this case, the regression was intended (and deemed acceptable since we haven't officially "released" the feature we're breaking yet). Unfortunately the mechanism to skip integration tests during the regression tests has been broken recently, so had to be reintroduced here. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- allowed-breaking-changes.txt | 4 + .../stack-synthesizers/default-synthesizer.ts | 62 ++++++--- .../test.new-style-synthesis.ts | 74 +++++++++- packages/aws-cdk/.gitignore | 5 +- packages/aws-cdk/.npmignore | 3 + .../lib/api/bootstrap/bootstrap-template.yaml | 48 ++++++- .../integ/cli-regression-patches/README.md | 54 ++++++++ .../cli-regression-patches/v1.44.0/NOTES.md | 18 +++ .../v1.44.0/bootstrapping.integtest.js | 126 ++++++++++++++++++ .../v1.44.0/test.sh} | 15 ++- packages/aws-cdk/test/integ/cli.exclusions.js | 70 ---------- packages/aws-cdk/test/integ/cli/README.md | 7 +- packages/aws-cdk/test/integ/cli/app/app.js | 2 +- .../aws-cdk/test/integ/cli/aws-helpers.ts | 5 + .../test/integ/cli/bootstrapping.integtest.ts | 36 ++++- .../aws-cdk/test/integ/cli/cdk-helpers.ts | 8 +- .../aws-cdk/test/integ/cli/cli.integtest.ts | 57 ++++---- packages/aws-cdk/test/integ/cli/jest.setup.js | 8 -- .../aws-cdk/test/integ/cli/skip-tests.txt | 8 ++ .../aws-cdk/test/integ/cli/test-helpers.ts | 23 ++++ packages/aws-cdk/test/integ/cli/test.sh | 47 ++----- ...est-cli-regression-against-current-code.sh | 14 +- ...t-cli-regression-against-latest-release.sh | 2 +- packages/cdk-assets/lib/private/shell.ts | 6 +- 24 files changed, 504 insertions(+), 198 deletions(-) create mode 100644 packages/aws-cdk/test/integ/cli-regression-patches/README.md create mode 100644 packages/aws-cdk/test/integ/cli-regression-patches/v1.44.0/NOTES.md create mode 100644 packages/aws-cdk/test/integ/cli-regression-patches/v1.44.0/bootstrapping.integtest.js rename packages/aws-cdk/test/integ/{cli/test-jest.sh => cli-regression-patches/v1.44.0/test.sh} (52%) delete mode 100644 packages/aws-cdk/test/integ/cli.exclusions.js delete mode 100644 packages/aws-cdk/test/integ/cli/jest.setup.js create mode 100644 packages/aws-cdk/test/integ/cli/skip-tests.txt create mode 100644 packages/aws-cdk/test/integ/cli/test-helpers.ts diff --git a/allowed-breaking-changes.txt b/allowed-breaking-changes.txt index 8b137891791fe..e6bdc57ed11ae 100644 --- a/allowed-breaking-changes.txt +++ b/allowed-breaking-changes.txt @@ -1 +1,5 @@ +removed:@aws-cdk/core.BootstraplessSynthesizer.DEFAULT_ASSET_PUBLISHING_ROLE_ARN +removed:@aws-cdk/core.DefaultStackSynthesizer.DEFAULT_ASSET_PUBLISHING_ROLE_ARN +removed:@aws-cdk/core.DefaultStackSynthesizerProps.assetPublishingExternalId +removed:@aws-cdk/core.DefaultStackSynthesizerProps.assetPublishingRoleArn diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts index ace086a9c4bd3..5cef2ac3daab4 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts @@ -13,6 +13,11 @@ import { IStackSynthesizer } from './types'; export const BOOTSTRAP_QUALIFIER_CONTEXT = '@aws-cdk/core:bootstrapQualifier'; +/** + * The minimum bootstrap stack version required by this app. + */ +const MIN_BOOTSTRAP_STACK_VERSION = 2; + /** * Configuration properties for DefaultStackSynthesizer */ @@ -44,7 +49,7 @@ export interface DefaultStackSynthesizerProps { readonly imageAssetsRepositoryName?: string; /** - * The role to use to publish assets to this environment + * The role to use to publish file assets to the S3 bucket in this environment * * You must supply this if you have given a non-standard name to the publishing role. * @@ -52,16 +57,36 @@ export interface DefaultStackSynthesizerProps { * be replaced with the values of qualifier and the stack's account and region, * respectively. * - * @default DefaultStackSynthesizer.DEFAULT_ASSET_PUBLISHING_ROLE_ARN + * @default DefaultStackSynthesizer.DEFAULT_FILE_ASSET_PUBLISHING_ROLE_ARN */ - readonly assetPublishingRoleArn?: string; + readonly fileAssetPublishingRoleArn?: string; /** - * External ID to use when assuming role for asset publishing + * External ID to use when assuming role for file asset publishing * * @default - No external ID */ - readonly assetPublishingExternalId?: string; + readonly fileAssetPublishingExternalId?: string; + + /** + * The role to use to publish image assets to the ECR repository in this environment + * + * You must supply this if you have given a non-standard name to the publishing role. + * + * The placeholders `${Qualifier}`, `${AWS::AccountId}` and `${AWS::Region}` will + * be replaced with the values of qualifier and the stack's account and region, + * respectively. + * + * @default DefaultStackSynthesizer.DEFAULT_IMAGE_ASSET_PUBLISHING_ROLE_ARN + */ + readonly imageAssetPublishingRoleArn?: string; + + /** + * External ID to use when assuming role for image asset publishing + * + * @default - No external ID + */ + readonly imageAssetPublishingExternalId?: string; /** * The role to assume to initiate a deployment in this environment @@ -126,9 +151,14 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { public static readonly DEFAULT_DEPLOY_ROLE_ARN = 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-${Qualifier}-deploy-role-${AWS::AccountId}-${AWS::Region}'; /** - * Default asset publishing role ARN. + * Default asset publishing role ARN for file (S3) assets. + */ + public static readonly DEFAULT_FILE_ASSET_PUBLISHING_ROLE_ARN = 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-${Qualifier}-file-publishing-role-${AWS::AccountId}-${AWS::Region}'; + + /** + * Default asset publishing role ARN for image (ECR) assets. */ - public static readonly DEFAULT_ASSET_PUBLISHING_ROLE_ARN = 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-${Qualifier}-publishing-role-${AWS::AccountId}-${AWS::Region}'; + public static readonly DEFAULT_IMAGE_ASSET_PUBLISHING_ROLE_ARN = 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-${Qualifier}-image-publishing-role-${AWS::AccountId}-${AWS::Region}'; /** * Default image assets repository name @@ -145,7 +175,8 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { private repositoryName?: string; private _deployRoleArn?: string; private _cloudFormationExecutionRoleArn?: string; - private assetPublishingRoleArn?: string; + private fileAssetPublishingRoleArn?: string; + private imageAssetPublishingRoleArn?: string; private readonly files: NonNullable = {}; private readonly dockerImages: NonNullable = {}; @@ -178,7 +209,8 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { this.repositoryName = specialize(this.props.imageAssetsRepositoryName ?? DefaultStackSynthesizer.DEFAULT_IMAGE_ASSETS_REPOSITORY_NAME); this._deployRoleArn = specialize(this.props.deployRoleArn ?? DefaultStackSynthesizer.DEFAULT_DEPLOY_ROLE_ARN); this._cloudFormationExecutionRoleArn = specialize(this.props.cloudFormationExecutionRole ?? DefaultStackSynthesizer.DEFAULT_CLOUDFORMATION_ROLE_ARN); - this.assetPublishingRoleArn = specialize(this.props.assetPublishingRoleArn ?? DefaultStackSynthesizer.DEFAULT_ASSET_PUBLISHING_ROLE_ARN); + this.fileAssetPublishingRoleArn = specialize(this.props.fileAssetPublishingRoleArn ?? DefaultStackSynthesizer.DEFAULT_FILE_ASSET_PUBLISHING_ROLE_ARN); + this.imageAssetPublishingRoleArn = specialize(this.props.imageAssetPublishingRoleArn ?? DefaultStackSynthesizer.DEFAULT_IMAGE_ASSET_PUBLISHING_ROLE_ARN); // tslint:enable:max-line-length } @@ -199,8 +231,8 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { bucketName: this.bucketName, objectKey, region: resolvedOr(this.stack.region, undefined), - assumeRoleArn: this.assetPublishingRoleArn, - assumeRoleExternalId: this.props.assetPublishingExternalId, + assumeRoleArn: this.fileAssetPublishingRoleArn, + assumeRoleExternalId: this.props.fileAssetPublishingExternalId, }, }, }; @@ -237,8 +269,8 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { repositoryName: this.repositoryName, imageTag, region: resolvedOr(this.stack.region, undefined), - assumeRoleArn: this.assetPublishingRoleArn, - assumeRoleExternalId: this.props.assetPublishingExternalId, + assumeRoleArn: this.imageAssetPublishingRoleArn, + assumeRoleExternalId: this.props.imageAssetPublishingExternalId, }, }, }; @@ -262,7 +294,7 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { assumeRoleArn: this._deployRoleArn, cloudFormationExecutionRoleArn: this._cloudFormationExecutionRoleArn, stackTemplateAssetObjectUrl: templateManifestUrl, - requiresBootstrapStackVersion: 1, + requiresBootstrapStackVersion: MIN_BOOTSTRAP_STACK_VERSION, }, [artifactId]); } @@ -344,7 +376,7 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { type: cxschema.ArtifactType.ASSET_MANIFEST, properties: { file: manifestFile, - requiresBootstrapStackVersion: 1, + requiresBootstrapStackVersion: MIN_BOOTSTRAP_STACK_VERSION, }, }); diff --git a/packages/@aws-cdk/core/test/stack-synthesis/test.new-style-synthesis.ts b/packages/@aws-cdk/core/test/stack-synthesis/test.new-style-synthesis.ts index 723e7969c1d06..43591b9931148 100644 --- a/packages/@aws-cdk/core/test/stack-synthesis/test.new-style-synthesis.ts +++ b/packages/@aws-cdk/core/test/stack-synthesis/test.new-style-synthesis.ts @@ -2,7 +2,7 @@ import * as asset_schema from '@aws-cdk/cdk-assets-schema'; import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs'; import { Test } from 'nodeunit'; -import { App, CfnResource, FileAssetPackaging, Stack } from '../../lib'; +import { App, CfnResource, DefaultStackSynthesizer, FileAssetPackaging, Stack } from '../../lib'; import { evaluateCFN } from '../evaluate-cfn'; const CFN_CONTEXT = { @@ -50,7 +50,7 @@ export = { 'current_account-current_region': { bucketName: 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', objectKey: '4bdae6e3b1b15f08c889d6c9133f24731ee14827a9a9ab9b6b6a9b42b6d34910', - assumeRoleArn: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-publishing-role-${AWS::AccountId}-${AWS::Region}', + assumeRoleArn: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}', }, }, }); @@ -106,22 +106,75 @@ export = { const asm = app.synth(); // THEN - we have an asset manifest with both assets and the stack template in there - const manifestArtifact = asm.artifacts.filter(isAssetManifest)[0]; - test.ok(manifestArtifact); - const manifest: asset_schema.ManifestFile = JSON.parse(fs.readFileSync(manifestArtifact.file, { encoding: 'utf-8' })); + const manifest = readAssetManifest(asm); test.equals(Object.keys(manifest.files || {}).length, 2); test.equals(Object.keys(manifest.dockerImages || {}).length, 1); // THEN - every artifact has an assumeRoleArn - for (const file of Object.values({...manifest.files, ...manifest.dockerImages})) { + for (const file of Object.values(manifest.files ?? {})) { + for (const destination of Object.values(file.destinations)) { + test.deepEqual(destination.assumeRoleArn, 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}'); + } + } + + for (const file of Object.values(manifest.dockerImages ?? {})) { for (const destination of Object.values(file.destinations)) { - test.ok(destination.assumeRoleArn); + test.deepEqual(destination.assumeRoleArn, 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-image-publishing-role-${AWS::AccountId}-${AWS::Region}'); } } test.done(); }, + + 'customize publishing resources'(test: Test) { + // GIVEN + const myapp = new App(); + + // WHEN + const mystack = new Stack(myapp, 'mystack', { + synthesizer: new DefaultStackSynthesizer({ + fileAssetsBucketName: 'file-asset-bucket', + fileAssetPublishingRoleArn: 'file:role:arn', + fileAssetPublishingExternalId: 'file-external-id', + + imageAssetsRepositoryName: 'image-ecr-repository', + imageAssetPublishingRoleArn: 'image:role:arn', + imageAssetPublishingExternalId: 'image-external-id', + }), + }); + + mystack.synthesizer.addFileAsset({ + fileName: __filename, + packaging: FileAssetPackaging.FILE, + sourceHash: 'file-asset-hash', + }); + + mystack.synthesizer.addDockerImageAsset({ + directoryName: '.', + sourceHash: 'docker-asset-hash', + }); + + // THEN + const asm = myapp.synth(); + const manifest = readAssetManifest(asm); + + test.deepEqual(manifest.files?.['file-asset-hash']?.destinations?.['current_account-current_region'], { + bucketName: 'file-asset-bucket', + objectKey: 'file-asset-hash', + assumeRoleArn: 'file:role:arn', + assumeRoleExternalId: 'file-external-id', + }); + + test.deepEqual(manifest.dockerImages?.['docker-asset-hash']?.destinations?.['current_account-current_region'] , { + repositoryName: 'image-ecr-repository', + imageTag: 'docker-asset-hash', + assumeRoleArn: 'image:role:arn', + assumeRoleExternalId: 'image-external-id', + }); + + test.done(); + }, }; /** @@ -135,4 +188,11 @@ function evalCFN(value: any) { function isAssetManifest(x: cxapi.CloudArtifact): x is cxapi.AssetManifestArtifact { return x instanceof cxapi.AssetManifestArtifact; +} + +function readAssetManifest(asm: cxapi.CloudAssembly): asset_schema.ManifestFile { + const manifestArtifact = asm.artifacts.filter(isAssetManifest)[0]; + if (!manifestArtifact) { throw new Error('no asset manifest in assembly'); } + + return JSON.parse(fs.readFileSync(manifestArtifact.file, { encoding: 'utf-8' })); } \ No newline at end of file diff --git a/packages/aws-cdk/.gitignore b/packages/aws-cdk/.gitignore index 35d7fd343f085..59c135c41f21b 100644 --- a/packages/aws-cdk/.gitignore +++ b/packages/aws-cdk/.gitignore @@ -32,5 +32,6 @@ cdk.context.json # as the subdirs contain .js files that should be committed) test/integ/cli/*.js test/integ/cli/*.d.ts -!test/integ/cli/jest.setup.js -!test/integ/cli/jest.config.js +!test/integ/cli-regression-patches/**/* + +.DS_Store diff --git a/packages/aws-cdk/.npmignore b/packages/aws-cdk/.npmignore index 2a37a179d8d4a..49b9729723982 100644 --- a/packages/aws-cdk/.npmignore +++ b/packages/aws-cdk/.npmignore @@ -25,3 +25,6 @@ tsconfig.json jest.config.js !lib/init-templates/**/jest.config.js !test/integ/cli/jest.config.js +!test/integ/cli-regression-patches/**/* + +.DS_Store diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml b/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml index 4da1a80bbeedc..5b61c2e99e7dd 100644 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml @@ -106,7 +106,7 @@ Resources: Effect: Allow Principal: AWS: - Fn::Sub: "${PublishingRole.Arn}" + Fn::Sub: "${FilePublishingRole.Arn}" Resource: "*" Condition: CreateNewKey StagingBucket: @@ -158,7 +158,7 @@ Resources: - HasCustomContainerAssetsRepositoryName - Fn::Sub: "${ContainerAssetsRepositoryName}" - Fn::Sub: cdk-${Qualifier}-container-assets-${AWS::AccountId}-${AWS::Region} - PublishingRole: + FilePublishingRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: @@ -177,8 +177,28 @@ Resources: Ref: TrustedAccounts - Ref: AWS::NoValue RoleName: - Fn::Sub: cdk-${Qualifier}-publishing-role-${AWS::AccountId}-${AWS::Region} - PublishingRoleDefaultPolicy: + Fn::Sub: cdk-${Qualifier}-file-publishing-role-${AWS::AccountId}-${AWS::Region} + ImagePublishingRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Fn::If: + - HasTrustedAccounts + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + - Ref: AWS::NoValue + RoleName: + Fn::Sub: cdk-${Qualifier}-image-publishing-role-${AWS::AccountId}-${AWS::Region} + FilePublishingRoleDefaultPolicy: Type: AWS::IAM::Policy Properties: PolicyDocument: @@ -206,6 +226,16 @@ Resources: - CreateNewKey - Fn::Sub: "${FileAssetsBucketEncryptionKey.Arn}" - Fn::Sub: arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/${FileAssetsBucketKmsKeyId} + Version: '2012-10-17' + Roles: + - Ref: FilePublishingRole + PolicyName: + Fn::Sub: cdk-${Qualifier}-file-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region} + ImagePublishingRoleDefaultPolicy: + Type: AWS::IAM::Policy + Properties: + PolicyDocument: + Statement: - Action: - ecr:PutImage - ecr:InitiateLayerUpload @@ -223,9 +253,9 @@ Resources: Effect: Allow Version: '2012-10-17' Roles: - - Ref: PublishingRole + - Ref: ImagePublishingRole PolicyName: - Fn::Sub: cdk-${Qualifier}-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region} + Fn::Sub: cdk-${Qualifier}-image-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region} DeploymentActionRole: Type: AWS::IAM::Role Properties: @@ -317,10 +347,14 @@ Outputs: Description: The domain name of the S3 bucket owned by the CDK toolkit stack Value: Fn::Sub: "${StagingBucket.RegionalDomainName}" + ImageRepositoryName: + Description: The name of the ECR repository which hosts docker image assets + Value: + Fn::Sub: "${ContainerAssetsRepository}" BootstrapVersion: Description: The version of the bootstrap resources that are currently mastered in this stack - Value: '1' + Value: '2' Export: Name: Fn::Sub: CdkBootstrap-${Qualifier}-Version \ No newline at end of file diff --git a/packages/aws-cdk/test/integ/cli-regression-patches/README.md b/packages/aws-cdk/test/integ/cli-regression-patches/README.md new file mode 100644 index 0000000000000..c930255e85809 --- /dev/null +++ b/packages/aws-cdk/test/integ/cli-regression-patches/README.md @@ -0,0 +1,54 @@ +Regression Test Patches +======================== + +The regression test suite will use the test suite of an OLD version +of the CLI when testing a NEW version of the CLI, to make sure the +old tests still pass. + +Sometimes though, the old tests won't pass. This can happen when we +introduce breaking changes to the framework or CLI (for something serious, +such as security reasons), or maybe because we had a bug in an old +version that happened to pass, but now the test needs to be updated +in order to pass a bugfix. + +## Mechanism + +The files in this directory will be copied over the test directory +so that you can exclude tests from running, or patch up test running +scripts. + +Files will be copied like so: + +``` +aws-cdk/test/integ/cli-regression-patches/vX.Y.Z/* + +# will be copied into + +aws-cdk/test/integ/cli +``` + +For example, to skip a certain integration test during regression +testing, create the following file: + +``` +cli-regression-patches/vX.Y.Z/skip-tests.txt +``` + +If you need to replace source files, it's probably best to stick +compiled `.js` files in here. `.ts` source files wouldn't compile +because they'd be missing `imports`. + +## Versioning + +The patch sets are versioned, so that they will only be applied for +a certain version of the tests and will automatically age out when +we proceed past that release. + +The version in the directory name needs to be named after the +version that contains the *tests* we're running, that need to be +patched. + +So for example, if we are running regression tests for release +candidate `1.45.0`, we would use the tests from released version +`1.44.0`, and so you would call the patch directory `v1.44.0`. + diff --git a/packages/aws-cdk/test/integ/cli-regression-patches/v1.44.0/NOTES.md b/packages/aws-cdk/test/integ/cli-regression-patches/v1.44.0/NOTES.md new file mode 100644 index 0000000000000..961813bc3107b --- /dev/null +++ b/packages/aws-cdk/test/integ/cli-regression-patches/v1.44.0/NOTES.md @@ -0,0 +1,18 @@ +Patch notes: + +- Replace `test.sh` since we removed the old test exclusion + mechanism, and the `cli.exclusions.js` file that the old `test.sh` + depended upon. + +- We removed the old asset-publishing role from the new bootstrap + stack, and split it into separate file- and docker-publishing roles. + Since 1.44.0 would still expect the old asset-publishing role, + its test would fail, so we disable it: + +``` +test.skip('deploy new style synthesis to new style bootstrap', async () => { +``` + +There is a better mechanism for skipping certain tests by using `skip-tests.txt`, +but that one is only available AFTER this release, so for this version we just replace +source files. diff --git a/packages/aws-cdk/test/integ/cli-regression-patches/v1.44.0/bootstrapping.integtest.js b/packages/aws-cdk/test/integ/cli-regression-patches/v1.44.0/bootstrapping.integtest.js new file mode 100644 index 0000000000000..d715afa923e1d --- /dev/null +++ b/packages/aws-cdk/test/integ/cli-regression-patches/v1.44.0/bootstrapping.integtest.js @@ -0,0 +1,126 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const aws_helpers_1 = require("./aws-helpers"); +const cdk_helpers_1 = require("./cdk-helpers"); +jest.setTimeout(600000); +const QUALIFIER = randomString(); +beforeAll(async () => { + await cdk_helpers_1.prepareAppFixture(); +}); +beforeEach(async () => { + await cdk_helpers_1.cleanup(); +}); +afterEach(async () => { + await cdk_helpers_1.cleanup(); +}); +test('can bootstrap without execution', async () => { + var _a; + const bootstrapStackName = cdk_helpers_1.fullStackName('bootstrap-stack'); + await cdk_helpers_1.cdk(['bootstrap', + '--toolkit-stack-name', bootstrapStackName, + '--no-execute']); + const resp = await aws_helpers_1.cloudFormation('describeStacks', { + StackName: bootstrapStackName, + }); + expect((_a = resp.Stacks) === null || _a === void 0 ? void 0 : _a[0].StackStatus).toEqual('REVIEW_IN_PROGRESS'); +}); +test('upgrade legacy bootstrap stack to new bootstrap stack while in use', async () => { + const bootstrapStackName = cdk_helpers_1.fullStackName('bootstrap-stack'); + const legacyBootstrapBucketName = `aws-cdk-bootstrap-integ-test-legacy-bckt-${randomString()}`; + const newBootstrapBucketName = `aws-cdk-bootstrap-integ-test-v2-bckt-${randomString()}`; + cdk_helpers_1.rememberToDeleteBucket(legacyBootstrapBucketName); // This one will leak + cdk_helpers_1.rememberToDeleteBucket(newBootstrapBucketName); // This one shouldn't leak if the test succeeds, but let's be safe in case it doesn't + // Legacy bootstrap + await cdk_helpers_1.cdk(['bootstrap', + '--toolkit-stack-name', bootstrapStackName, + '--bootstrap-bucket-name', legacyBootstrapBucketName]); + // Deploy stack that uses file assets + await cdk_helpers_1.cdkDeploy('lambda', { + options: ['--toolkit-stack-name', bootstrapStackName], + }); + // Upgrade bootstrap stack to "new" style + await cdk_helpers_1.cdk(['bootstrap', + '--toolkit-stack-name', bootstrapStackName, + '--bootstrap-bucket-name', newBootstrapBucketName, + '--qualifier', QUALIFIER], { + modEnv: { + CDK_NEW_BOOTSTRAP: '1', + }, + }); + // (Force) deploy stack again + // --force to bypass the check which says that the template hasn't changed. + await cdk_helpers_1.cdkDeploy('lambda', { + options: [ + '--toolkit-stack-name', bootstrapStackName, + '--force', + ], + }); +}); +test.skip('deploy new style synthesis to new style bootstrap', async () => { + const bootstrapStackName = cdk_helpers_1.fullStackName('bootstrap-stack'); + await cdk_helpers_1.cdk(['bootstrap', + '--toolkit-stack-name', bootstrapStackName, + '--qualifier', QUALIFIER, + '--cloudformation-execution-policies', 'arn:aws:iam::aws:policy/AdministratorAccess', + ], { + modEnv: { + CDK_NEW_BOOTSTRAP: '1', + }, + }); + // Deploy stack that uses file assets + await cdk_helpers_1.cdkDeploy('lambda', { + options: [ + '--toolkit-stack-name', bootstrapStackName, + '--context', `@aws-cdk/core:bootstrapQualifier=${QUALIFIER}`, + '--context', '@aws-cdk/core:newStyleStackSynthesis=1', + ], + }); +}); +test('deploy old style synthesis to new style bootstrap', async () => { + const bootstrapStackName = cdk_helpers_1.fullStackName('bootstrap-stack'); + await cdk_helpers_1.cdk(['bootstrap', + '--toolkit-stack-name', bootstrapStackName, + '--qualifier', QUALIFIER, + '--cloudformation-execution-policies', 'arn:aws:iam::aws:policy/AdministratorAccess', + ], { + modEnv: { + CDK_NEW_BOOTSTRAP: '1', + }, + }); + // Deploy stack that uses file assets + await cdk_helpers_1.cdkDeploy('lambda', { + options: [ + '--toolkit-stack-name', bootstrapStackName, + ], + }); +}); +test('deploying new style synthesis to old style bootstrap fails', async () => { + const bootstrapStackName = cdk_helpers_1.fullStackName('bootstrap-stack'); + await cdk_helpers_1.cdk(['bootstrap', '--toolkit-stack-name', bootstrapStackName]); + // Deploy stack that uses file assets, this fails because the bootstrap stack + // is version checked. + await expect(cdk_helpers_1.cdkDeploy('lambda', { + options: [ + '--toolkit-stack-name', bootstrapStackName, + '--context', '@aws-cdk/core:newStyleStackSynthesis=1', + ], + })).rejects.toThrow('exited with error'); +}); +test('can create multiple legacy bootstrap stacks', async () => { + var _a; + const bootstrapStackName1 = cdk_helpers_1.fullStackName('bootstrap-stack-1'); + const bootstrapStackName2 = cdk_helpers_1.fullStackName('bootstrap-stack-2'); + // deploy two toolkit stacks into the same environment (see #1416) + // one with tags + await cdk_helpers_1.cdk(['bootstrap', '-v', '--toolkit-stack-name', bootstrapStackName1, '--tags', 'Foo=Bar']); + await cdk_helpers_1.cdk(['bootstrap', '-v', '--toolkit-stack-name', bootstrapStackName2]); + const response = await aws_helpers_1.cloudFormation('describeStacks', { StackName: bootstrapStackName1 }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].Tags).toEqual([ + { Key: 'Foo', Value: 'Bar' }, + ]); +}); +function randomString() { + // Crazy + return Math.random().toString(36).replace(/[^a-z0-9]+/g, ''); +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYm9vdHN0cmFwcGluZy5pbnRlZ3Rlc3QuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJib290c3RyYXBwaW5nLmludGVndGVzdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLCtDQUErQztBQUMvQywrQ0FBa0g7QUFFbEgsSUFBSSxDQUFDLFVBQVUsQ0FBQyxNQUFPLENBQUMsQ0FBQztBQUV6QixNQUFNLFNBQVMsR0FBRyxZQUFZLEVBQUUsQ0FBQztBQUVqQyxTQUFTLENBQUMsS0FBSyxJQUFJLEVBQUU7SUFDbkIsTUFBTSwrQkFBaUIsRUFBRSxDQUFDO0FBQzVCLENBQUMsQ0FBQyxDQUFDO0FBRUgsVUFBVSxDQUFDLEtBQUssSUFBSSxFQUFFO0lBQ3BCLE1BQU0scUJBQU8sRUFBRSxDQUFDO0FBQ2xCLENBQUMsQ0FBQyxDQUFDO0FBRUgsU0FBUyxDQUFDLEtBQUssSUFBSSxFQUFFO0lBQ25CLE1BQU0scUJBQU8sRUFBRSxDQUFDO0FBQ2xCLENBQUMsQ0FBQyxDQUFDO0FBRUgsSUFBSSxDQUFDLGlDQUFpQyxFQUFFLEtBQUssSUFBSSxFQUFFOztJQUNqRCxNQUFNLGtCQUFrQixHQUFHLDJCQUFhLENBQUMsaUJBQWlCLENBQUMsQ0FBQztJQUU1RCxNQUFNLGlCQUFHLENBQUMsQ0FBQyxXQUFXO1FBQ3BCLHNCQUFzQixFQUFFLGtCQUFrQjtRQUMxQyxjQUFjLENBQUMsQ0FBQyxDQUFDO0lBRW5CLE1BQU0sSUFBSSxHQUFHLE1BQU0sNEJBQWMsQ0FBQyxnQkFBZ0IsRUFBRTtRQUNsRCxTQUFTLEVBQUUsa0JBQWtCO0tBQzlCLENBQUMsQ0FBQztJQUVILE1BQU0sT0FBQyxJQUFJLENBQUMsTUFBTSwwQ0FBRyxDQUFDLEVBQUUsV0FBVyxDQUFDLENBQUMsT0FBTyxDQUFDLG9CQUFvQixDQUFDLENBQUM7QUFDckUsQ0FBQyxDQUFDLENBQUM7QUFFSCxJQUFJLENBQUMsb0VBQW9FLEVBQUUsS0FBSyxJQUFJLEVBQUU7SUFDcEYsTUFBTSxrQkFBa0IsR0FBRywyQkFBYSxDQUFDLGlCQUFpQixDQUFDLENBQUM7SUFFNUQsTUFBTSx5QkFBeUIsR0FBRyw0Q0FBNEMsWUFBWSxFQUFFLEVBQUUsQ0FBQztJQUMvRixNQUFNLHNCQUFzQixHQUFHLHdDQUF3QyxZQUFZLEVBQUUsRUFBRSxDQUFDO0lBQ3hGLG9DQUFzQixDQUFDLHlCQUF5QixDQUFDLENBQUMsQ0FBRSxxQkFBcUI7SUFDekUsb0NBQXNCLENBQUMsc0JBQXNCLENBQUMsQ0FBQyxDQUFLLHFGQUFxRjtJQUV6SSxtQkFBbUI7SUFDbkIsTUFBTSxpQkFBRyxDQUFDLENBQUMsV0FBVztRQUNwQixzQkFBc0IsRUFBRSxrQkFBa0I7UUFDMUMseUJBQXlCLEVBQUUseUJBQXlCLENBQUMsQ0FBQyxDQUFDO0lBRXpELHFDQUFxQztJQUNyQyxNQUFNLHVCQUFTLENBQUMsUUFBUSxFQUFFO1FBQ3hCLE9BQU8sRUFBRSxDQUFDLHNCQUFzQixFQUFFLGtCQUFrQixDQUFDO0tBQ3RELENBQUMsQ0FBQztJQUVILHlDQUF5QztJQUN6QyxNQUFNLGlCQUFHLENBQUMsQ0FBQyxXQUFXO1FBQ3BCLHNCQUFzQixFQUFFLGtCQUFrQjtRQUMxQyx5QkFBeUIsRUFBRSxzQkFBc0I7UUFDakQsYUFBYSxFQUFFLFNBQVMsQ0FBQyxFQUFFO1FBQzNCLE1BQU0sRUFBRTtZQUNOLGlCQUFpQixFQUFFLEdBQUc7U0FDdkI7S0FDRixDQUFDLENBQUM7SUFFSCw2QkFBNkI7SUFDN0IsMkVBQTJFO0lBQzNFLE1BQU0sdUJBQVMsQ0FBQyxRQUFRLEVBQUU7UUFDeEIsT0FBTyxFQUFFO1lBQ1Asc0JBQXNCLEVBQUUsa0JBQWtCO1lBQzFDLFNBQVM7U0FDVjtLQUNGLENBQUMsQ0FBQztBQUNMLENBQUMsQ0FBQyxDQUFDO0FBRUgsSUFBSSxDQUFDLG1EQUFtRCxFQUFFLEtBQUssSUFBSSxFQUFFO0lBQ25FLE1BQU0sa0JBQWtCLEdBQUcsMkJBQWEsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0lBRTVELE1BQU0saUJBQUcsQ0FBQyxDQUFDLFdBQVc7UUFDcEIsc0JBQXNCLEVBQUUsa0JBQWtCO1FBQzFDLGFBQWEsRUFBRSxTQUFTO1FBQ3hCLHFDQUFxQyxFQUFFLDZDQUE2QztLQUNyRixFQUFFO1FBQ0QsTUFBTSxFQUFFO1lBQ04saUJBQWlCLEVBQUUsR0FBRztTQUN2QjtLQUNGLENBQUMsQ0FBQztJQUVILHFDQUFxQztJQUNyQyxNQUFNLHVCQUFTLENBQUMsUUFBUSxFQUFFO1FBQ3hCLE9BQU8sRUFBRTtZQUNQLHNCQUFzQixFQUFFLGtCQUFrQjtZQUMxQyxXQUFXLEVBQUUsb0NBQW9DLFNBQVMsRUFBRTtZQUM1RCxXQUFXLEVBQUUsd0NBQXdDO1NBQ3REO0tBQ0YsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyxDQUFDLENBQUM7QUFFSCxJQUFJLENBQUMsbURBQW1ELEVBQUUsS0FBSyxJQUFJLEVBQUU7SUFDbkUsTUFBTSxrQkFBa0IsR0FBRywyQkFBYSxDQUFDLGlCQUFpQixDQUFDLENBQUM7SUFFNUQsTUFBTSxpQkFBRyxDQUFDLENBQUMsV0FBVztRQUNwQixzQkFBc0IsRUFBRSxrQkFBa0I7UUFDMUMsYUFBYSxFQUFFLFNBQVM7UUFDeEIscUNBQXFDLEVBQUUsNkNBQTZDO0tBQ3JGLEVBQUU7UUFDRCxNQUFNLEVBQUU7WUFDTixpQkFBaUIsRUFBRSxHQUFHO1NBQ3ZCO0tBQ0YsQ0FBQyxDQUFDO0lBRUgscUNBQXFDO0lBQ3JDLE1BQU0sdUJBQVMsQ0FBQyxRQUFRLEVBQUU7UUFDeEIsT0FBTyxFQUFFO1lBQ1Asc0JBQXNCLEVBQUUsa0JBQWtCO1NBQzNDO0tBQ0YsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyxDQUFDLENBQUM7QUFFSCxJQUFJLENBQUMsNERBQTRELEVBQUUsS0FBSyxJQUFJLEVBQUU7SUFDNUUsTUFBTSxrQkFBa0IsR0FBRywyQkFBYSxDQUFDLGlCQUFpQixDQUFDLENBQUM7SUFFNUQsTUFBTSxpQkFBRyxDQUFDLENBQUMsV0FBVyxFQUFFLHNCQUFzQixFQUFFLGtCQUFrQixDQUFDLENBQUMsQ0FBQztJQUVyRSw2RUFBNkU7SUFDN0Usc0JBQXNCO0lBQ3RCLE1BQU0sTUFBTSxDQUFDLHVCQUFTLENBQUMsUUFBUSxFQUFFO1FBQy9CLE9BQU8sRUFBRTtZQUNQLHNCQUFzQixFQUFFLGtCQUFrQjtZQUMxQyxXQUFXLEVBQUUsd0NBQXdDO1NBQ3REO0tBQ0YsQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxtQkFBbUIsQ0FBQyxDQUFDO0FBQzNDLENBQUMsQ0FBQyxDQUFDO0FBRUgsSUFBSSxDQUFDLDZDQUE2QyxFQUFFLEtBQUssSUFBSSxFQUFFOztJQUM3RCxNQUFNLG1CQUFtQixHQUFHLDJCQUFhLENBQUMsbUJBQW1CLENBQUMsQ0FBQztJQUMvRCxNQUFNLG1CQUFtQixHQUFHLDJCQUFhLENBQUMsbUJBQW1CLENBQUMsQ0FBQztJQUUvRCxrRUFBa0U7SUFDbEUsZ0JBQWdCO0lBQ2hCLE1BQU0saUJBQUcsQ0FBQyxDQUFDLFdBQVcsRUFBRSxJQUFJLEVBQUUsc0JBQXNCLEVBQUUsbUJBQW1CLEVBQUUsUUFBUSxFQUFFLFNBQVMsQ0FBQyxDQUFDLENBQUM7SUFDakcsTUFBTSxpQkFBRyxDQUFDLENBQUMsV0FBVyxFQUFFLElBQUksRUFBRSxzQkFBc0IsRUFBRSxtQkFBbUIsQ0FBQyxDQUFDLENBQUM7SUFFNUUsTUFBTSxRQUFRLEdBQUcsTUFBTSw0QkFBYyxDQUFDLGdCQUFnQixFQUFFLEVBQUUsU0FBUyxFQUFFLG1CQUFtQixFQUFFLENBQUMsQ0FBQztJQUM1RixNQUFNLE9BQUMsUUFBUSxDQUFDLE1BQU0sMENBQUcsQ0FBQyxFQUFFLElBQUksQ0FBQyxDQUFDLE9BQU8sQ0FBQztRQUN4QyxFQUFFLEdBQUcsRUFBRSxLQUFLLEVBQUUsS0FBSyxFQUFFLEtBQUssRUFBRTtLQUM3QixDQUFDLENBQUM7QUFDTCxDQUFDLENBQUMsQ0FBQztBQUVILFNBQVMsWUFBWTtJQUNuQixRQUFRO0lBQ1IsT0FBTyxJQUFJLENBQUMsTUFBTSxFQUFFLENBQUMsUUFBUSxDQUFDLEVBQUUsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxhQUFhLEVBQUUsRUFBRSxDQUFDLENBQUM7QUFDL0QsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGNsb3VkRm9ybWF0aW9uIH0gZnJvbSAnLi9hd3MtaGVscGVycyc7XG5pbXBvcnQgeyBjZGssIGNka0RlcGxveSwgY2xlYW51cCwgZnVsbFN0YWNrTmFtZSwgcHJlcGFyZUFwcEZpeHR1cmUsIHJlbWVtYmVyVG9EZWxldGVCdWNrZXQgfSBmcm9tICcuL2Nkay1oZWxwZXJzJztcblxuamVzdC5zZXRUaW1lb3V0KDYwMF8wMDApO1xuXG5jb25zdCBRVUFMSUZJRVIgPSByYW5kb21TdHJpbmcoKTtcblxuYmVmb3JlQWxsKGFzeW5jICgpID0+IHtcbiAgYXdhaXQgcHJlcGFyZUFwcEZpeHR1cmUoKTtcbn0pO1xuXG5iZWZvcmVFYWNoKGFzeW5jICgpID0+IHtcbiAgYXdhaXQgY2xlYW51cCgpO1xufSk7XG5cbmFmdGVyRWFjaChhc3luYyAoKSA9PiB7XG4gIGF3YWl0IGNsZWFudXAoKTtcbn0pO1xuXG50ZXN0KCdjYW4gYm9vdHN0cmFwIHdpdGhvdXQgZXhlY3V0aW9uJywgYXN5bmMgKCkgPT4ge1xuICBjb25zdCBib290c3RyYXBTdGFja05hbWUgPSBmdWxsU3RhY2tOYW1lKCdib290c3RyYXAtc3RhY2snKTtcblxuICBhd2FpdCBjZGsoWydib290c3RyYXAnLFxuICAgICctLXRvb2xraXQtc3RhY2stbmFtZScsIGJvb3RzdHJhcFN0YWNrTmFtZSxcbiAgICAnLS1uby1leGVjdXRlJ10pO1xuXG4gIGNvbnN0IHJlc3AgPSBhd2FpdCBjbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBib290c3RyYXBTdGFja05hbWUsXG4gIH0pO1xuXG4gIGV4cGVjdChyZXNwLlN0YWNrcz8uWzBdLlN0YWNrU3RhdHVzKS50b0VxdWFsKCdSRVZJRVdfSU5fUFJPR1JFU1MnKTtcbn0pO1xuXG50ZXN0KCd1cGdyYWRlIGxlZ2FjeSBib290c3RyYXAgc3RhY2sgdG8gbmV3IGJvb3RzdHJhcCBzdGFjayB3aGlsZSBpbiB1c2UnLCBhc3luYyAoKSA9PiB7XG4gIGNvbnN0IGJvb3RzdHJhcFN0YWNrTmFtZSA9IGZ1bGxTdGFja05hbWUoJ2Jvb3RzdHJhcC1zdGFjaycpO1xuXG4gIGNvbnN0IGxlZ2FjeUJvb3RzdHJhcEJ1Y2tldE5hbWUgPSBgYXdzLWNkay1ib290c3RyYXAtaW50ZWctdGVzdC1sZWdhY3ktYmNrdC0ke3JhbmRvbVN0cmluZygpfWA7XG4gIGNvbnN0IG5ld0Jvb3RzdHJhcEJ1Y2tldE5hbWUgPSBgYXdzLWNkay1ib290c3RyYXAtaW50ZWctdGVzdC12Mi1iY2t0LSR7cmFuZG9tU3RyaW5nKCl9YDtcbiAgcmVtZW1iZXJUb0RlbGV0ZUJ1Y2tldChsZWdhY3lCb290c3RyYXBCdWNrZXROYW1lKTsgIC8vIFRoaXMgb25lIHdpbGwgbGVha1xuICByZW1lbWJlclRvRGVsZXRlQnVja2V0KG5ld0Jvb3RzdHJhcEJ1Y2tldE5hbWUpOyAgICAgLy8gVGhpcyBvbmUgc2hvdWxkbid0IGxlYWsgaWYgdGhlIHRlc3Qgc3VjY2VlZHMsIGJ1dCBsZXQncyBiZSBzYWZlIGluIGNhc2UgaXQgZG9lc24ndFxuXG4gIC8vIExlZ2FjeSBib290c3RyYXBcbiAgYXdhaXQgY2RrKFsnYm9vdHN0cmFwJyxcbiAgICAnLS10b29sa2l0LXN0YWNrLW5hbWUnLCBib290c3RyYXBTdGFja05hbWUsXG4gICAgJy0tYm9vdHN0cmFwLWJ1Y2tldC1uYW1lJywgbGVnYWN5Qm9vdHN0cmFwQnVja2V0TmFtZV0pO1xuXG4gIC8vIERlcGxveSBzdGFjayB0aGF0IHVzZXMgZmlsZSBhc3NldHNcbiAgYXdhaXQgY2RrRGVwbG95KCdsYW1iZGEnLCB7XG4gICAgb3B0aW9uczogWyctLXRvb2xraXQtc3RhY2stbmFtZScsIGJvb3RzdHJhcFN0YWNrTmFtZV0sXG4gIH0pO1xuXG4gIC8vIFVwZ3JhZGUgYm9vdHN0cmFwIHN0YWNrIHRvIFwibmV3XCIgc3R5bGVcbiAgYXdhaXQgY2RrKFsnYm9vdHN0cmFwJyxcbiAgICAnLS10b29sa2l0LXN0YWNrLW5hbWUnLCBib290c3RyYXBTdGFja05hbWUsXG4gICAgJy0tYm9vdHN0cmFwLWJ1Y2tldC1uYW1lJywgbmV3Qm9vdHN0cmFwQnVja2V0TmFtZSxcbiAgICAnLS1xdWFsaWZpZXInLCBRVUFMSUZJRVJdLCB7XG4gICAgbW9kRW52OiB7XG4gICAgICBDREtfTkVXX0JPT1RTVFJBUDogJzEnLFxuICAgIH0sXG4gIH0pO1xuXG4gIC8vIChGb3JjZSkgZGVwbG95IHN0YWNrIGFnYWluXG4gIC8vIC0tZm9yY2UgdG8gYnlwYXNzIHRoZSBjaGVjayB3aGljaCBzYXlzIHRoYXQgdGhlIHRlbXBsYXRlIGhhc24ndCBjaGFuZ2VkLlxuICBhd2FpdCBjZGtEZXBsb3koJ2xhbWJkYScsIHtcbiAgICBvcHRpb25zOiBbXG4gICAgICAnLS10b29sa2l0LXN0YWNrLW5hbWUnLCBib290c3RyYXBTdGFja05hbWUsXG4gICAgICAnLS1mb3JjZScsXG4gICAgXSxcbiAgfSk7XG59KTtcblxudGVzdCgnZGVwbG95IG5ldyBzdHlsZSBzeW50aGVzaXMgdG8gbmV3IHN0eWxlIGJvb3RzdHJhcCcsIGFzeW5jICgpID0+IHtcbiAgY29uc3QgYm9vdHN0cmFwU3RhY2tOYW1lID0gZnVsbFN0YWNrTmFtZSgnYm9vdHN0cmFwLXN0YWNrJyk7XG5cbiAgYXdhaXQgY2RrKFsnYm9vdHN0cmFwJyxcbiAgICAnLS10b29sa2l0LXN0YWNrLW5hbWUnLCBib290c3RyYXBTdGFja05hbWUsXG4gICAgJy0tcXVhbGlmaWVyJywgUVVBTElGSUVSLFxuICAgICctLWNsb3VkZm9ybWF0aW9uLWV4ZWN1dGlvbi1wb2xpY2llcycsICdhcm46YXdzOmlhbTo6YXdzOnBvbGljeS9BZG1pbmlzdHJhdG9yQWNjZXNzJyxcbiAgXSwge1xuICAgIG1vZEVudjoge1xuICAgICAgQ0RLX05FV19CT09UU1RSQVA6ICcxJyxcbiAgICB9LFxuICB9KTtcblxuICAvLyBEZXBsb3kgc3RhY2sgdGhhdCB1c2VzIGZpbGUgYXNzZXRzXG4gIGF3YWl0IGNka0RlcGxveSgnbGFtYmRhJywge1xuICAgIG9wdGlvbnM6IFtcbiAgICAgICctLXRvb2xraXQtc3RhY2stbmFtZScsIGJvb3RzdHJhcFN0YWNrTmFtZSxcbiAgICAgICctLWNvbnRleHQnLCBgQGF3cy1jZGsvY29yZTpib290c3RyYXBRdWFsaWZpZXI9JHtRVUFMSUZJRVJ9YCxcbiAgICAgICctLWNvbnRleHQnLCAnQGF3cy1jZGsvY29yZTpuZXdTdHlsZVN0YWNrU3ludGhlc2lzPTEnLFxuICAgIF0sXG4gIH0pO1xufSk7XG5cbnRlc3QoJ2RlcGxveSBvbGQgc3R5bGUgc3ludGhlc2lzIHRvIG5ldyBzdHlsZSBib290c3RyYXAnLCBhc3luYyAoKSA9PiB7XG4gIGNvbnN0IGJvb3RzdHJhcFN0YWNrTmFtZSA9IGZ1bGxTdGFja05hbWUoJ2Jvb3RzdHJhcC1zdGFjaycpO1xuXG4gIGF3YWl0IGNkayhbJ2Jvb3RzdHJhcCcsXG4gICAgJy0tdG9vbGtpdC1zdGFjay1uYW1lJywgYm9vdHN0cmFwU3RhY2tOYW1lLFxuICAgICctLXF1YWxpZmllcicsIFFVQUxJRklFUixcbiAgICAnLS1jbG91ZGZvcm1hdGlvbi1leGVjdXRpb24tcG9saWNpZXMnLCAnYXJuOmF3czppYW06OmF3czpwb2xpY3kvQWRtaW5pc3RyYXRvckFjY2VzcycsXG4gIF0sIHtcbiAgICBtb2RFbnY6IHtcbiAgICAgIENES19ORVdfQk9PVFNUUkFQOiAnMScsXG4gICAgfSxcbiAgfSk7XG5cbiAgLy8gRGVwbG95IHN0YWNrIHRoYXQgdXNlcyBmaWxlIGFzc2V0c1xuICBhd2FpdCBjZGtEZXBsb3koJ2xhbWJkYScsIHtcbiAgICBvcHRpb25zOiBbXG4gICAgICAnLS10b29sa2l0LXN0YWNrLW5hbWUnLCBib290c3RyYXBTdGFja05hbWUsXG4gICAgXSxcbiAgfSk7XG59KTtcblxudGVzdCgnZGVwbG95aW5nIG5ldyBzdHlsZSBzeW50aGVzaXMgdG8gb2xkIHN0eWxlIGJvb3RzdHJhcCBmYWlscycsIGFzeW5jICgpID0+IHtcbiAgY29uc3QgYm9vdHN0cmFwU3RhY2tOYW1lID0gZnVsbFN0YWNrTmFtZSgnYm9vdHN0cmFwLXN0YWNrJyk7XG5cbiAgYXdhaXQgY2RrKFsnYm9vdHN0cmFwJywgJy0tdG9vbGtpdC1zdGFjay1uYW1lJywgYm9vdHN0cmFwU3RhY2tOYW1lXSk7XG5cbiAgLy8gRGVwbG95IHN0YWNrIHRoYXQgdXNlcyBmaWxlIGFzc2V0cywgdGhpcyBmYWlscyBiZWNhdXNlIHRoZSBib290c3RyYXAgc3RhY2tcbiAgLy8gaXMgdmVyc2lvbiBjaGVja2VkLlxuICBhd2FpdCBleHBlY3QoY2RrRGVwbG95KCdsYW1iZGEnLCB7XG4gICAgb3B0aW9uczogW1xuICAgICAgJy0tdG9vbGtpdC1zdGFjay1uYW1lJywgYm9vdHN0cmFwU3RhY2tOYW1lLFxuICAgICAgJy0tY29udGV4dCcsICdAYXdzLWNkay9jb3JlOm5ld1N0eWxlU3RhY2tTeW50aGVzaXM9MScsXG4gICAgXSxcbiAgfSkpLnJlamVjdHMudG9UaHJvdygnZXhpdGVkIHdpdGggZXJyb3InKTtcbn0pO1xuXG50ZXN0KCdjYW4gY3JlYXRlIG11bHRpcGxlIGxlZ2FjeSBib290c3RyYXAgc3RhY2tzJywgYXN5bmMgKCkgPT4ge1xuICBjb25zdCBib290c3RyYXBTdGFja05hbWUxID0gZnVsbFN0YWNrTmFtZSgnYm9vdHN0cmFwLXN0YWNrLTEnKTtcbiAgY29uc3QgYm9vdHN0cmFwU3RhY2tOYW1lMiA9IGZ1bGxTdGFja05hbWUoJ2Jvb3RzdHJhcC1zdGFjay0yJyk7XG5cbiAgLy8gZGVwbG95IHR3byB0b29sa2l0IHN0YWNrcyBpbnRvIHRoZSBzYW1lIGVudmlyb25tZW50IChzZWUgIzE0MTYpXG4gIC8vIG9uZSB3aXRoIHRhZ3NcbiAgYXdhaXQgY2RrKFsnYm9vdHN0cmFwJywgJy12JywgJy0tdG9vbGtpdC1zdGFjay1uYW1lJywgYm9vdHN0cmFwU3RhY2tOYW1lMSwgJy0tdGFncycsICdGb289QmFyJ10pO1xuICBhd2FpdCBjZGsoWydib290c3RyYXAnLCAnLXYnLCAnLS10b29sa2l0LXN0YWNrLW5hbWUnLCBib290c3RyYXBTdGFja05hbWUyXSk7XG5cbiAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBjbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7IFN0YWNrTmFtZTogYm9vdHN0cmFwU3RhY2tOYW1lMSB9KTtcbiAgZXhwZWN0KHJlc3BvbnNlLlN0YWNrcz8uWzBdLlRhZ3MpLnRvRXF1YWwoW1xuICAgIHsgS2V5OiAnRm9vJywgVmFsdWU6ICdCYXInIH0sXG4gIF0pO1xufSk7XG5cbmZ1bmN0aW9uIHJhbmRvbVN0cmluZygpIHtcbiAgLy8gQ3JhenlcbiAgcmV0dXJuIE1hdGgucmFuZG9tKCkudG9TdHJpbmcoMzYpLnJlcGxhY2UoL1teYS16MC05XSsvZywgJycpO1xufVxuIl19 diff --git a/packages/aws-cdk/test/integ/cli/test-jest.sh b/packages/aws-cdk/test/integ/cli-regression-patches/v1.44.0/test.sh similarity index 52% rename from packages/aws-cdk/test/integ/cli/test-jest.sh rename to packages/aws-cdk/test/integ/cli-regression-patches/v1.44.0/test.sh index 3367ac0129919..482956df450f4 100755 --- a/packages/aws-cdk/test/integ/cli/test-jest.sh +++ b/packages/aws-cdk/test/integ/cli-regression-patches/v1.44.0/test.sh @@ -1,10 +1,17 @@ #!/bin/bash -# A number of tests have been written in TS/Jest, instead of bash. -# This script runs them. - set -euo pipefail scriptdir=$(cd $(dirname $0) && pwd) +echo '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' +echo 'CLI Integration Tests' +echo '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' + +current_version=$(node -p "require('${scriptdir}/../../../package.json').version") + +# This allows injecting different versions, not just the current one. +# Useful when testing. +export VERSION_UNDER_TEST=${VERSION_UNDER_TEST:-${current_version}} + cd $scriptdir # Install these dependencies that the tests (written in Jest) need. @@ -16,4 +23,4 @@ if ! npx --no-install jest --version; then npm install --prefix . jest aws-sdk fi -npx jest --runInBand --verbose --setupFilesAfterEnv "$PWD/jest.setup.js" "$@" +npx jest --runInBand --verbose "$@" \ No newline at end of file diff --git a/packages/aws-cdk/test/integ/cli.exclusions.js b/packages/aws-cdk/test/integ/cli.exclusions.js deleted file mode 100644 index 4204b62bd527c..0000000000000 --- a/packages/aws-cdk/test/integ/cli.exclusions.js +++ /dev/null @@ -1,70 +0,0 @@ -/** -List of exclusions when running backwards compatibility tests. -Add when you need to exclude a specific integration test from a specific version. - -This is an escape hatch for the rare cases where we need to introduce -a change that breaks existing integration tests. (e.g security) - -For example: - -{ - "test": "test-cdk-iam-diff.sh", - "version": "v1.30.0", - "justification": "iam policy generation has changed in version > 1.30.0 because..." -}, - -*/ -const exclusions = [ - { - "test": "test-cdk-deploy-nested-stack-with-parameters.sh", - "version": "v1.37.0", - "justification": "This test doesn't use a unique sns topic name for the topic in the nested stack and it collides with our regular integ suite" - }, - { - "test": "test-cdk-deploy-wildcard-with-outputs.sh", - "version": "v1.37.0", - "justification": "This test doesn't use a unique sns topic name and it collides with our regular integ suite" - }, - { - "test": "test-cdk-deploy-with-outputs.sh", - "version": "v1.37.0", - "justification": "This test doesn't use a unique sns topic name and it collides with our regular integ suite" - } -] - -function getExclusion(test, version) { - - const filtered = exclusions.filter(e => { - return e.test === test && e.version === version; - }); - - if (filtered.length === 0) { - return undefined; - } - - if (filtered.length === 1) { - return filtered[0]; - } - - throw new Error(`Multiple exclusions found for (${test, version}): ${filtered.length}`); - -} - -module.exports.shouldSkip = function (test, version) { - - const exclusion = getExclusion(test, version); - - return exclusion != undefined - -} - -module.exports.getJustification = function (test, version) { - - const exclusion = getExclusion(test, version); - - if (!exclusion) { - throw new Error(`Exclusion not found for (${test}, ${version})`); - } - - return exclusion.justification; -} diff --git a/packages/aws-cdk/test/integ/cli/README.md b/packages/aws-cdk/test/integ/cli/README.md index 44d531623e112..9e0e8d9b5e5f1 100644 --- a/packages/aws-cdk/test/integ/cli/README.md +++ b/packages/aws-cdk/test/integ/cli/README.md @@ -20,9 +20,6 @@ Running against a failing dist build: ## Adding tests -Older tests were written in bash; new tests should be written in -TypeScript/Jest, that is much more comfortable to write in. - Even though tests are now written in TypeScript, this does not conceptually change their SUT! They are still testing the CLI via running it as a subprocess, they are NOT reaching directly into the CLI @@ -34,8 +31,8 @@ Compilation of the tests is done as part of the normal package build, at which point it is using the dependencies brought in by the containing `aws-cdk` package's `package.json`. -When run in a non-develompent repo (as done during integ tests or canary runs), -the required dependencies are brought in just-in-time via `test-jest.sh`. Any +When run in a non-development repo (as done during integ tests or canary runs), +the required dependencies are brought in just-in-time via `test.sh`. Any new dependencies added for the tests should be added there as well. But, better yet, don't add any dependencies at all. You shouldn't need to, these tests are simple. diff --git a/packages/aws-cdk/test/integ/cli/app/app.js b/packages/aws-cdk/test/integ/cli/app/app.js index efd6acde64145..a61bc4b798b32 100644 --- a/packages/aws-cdk/test/integ/cli/app/app.js +++ b/packages/aws-cdk/test/integ/cli/app/app.js @@ -255,7 +255,7 @@ new MultiParameterStack(app, `${stackPrefix}-param-test-3`); new OutputsStack(app, `${stackPrefix}-outputs-test-1`); new AnotherOutputsStack(app, `${stackPrefix}-outputs-test-2`); // Not included in wildcard -new IamStack(app, `${stackPrefix}-iam-test`); +new IamStack(app, `${stackPrefix}-iam-test`, { env: defaultEnv }); const providing = new ProvidingStack(app, `${stackPrefix}-order-providing`); new ConsumingStack(app, `${stackPrefix}-order-consuming`, { providingStack: providing }); diff --git a/packages/aws-cdk/test/integ/cli/aws-helpers.ts b/packages/aws-cdk/test/integ/cli/aws-helpers.ts index 92cb7a77131a3..fb54db4f60bcd 100644 --- a/packages/aws-cdk/test/integ/cli/aws-helpers.ts +++ b/packages/aws-cdk/test/integ/cli/aws-helpers.ts @@ -20,6 +20,7 @@ export let testEnv = async (): Promise => { export const cloudFormation = makeAwsCaller(AWS.CloudFormation); export const s3 = makeAwsCaller(AWS.S3); +export const ecr = makeAwsCaller(AWS.ECR); export const sns = makeAwsCaller(AWS.SNS); export const iam = makeAwsCaller(AWS.IAM); export const lambda = makeAwsCaller(AWS.Lambda); @@ -188,6 +189,10 @@ export async function emptyBucket(bucketName: string) { }); } +export async function deleteImageRepository(repositoryName: string) { + await ecr('deleteRepository', { repositoryName, force: true }); +} + export async function deleteBucket(bucketName: string) { try { await emptyBucket(bucketName); diff --git a/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts b/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts index 3c674470abe4c..93f9a0974aa2a 100644 --- a/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts +++ b/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts @@ -1,5 +1,6 @@ import { cloudFormation } from './aws-helpers'; import { cdk, cdkDeploy, cleanup, fullStackName, prepareAppFixture, rememberToDeleteBucket } from './cdk-helpers'; +import { integTest } from './test-helpers'; jest.setTimeout(600_000); @@ -17,7 +18,7 @@ afterEach(async () => { await cleanup(); }); -test('can bootstrap without execution', async () => { +integTest('can bootstrap without execution', async () => { const bootstrapStackName = fullStackName('bootstrap-stack'); await cdk(['bootstrap', @@ -31,7 +32,7 @@ test('can bootstrap without execution', async () => { expect(resp.Stacks?.[0].StackStatus).toEqual('REVIEW_IN_PROGRESS'); }); -test('upgrade legacy bootstrap stack to new bootstrap stack while in use', async () => { +integTest('upgrade legacy bootstrap stack to new bootstrap stack while in use', async () => { const bootstrapStackName = fullStackName('bootstrap-stack'); const legacyBootstrapBucketName = `aws-cdk-bootstrap-integ-test-legacy-bckt-${randomString()}`; @@ -69,7 +70,7 @@ test('upgrade legacy bootstrap stack to new bootstrap stack while in use', async }); }); -test('deploy new style synthesis to new style bootstrap', async () => { +integTest('deploy new style synthesis to new style bootstrap', async () => { const bootstrapStackName = fullStackName('bootstrap-stack'); await cdk(['bootstrap', @@ -92,7 +93,30 @@ test('deploy new style synthesis to new style bootstrap', async () => { }); }); -test('deploy old style synthesis to new style bootstrap', async () => { +integTest('deploy new style synthesis to new style bootstrap (with docker image)', async () => { + const bootstrapStackName = fullStackName('bootstrap-stack'); + + await cdk(['bootstrap', + '--toolkit-stack-name', bootstrapStackName, + '--qualifier', QUALIFIER, + '--cloudformation-execution-policies', 'arn:aws:iam::aws:policy/AdministratorAccess', + ], { + modEnv: { + CDK_NEW_BOOTSTRAP: '1', + }, + }); + + // Deploy stack that uses file assets + await cdkDeploy('docker', { + options: [ + '--toolkit-stack-name', bootstrapStackName, + '--context', `@aws-cdk/core:bootstrapQualifier=${QUALIFIER}`, + '--context', '@aws-cdk/core:newStyleStackSynthesis=1', + ], + }); +}); + +integTest('deploy old style synthesis to new style bootstrap', async () => { const bootstrapStackName = fullStackName('bootstrap-stack'); await cdk(['bootstrap', @@ -113,7 +137,7 @@ test('deploy old style synthesis to new style bootstrap', async () => { }); }); -test('deploying new style synthesis to old style bootstrap fails', async () => { +integTest('deploying new style synthesis to old style bootstrap fails', async () => { const bootstrapStackName = fullStackName('bootstrap-stack'); await cdk(['bootstrap', '--toolkit-stack-name', bootstrapStackName]); @@ -128,7 +152,7 @@ test('deploying new style synthesis to old style bootstrap fails', async () => { })).rejects.toThrow('exited with error'); }); -test('can create multiple legacy bootstrap stacks', async () => { +integTest('can create multiple legacy bootstrap stacks', async () => { const bootstrapStackName1 = fullStackName('bootstrap-stack-1'); const bootstrapStackName2 = fullStackName('bootstrap-stack-2'); diff --git a/packages/aws-cdk/test/integ/cli/cdk-helpers.ts b/packages/aws-cdk/test/integ/cli/cdk-helpers.ts index d43e7bfe23000..410b8d71d9e71 100644 --- a/packages/aws-cdk/test/integ/cli/cdk-helpers.ts +++ b/packages/aws-cdk/test/integ/cli/cdk-helpers.ts @@ -1,7 +1,7 @@ import * as child_process from 'child_process'; import * as os from 'os'; import * as path from 'path'; -import { cloudFormation, deleteBucket, deleteStacks, emptyBucket, outputFromStack, testEnv } from './aws-helpers'; +import { cloudFormation, deleteBucket, deleteImageRepository, deleteStacks, emptyBucket, outputFromStack, testEnv } from './aws-helpers'; export const INTEG_TEST_DIR = path.join(os.tmpdir(), 'cdk-integ-test2'); @@ -155,6 +155,10 @@ export async function cleanup(): Promise { const bucketNames = stacksToDelete.map(stack => outputFromStack('BucketName', stack)).filter(defined); await Promise.all(bucketNames.map(emptyBucket)); + // Bootstrap stacks have ECR repositories with images which should be deleted + const imageRepositoryNames = stacksToDelete.map(stack => outputFromStack('ImageRepositoryName', stack)).filter(defined); + await Promise.all(imageRepositoryNames.map(deleteImageRepository)); + await deleteStacks(...stacksToDelete.map(s => s.StackName)); // We might have leaked some buckets by upgrading the bootstrap stack. Be @@ -209,7 +213,7 @@ export async function shell(command: string[], options: ShellOptions = {}): Prom if (code === 0 || options.allowErrExit) { resolve((Buffer.concat(stdout).toString('utf-8') + Buffer.concat(stderr).toString('utf-8')).trim()); } else { - reject(new Error(`'${command.join(' ')}' exited with error code ${code}`)); + reject(new Error(`'${command.join(' ')}' exited with error code ${code}: ${Buffer.concat(stderr).toString('utf-8').trim()}`)); } }); }); diff --git a/packages/aws-cdk/test/integ/cli/cli.integtest.ts b/packages/aws-cdk/test/integ/cli/cli.integtest.ts index 47a7f9fe46ce3..4c9896236155a 100644 --- a/packages/aws-cdk/test/integ/cli/cli.integtest.ts +++ b/packages/aws-cdk/test/integ/cli/cli.integtest.ts @@ -4,6 +4,7 @@ import * as path from 'path'; import { cloudFormation, iam, lambda, retry, sleep, sns, sts, testEnv } from './aws-helpers'; import { cdk, cdkDeploy, cdkDestroy, cleanup, cloneDirectory, fullStackName, INTEG_TEST_DIR, log, prepareAppFixture, shell, STACK_NAME_PREFIX } from './cdk-helpers'; +import { integTest } from './test-helpers'; jest.setTimeout(600 * 1000); @@ -19,7 +20,7 @@ afterEach(async () => { await cleanup(); }); -test('VPC Lookup', async () => { +integTest('VPC Lookup', async () => { log('Making sure we are clean before starting.'); await cdkDestroy('define-vpc', { modEnv: { ENABLE_VPC_TESTING: 'DEFINE' }}); @@ -31,14 +32,14 @@ test('VPC Lookup', async () => { await cdkDeploy('import-vpc', { modEnv: { ENABLE_VPC_TESTING: 'IMPORT' }}); }); -test('Two ways of shoing the version', async () => { +integTest('Two ways of shoing the version', async () => { const version1 = await cdk(['version']); const version2 = await cdk(['--version']); expect(version1).toEqual(version2); }); -test('Termination protection', async () => { +integTest('Termination protection', async () => { const stackName = 'termination-protection'; await cdkDeploy(stackName); @@ -50,7 +51,7 @@ test('Termination protection', async () => { await cdkDestroy(stackName); }); -test('cdk synth', async () => { +integTest('cdk synth', async () => { await expect(cdk(['synth', fullStackName('test-1')])).resolves.toEqual( `Resources: topic69831491: @@ -70,7 +71,7 @@ test('cdk synth', async () => { aws:cdk:path: ${STACK_NAME_PREFIX}-test-2/topic2/Resource`); }); -test('ssm parameter provider error', async () => { +integTest('ssm parameter provider error', async () => { await expect(cdk(['synth', fullStackName('missing-ssm-parameter'), '-c', 'test:ssm-parameter-name=/does/not/exist', @@ -79,7 +80,7 @@ test('ssm parameter provider error', async () => { })).resolves.toContain('SSM parameter not available in account'); }); -test('automatic ordering', async () => { +integTest('automatic ordering', async () => { // Deploy the consuming stack which will include the producing stack await cdkDeploy('order-consuming'); @@ -87,7 +88,7 @@ test('automatic ordering', async () => { await cdkDestroy('order-providing'); }); -test('context setting', async () => { +integTest('context setting', async () => { await fs.writeFile(path.join(INTEG_TEST_DIR, 'cdk.context.json'), JSON.stringify({ contextkey: 'this is the context value', })); @@ -106,7 +107,7 @@ test('context setting', async () => { } }); -test('deploy', async () => { +integTest('deploy', async () => { const stackArn = await cdkDeploy('test-2', { captureStderr: false }); // verify the number of resources in the stack @@ -116,14 +117,14 @@ test('deploy', async () => { expect(response.StackResources?.length).toEqual(2); }); -test('deploy all', async () => { +integTest('deploy all', async () => { const arns = await cdkDeploy('test-*', { captureStderr: false }); // verify that we only deployed a single stack (there's a single ARN in the output) expect(arns.split('\n').length).toEqual(2); }); -test('nested stack with parameters', async () => { +integTest('nested stack with parameters', async () => { // STACK_NAME_PREFIX is used in MyTopicParam to allow multiple instances // of this test to run in parallel, othewise they will attempt to create the same SNS topic. const stackArn = await cdkDeploy('with-nested-stack-using-parameters', { @@ -141,7 +142,7 @@ test('nested stack with parameters', async () => { expect(response.StackResources?.length).toEqual(1); }); -test('deploy without execute', async () => { +integTest('deploy without execute', async () => { const stackArn = await cdkDeploy('test-2', { options: ['--no-execute'], captureStderr: false, @@ -156,7 +157,7 @@ test('deploy without execute', async () => { expect(response.Stacks?.[0].StackStatus).toEqual('REVIEW_IN_PROGRESS'); }); -test('security related changes without a CLI are expected to fail', async () => { +integTest('security related changes without a CLI are expected to fail', async () => { // redirect /dev/null to stdin, which means there will not be tty attached // since this stack includes security-related changes, the deployment should // immediately fail because we can't confirm the changes @@ -172,7 +173,7 @@ test('security related changes without a CLI are expected to fail', async () => })).rejects.toThrow('does not exist'); }); -test('deploy wildcard with outputs', async () => { +integTest('deploy wildcard with outputs', async () => { const outputsFile = path.join(INTEG_TEST_DIR, 'outputs', 'outputs.json'); await fs.mkdir(path.dirname(outputsFile), { recursive: true }); @@ -191,7 +192,7 @@ test('deploy wildcard with outputs', async () => { }); }); -test('deploy with parameters', async () => { +integTest('deploy with parameters', async () => { const stackArn = await cdkDeploy('param-test-1', { options: [ '--parameters', `TopicNameParam=${STACK_NAME_PREFIX}bazinga`, @@ -211,7 +212,7 @@ test('deploy with parameters', async () => { ]); }); -test('deploy with wildcard and parameters', async () => { +integTest('deploy with wildcard and parameters', async () => { await cdkDeploy('param-test-*', { options: [ '--parameters', `${STACK_NAME_PREFIX}-param-test-1:TopicNameParam=${STACK_NAME_PREFIX}bazinga`, @@ -222,7 +223,7 @@ test('deploy with wildcard and parameters', async () => { }); }); -test('deploy with parameters multi', async () => { +integTest('deploy with parameters multi', async () => { const paramVal1 = `${STACK_NAME_PREFIX}bazinga`; const paramVal2 = `${STACK_NAME_PREFIX}=jagshemash`; @@ -250,7 +251,7 @@ test('deploy with parameters multi', async () => { ]); }); -test('deploy with notification ARN', async () => { +integTest('deploy with notification ARN', async () => { const topicName = `${STACK_NAME_PREFIX}-test-topic`; const response = await sns('createTopic', { Name: topicName }); @@ -272,7 +273,7 @@ test('deploy with notification ARN', async () => { } }); -test('deploy with role', async () => { +integTest('deploy with role', async () => { const roleName = `${STACK_NAME_PREFIX}-test-role`; await deleteRole(); @@ -350,7 +351,7 @@ test('deploy with role', async () => { } }); -test('cdk diff', async () => { +integTest('cdk diff', async () => { const diff1 = await cdk(['diff', fullStackName('test-1')]); expect(diff1).toContain('AWS::SNS::Topic'); @@ -362,11 +363,11 @@ test('cdk diff', async () => { .rejects.toThrow('exited with error'); }); -test('deploy stack with docker asset', async () => { +integTest('deploy stack with docker asset', async () => { await cdkDeploy('docker'); }); -test('deploy and test stack with lambda asset', async () => { +integTest('deploy and test stack with lambda asset', async () => { const stackArn = await cdkDeploy('lambda', { captureStderr: false }); const response = await cloudFormation('describeStacks', { @@ -384,7 +385,7 @@ test('deploy and test stack with lambda asset', async () => { expect(JSON.stringify(output.Payload)).toContain('dear asset'); }); -test('cdk ls', async () => { +integTest('cdk ls', async () => { const listing = await cdk(['ls'], { captureStderr: false }); const expectedStacks = [ @@ -414,7 +415,7 @@ test('cdk ls', async () => { } }); -test('deploy stack without resource', async () => { +integTest('deploy stack without resource', async () => { // Deploy the stack without resources await cdkDeploy('conditional-resource', { modEnv: { NO_RESOURCE: 'TRUE' }}); @@ -432,7 +433,7 @@ test('deploy stack without resource', async () => { .rejects.toThrow('conditional-resource does not exist'); }); -test('IAM diff', async () => { +integTest('IAM diff', async () => { const output = await cdk(['diff', fullStackName('iam-test')]); // Roughly check for a table like this: @@ -448,7 +449,7 @@ test('IAM diff', async () => { expect(output).toContain('ec2.${AWS::URLSuffix}'); }); -test('fast deploy', async () => { +integTest('fast deploy', async () => { // we are using a stack with a nested stack because CFN will always attempt to // update a nested stack, which will allow us to verify that updates are actually // skipped unless --force is specified. @@ -479,12 +480,12 @@ test('fast deploy', async () => { } }); -test('failed deploy does not hang', async () => { +integTest('failed deploy does not hang', async () => { // this will hang if we introduce https://github.com/aws/aws-cdk/issues/6403 again. await expect(cdkDeploy('failed')).rejects.toThrow('exited with error'); }); -test('can still load old assemblies', async () => { +integTest('can still load old assemblies', async () => { const cxAsmDir = path.join(os.tmpdir(), 'cdk-integ-cx'); const testAssembliesDirectory = path.join(__dirname, 'cloud-assemblies'); @@ -519,7 +520,7 @@ test('can still load old assemblies', async () => { } }); -test('generating and loading assembly', async () => { +integTest('generating and loading assembly', async () => { const asmOutputDir = path.join(os.tmpdir(), 'cdk-integ-asm'); await shell(['rm', '-rf', asmOutputDir]); diff --git a/packages/aws-cdk/test/integ/cli/jest.setup.js b/packages/aws-cdk/test/integ/cli/jest.setup.js deleted file mode 100644 index 752a75d4f73d4..0000000000000 --- a/packages/aws-cdk/test/integ/cli/jest.setup.js +++ /dev/null @@ -1,8 +0,0 @@ -// Print a big banner before every test, much more readable output -jasmine.getEnv().addReporter({ - specStarted: currentTest => { - process.stdout.write('================================================================\n'); - process.stdout.write(`${currentTest.fullName}\n`); - process.stdout.write('================================================================\n'); - } -}); \ No newline at end of file diff --git a/packages/aws-cdk/test/integ/cli/skip-tests.txt b/packages/aws-cdk/test/integ/cli/skip-tests.txt new file mode 100644 index 0000000000000..bb43b8f55b68f --- /dev/null +++ b/packages/aws-cdk/test/integ/cli/skip-tests.txt @@ -0,0 +1,8 @@ +# This file is empty on purpose. Leave it here as documentation +# and an example. +# +# Copy this file to cli-regression-patches/vX.Y.Z/skip-tests.txt +# and edit it there if you want to exclude certain tests from running +# when performing a certain version's regression tests. +# +# Put a test name on a line by itself to skip it. \ No newline at end of file diff --git a/packages/aws-cdk/test/integ/cli/test-helpers.ts b/packages/aws-cdk/test/integ/cli/test-helpers.ts new file mode 100644 index 0000000000000..1aef74a6efd28 --- /dev/null +++ b/packages/aws-cdk/test/integ/cli/test-helpers.ts @@ -0,0 +1,23 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +const SKIP_TESTS = fs.readFileSync(path.join(__dirname, 'skip-tests.txt'), { encoding: 'utf-8' }).split('\n'); + +/** + * A wrapper for jest's 'test' which takes regression-disabled tests into account and prints a banner + */ +export function integTest(name: string, callback: () => A | Promise) { + const runner = shouldSkip(name) ? test.skip : test; + + runner(name, () => { + process.stdout.write('================================================================\n'); + process.stdout.write(`${name}\n`); + process.stdout.write('================================================================\n'); + + return callback(); + }); +} + +function shouldSkip(testName: string) { + return SKIP_TESTS.includes(testName); +} \ No newline at end of file diff --git a/packages/aws-cdk/test/integ/cli/test.sh b/packages/aws-cdk/test/integ/cli/test.sh index 75f98aefb9380..482956df450f4 100755 --- a/packages/aws-cdk/test/integ/cli/test.sh +++ b/packages/aws-cdk/test/integ/cli/test.sh @@ -10,42 +10,17 @@ current_version=$(node -p "require('${scriptdir}/../../../package.json').version # This allows injecting different versions, not just the current one. # Useful when testing. -VERSION_UNDER_TEST=${VERSION_UNDER_TEST:-${current_version}} +export VERSION_UNDER_TEST=${VERSION_UNDER_TEST:-${current_version}} -# check if a specific test should be skiped -# from execution in the current version. -function should_skip { - test=$1 - echo $(node -p "require('${scriptdir}/../cli.exclusions.js').shouldSkip('${test}', '${VERSION_UNDER_TEST}')") -} +cd $scriptdir -# get the justification for why a test is skipped. -# this will fail if there is no justification! -function get_skip_jusitification { - test=$1 - echo $(node -p "require('${scriptdir}/../cli.exclusions.js').getJustification('${test}', '${VERSION_UNDER_TEST}')") -} - -for test in $(cd ${scriptdir} && ls test-*.sh); do - echo "============================================================================================" - - # first check this if this test should be skipped. - # this can happen when running in regression mode - # when we introduce an intentional breaking change. - skip=$(should_skip ${test}) - - if [ ${skip} == "true" ]; then - - # make sure we have a justification, this will fail if not. - jusitification="$(get_skip_jusitification ${test})" - - # skip this specific test. - echo "${test} - skipped (${jusitification})" - continue - fi - - echo "${test}" - echo "============================================================================================" - /bin/bash ${scriptdir}/${test} -done +# Install these dependencies that the tests (written in Jest) need. +# Only if we're not running from the repo, because if we are the +# dependencies have already been installed by the containing 'aws-cdk' package's +# package.json. +if ! npx --no-install jest --version; then + echo 'Looks like we need to install jest first. Hold on.' >& 2 + npm install --prefix . jest aws-sdk +fi +npx jest --runInBand --verbose "$@" \ No newline at end of file diff --git a/packages/aws-cdk/test/integ/test-cli-regression-against-current-code.sh b/packages/aws-cdk/test/integ/test-cli-regression-against-current-code.sh index a9a68d19e6001..02219e64c73f4 100755 --- a/packages/aws-cdk/test/integ/test-cli-regression-against-current-code.sh +++ b/packages/aws-cdk/test/integ/test-cli-regression-against-current-code.sh @@ -70,15 +70,19 @@ download_repo ${VERSION_UNDER_TEST} # bad behvaior when using it as directory names. sanitized_version=$(sed 's/\//-/g' <<< "${VERSION_UNDER_TEST}") +# Test must be created in the same directory here because the script files liberally +# include files from '..' and they have to exist. integ_under_test=${integdir}/cli-backwards-tests-${sanitized_version} rm -rf ${integ_under_test} echo "Copying integration tests of version ${VERSION_UNDER_TEST} to ${integ_under_test} (dont worry, its gitignored)" cp -r ${temp_dir}/package/test/integ/cli ${integ_under_test} -echo "Hotpatching the test runner (can be removed after release 1.40.0)" >&2 -cp -r ${integdir}/cli/test-jest.sh ${integ_under_test} -cp -r ${integdir}/cli/jest.config.js ${integ_under_test} -cp -r ${integdir}/cli/jest.setup.js ${integ_under_test} +patch_dir="${integdir}/cli-regression-patches/${VERSION_UNDER_TEST}" +if [[ -d "$patch_dir" ]]; then + echo "Hotpatching the tests with files from $patch_dir" >&2 + cp -r "$patch_dir"/* ${integ_under_test} +fi echo "Running integration tests of version ${VERSION_UNDER_TEST} from ${integ_under_test}" -VERSION_UNDER_TEST=${VERSION_UNDER_TEST} ${integ_under_test}/test.sh +set -x +VERSION_UNDER_TEST=${VERSION_UNDER_TEST} ${integ_under_test}/test.sh "$@" diff --git a/packages/aws-cdk/test/integ/test-cli-regression-against-latest-release.sh b/packages/aws-cdk/test/integ/test-cli-regression-against-latest-release.sh index fc3e4e3b9a859..6d0133ca06108 100755 --- a/packages/aws-cdk/test/integ/test-cli-regression-against-latest-release.sh +++ b/packages/aws-cdk/test/integ/test-cli-regression-against-latest-release.sh @@ -5,4 +5,4 @@ integdir=$(cd $(dirname $0) && pwd) # run the regular regression test but pass the env variable that will # eventually instruct our runners and wrappers to install the framework # from npmjs.org rather then using the local code. -USE_PUBLISHED_FRAMEWORK_VERSION=True ${integdir}/test-cli-regression-against-current-code.sh +USE_PUBLISHED_FRAMEWORK_VERSION=True ${integdir}/test-cli-regression-against-current-code.sh "$@" diff --git a/packages/cdk-assets/lib/private/shell.ts b/packages/cdk-assets/lib/private/shell.ts index fd145cf517704..1ae57dba1b062 100644 --- a/packages/cdk-assets/lib/private/shell.ts +++ b/packages/cdk-assets/lib/private/shell.ts @@ -30,6 +30,7 @@ export async function shell(command: string[], options: ShellOptions = {}): Prom } const stdout = new Array(); + const stderr = new Array(); // Both write to stdout and collect child.stdout.on('data', chunk => { @@ -43,6 +44,8 @@ export async function shell(command: string[], options: ShellOptions = {}): Prom if (!options.quiet) { process.stderr.write(chunk); } + + stderr.push(chunk); }); child.once('error', reject); @@ -51,7 +54,8 @@ export async function shell(command: string[], options: ShellOptions = {}): Prom if (code === 0) { resolve(Buffer.concat(stdout).toString('utf-8')); } else { - reject(new ProcessFailed(code, `${renderCommandLine(command)} exited with error code ${code}`)); + const out = Buffer.concat(stderr).toString('utf-8').trim(); + reject(new ProcessFailed(code, `${renderCommandLine(command)} exited with error code ${code}: ${out}`)); } }); }); From 681b3bbc7de517c06ac0bd848b73cc6d7267dfa1 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Sun, 7 Jun 2020 09:13:11 +0100 Subject: [PATCH 12/32] fix(cognito): error when using parameter for `domainPrefix` (#8399) Adds recognition of tokens for all validations that validate the content in some form. fixes #8314 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-cognito/lib/user-pool-attr.ts | 6 ++++-- .../aws-cognito/lib/user-pool-domain.ts | 7 +++++-- .../@aws-cdk/aws-cognito/lib/user-pool.ts | 8 +++---- .../aws-cognito/test/user-pool-attr.test.ts | 13 ++++++++++++ .../aws-cognito/test/user-pool-domain.test.ts | 13 +++++++++++- .../aws-cognito/test/user-pool.test.ts | 21 ++++++++++++++++++- 6 files changed, 58 insertions(+), 10 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts index e2a76c64120ef..c6fde417d1e4e 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts @@ -1,3 +1,5 @@ +import { Token } from '@aws-cdk/core'; + /** * The set of standard attributes that can be marked as required. * @@ -200,10 +202,10 @@ export class StringAttribute implements ICustomAttribute { private readonly mutable?: boolean; constructor(props: StringAttributeProps = {}) { - if (props.minLen && props.minLen < 0) { + if (props.minLen && !Token.isUnresolved(props.minLen) && props.minLen < 0) { throw new Error(`minLen cannot be less than 0 (value: ${props.minLen}).`); } - if (props.maxLen && props.maxLen > 2048) { + if (props.maxLen && !Token.isUnresolved(props.maxLen) && props.maxLen > 2048) { throw new Error(`maxLen cannot be greater than 2048 (value: ${props.maxLen}).`); } this.minLen = props?.minLen; diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts index e829cd2c03713..3566acf7c7aee 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts @@ -1,5 +1,5 @@ import { ICertificate } from '@aws-cdk/aws-certificatemanager'; -import { Construct, IResource, Resource, Stack } from '@aws-cdk/core'; +import { Construct, IResource, Resource, Stack, Token } from '@aws-cdk/core'; import { AwsCustomResource, AwsCustomResourcePolicy, AwsSdkCall, PhysicalResourceId } from '@aws-cdk/custom-resources'; import { CfnUserPoolDomain } from './cognito.generated'; import { IUserPool } from './user-pool'; @@ -90,7 +90,10 @@ export class UserPoolDomain extends Resource implements IUserPoolDomain { throw new Error('One of, and only one of, cognitoDomain or customDomain must be specified'); } - if (props.cognitoDomain?.domainPrefix && !/^[a-z0-9-]+$/.test(props.cognitoDomain.domainPrefix)) { + if (props.cognitoDomain?.domainPrefix && + !Token.isUnresolved(props.cognitoDomain?.domainPrefix) && + !/^[a-z0-9-]+$/.test(props.cognitoDomain.domainPrefix)) { + throw new Error('domainPrefix for cognitoDomain can contain only lowercase alphabets, numbers and hyphens'); } diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index 23af0723870ca..a0f25c13cd58b 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -1,6 +1,6 @@ import { IRole, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; -import { Construct, Duration, IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; +import { Construct, Duration, IResource, Lazy, Resource, Stack, Token } from '@aws-cdk/core'; import { CfnUserPool } from './cognito.generated'; import { ICustomAttribute, RequiredAttributes } from './user-pool-attr'; import { UserPoolClient, UserPoolClientOptions } from './user-pool-client'; @@ -722,10 +722,10 @@ export class UserPool extends UserPoolBase { if (emailStyle === VerificationEmailStyle.CODE) { const emailMessage = props.userVerification?.emailBody ?? `The verification code to your new account is ${CODE_TEMPLATE}`; - if (emailMessage.indexOf(CODE_TEMPLATE) < 0) { + if (!Token.isUnresolved(emailMessage) && emailMessage.indexOf(CODE_TEMPLATE) < 0) { throw new Error(`Verification email body must contain the template string '${CODE_TEMPLATE}'`); } - if (smsMessage.indexOf(CODE_TEMPLATE) < 0) { + if (!Token.isUnresolved(smsMessage) && smsMessage.indexOf(CODE_TEMPLATE) < 0) { throw new Error(`SMS message must contain the template string '${CODE_TEMPLATE}'`); } return { @@ -737,7 +737,7 @@ export class UserPool extends UserPoolBase { } else { const emailMessage = props.userVerification?.emailBody ?? `Verify your account by clicking on ${VERIFY_EMAIL_TEMPLATE}`; - if (emailMessage.indexOf(VERIFY_EMAIL_TEMPLATE) < 0) { + if (!Token.isUnresolved(emailMessage) && emailMessage.indexOf(VERIFY_EMAIL_TEMPLATE) < 0) { throw new Error(`Verification email body must contain the template string '${VERIFY_EMAIL_TEMPLATE}'`); } return { diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts index f001712a802a7..212f6835cb508 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts @@ -1,4 +1,5 @@ import '@aws-cdk/assert/jest'; +import { CfnParameter, Stack } from '@aws-cdk/core'; import { BooleanAttribute, CustomAttributeConfig, DateTimeAttribute, ICustomAttribute, NumberAttribute, StringAttribute } from '../lib'; describe('User Pool Attributes', () => { @@ -104,6 +105,18 @@ describe('User Pool Attributes', () => { expect(() => new StringAttribute({ maxLen: 5000 })) .toThrow(/maxLen cannot be greater than/); }); + + test('validation is skipped when minLen or maxLen are tokens', () => { + const stack = new Stack(); + const parameter = new CfnParameter(stack, 'Parameter', { + type: 'Number', + }); + + expect(() => new StringAttribute({ minLen: parameter.valueAsNumber })) + .not.toThrow(); + expect(() => new StringAttribute({ maxLen: parameter.valueAsNumber })) + .not.toThrow(); + }); }); describe('NumberAttribute', () => { diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-domain.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-domain.test.ts index b2a9c2bb326ad..41407985c8ed1 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-domain.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-domain.test.ts @@ -1,6 +1,6 @@ import '@aws-cdk/assert/jest'; import { Certificate } from '@aws-cdk/aws-certificatemanager'; -import { Stack } from '@aws-cdk/core'; +import { CfnParameter, Stack } from '@aws-cdk/core'; import { UserPool, UserPoolDomain } from '../lib'; describe('User Pool Client', () => { @@ -92,6 +92,17 @@ describe('User Pool Client', () => { })).toThrow(/lowercase alphabets, numbers and hyphens/); }); + test('does not fail when domainPrefix is a token', () => { + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + const parameter = new CfnParameter(stack, 'Paraeter'); + + expect(() => pool.addDomain('Domain', { + cognitoDomain: { domainPrefix: parameter.valueAsString }, + })).not.toThrow(); + }); + test('custom resource is added when cloudFrontDistribution method is called', () => { // GIVEN const stack = new Stack(); diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts index 7472086d57fab..9fad806f888ad 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts @@ -2,7 +2,7 @@ import '@aws-cdk/assert/jest'; import { ABSENT } from '@aws-cdk/assert/lib/assertions/have-resource'; import { Role } from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; -import { Construct, Duration, Stack, Tag } from '@aws-cdk/core'; +import { CfnParameter, Construct, Duration, Stack, Tag } from '@aws-cdk/core'; import { Mfa, NumberAttribute, StringAttribute, UserPool, UserPoolIdentityProvider, UserPoolOperation, VerificationEmailStyle } from '../lib'; describe('User Pool', () => { @@ -161,6 +161,25 @@ describe('User Pool', () => { })).not.toThrow(); }); + test('validation is skipped for email and sms messages when tokens', () => { + const stack = new Stack(); + const parameter = new CfnParameter(stack, 'Parameter'); + + expect(() => new UserPool(stack, 'Pool1', { + userVerification: { + emailStyle: VerificationEmailStyle.CODE, + emailBody: parameter.valueAsString, + }, + })).not.toThrow(); + + expect(() => new UserPool(stack, 'Pool2', { + userVerification: { + emailStyle: VerificationEmailStyle.CODE, + smsMessage: parameter.valueAsString, + }, + })).not.toThrow(); + }); + test('user invitation messages are configured correctly', () => { // GIVEN const stack = new Stack(); From 6407535863c06d6d3ccfc2c3f2b59470d2d88993 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Sun, 7 Jun 2020 21:00:57 +0300 Subject: [PATCH 13/32] chore(cli): fix "iam diff" integration test (#8421) The PR #8403 changed the "IAM stack" to use the default environment and forgot to update the expected output (which now does not contain a token for the URL suffix). --- packages/aws-cdk/test/integ/cli/cli.integtest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk/test/integ/cli/cli.integtest.ts b/packages/aws-cdk/test/integ/cli/cli.integtest.ts index 4c9896236155a..b31bbf314d60a 100644 --- a/packages/aws-cdk/test/integ/cli/cli.integtest.ts +++ b/packages/aws-cdk/test/integ/cli/cli.integtest.ts @@ -441,12 +441,12 @@ integTest('IAM diff', async () => { // ┌───┬─────────────────┬────────┬────────────────┬────────────────────────────-──┬───────────┐ // │ │ Resource │ Effect │ Action │ Principal │ Condition │ // ├───┼─────────────────┼────────┼────────────────┼───────────────────────────────┼───────────┤ - // │ + │ ${SomeRole.Arn} │ Allow │ sts:AssumeRole │ Service:ec2.${AWS::URLSuffix} │ │ + // │ + │ ${SomeRole.Arn} │ Allow │ sts:AssumeRole │ Service:ec2.amazonaws.com │ │ // └───┴─────────────────┴────────┴────────────────┴───────────────────────────────┴───────────┘ expect(output).toContain('${SomeRole.Arn}'); expect(output).toContain('sts:AssumeRole'); - expect(output).toContain('ec2.${AWS::URLSuffix}'); + expect(output).toContain('ec2.amazonaws.com'); }); integTest('fast deploy', async () => { From f5ebacde38ae73b2fb404bcc6b4d3eb9012b356f Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Mon, 8 Jun 2020 11:53:07 +0300 Subject: [PATCH 14/32] chore(core): Stages (#8423) Stages are self-contained application units that synthesize as a cloud assembly. This change centralizes prepare + synthesis logic into the stage level and changes `App` to extend `Stage`. Once `stage.synth()` is called, the stage becomes (practically) immutable. This means that subsequent synths will return the same output. The cloud assembly produced by stages is nested as an artifact inside another cloud assembly (either the App's top-level assembly) or a child. Authors: @rix0rrr, @eladb ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- allowed-breaking-changes.txt | 6 +- .../lib/artifact-schema.ts | 22 +- .../cloud-assembly-schema/lib/schema.ts | 5 + .../schema/cloud-assembly.schema.json | 21 ++ .../schema/cloud-assembly.version.json | 2 +- .../scripts/update-schema.sh | 2 +- packages/@aws-cdk/core/README.md | 64 +++- packages/@aws-cdk/core/lib/app.ts | 46 +-- .../@aws-cdk/core/lib/construct-compat.ts | 25 +- packages/@aws-cdk/core/lib/deps.ts | 18 +- packages/@aws-cdk/core/lib/index.ts | 1 + .../@aws-cdk/core/lib/private/prepare-app.ts | 25 +- packages/@aws-cdk/core/lib/private/refs.ts | 6 +- .../@aws-cdk/core/lib/private/synthesis.ts | 170 ++++++++++ packages/@aws-cdk/core/lib/stack.ts | 187 ++++++++--- packages/@aws-cdk/core/lib/stage.ts | 201 ++++++++++++ packages/@aws-cdk/core/test/test.stage.ts | 304 ++++++++++++++++++ .../cx-api/design/NESTED_ASSEMBLIES.md | 93 ++++++ packages/@aws-cdk/cx-api/jest.config.js | 10 +- .../asset-manifest-artifact.ts | 4 +- .../cloudformation-artifact.ts | 24 +- .../nested-cloud-assembly-artifact.ts | 49 +++ .../{ => artifacts}/tree-cloud-artifact.ts | 4 +- .../@aws-cdk/cx-api/lib/cloud-artifact.ts | 11 +- .../@aws-cdk/cx-api/lib/cloud-assembly.ts | 62 +++- packages/@aws-cdk/cx-api/lib/index.ts | 7 +- .../test/cloud-assembly-builder.test.ts | 34 +- .../@aws-cdk/cx-api/test/placeholders.test.ts | 29 +- 28 files changed, 1279 insertions(+), 153 deletions(-) create mode 100644 packages/@aws-cdk/core/lib/private/synthesis.ts create mode 100644 packages/@aws-cdk/core/lib/stage.ts create mode 100644 packages/@aws-cdk/core/test/test.stage.ts create mode 100644 packages/@aws-cdk/cx-api/design/NESTED_ASSEMBLIES.md rename packages/@aws-cdk/cx-api/lib/{ => artifacts}/asset-manifest-artifact.ts (90%) rename packages/@aws-cdk/cx-api/lib/{ => artifacts}/cloudformation-artifact.ts (89%) create mode 100644 packages/@aws-cdk/cx-api/lib/artifacts/nested-cloud-assembly-artifact.ts rename packages/@aws-cdk/cx-api/lib/{ => artifacts}/tree-cloud-artifact.ts (83%) diff --git a/allowed-breaking-changes.txt b/allowed-breaking-changes.txt index e6bdc57ed11ae..e174e6ace55d6 100644 --- a/allowed-breaking-changes.txt +++ b/allowed-breaking-changes.txt @@ -1,5 +1,9 @@ +# Actually adding any artifact type will break the load() type signature because I could have written +# const x: A | B = Manifest.load(); +# and that won't typecheck if Manifest.load() adds a union arm and now returns A | B | C. +change-return-type:@aws-cdk/cloud-assembly-schema.Manifest.load + removed:@aws-cdk/core.BootstraplessSynthesizer.DEFAULT_ASSET_PUBLISHING_ROLE_ARN removed:@aws-cdk/core.DefaultStackSynthesizer.DEFAULT_ASSET_PUBLISHING_ROLE_ARN removed:@aws-cdk/core.DefaultStackSynthesizerProps.assetPublishingExternalId removed:@aws-cdk/core.DefaultStackSynthesizerProps.assetPublishingRoleArn - diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/artifact-schema.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/artifact-schema.ts index 866a1a6553c38..dd1337d6d5e52 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/artifact-schema.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/artifact-schema.ts @@ -84,7 +84,27 @@ export interface TreeArtifactProperties { readonly file: string; } +/** + * Artifact properties for nested cloud assemblies + */ +export interface NestedCloudAssemblyProperties { + /** + * Relative path to the nested cloud assembly + */ + readonly directoryName: string; + + /** + * Display name for the cloud assembly + * + * @default - The artifact ID + */ + readonly displayName?: string; +} + /** * Properties for manifest artifacts */ -export type ArtifactProperties = AwsCloudFormationStackProperties | AssetManifestProperties | TreeArtifactProperties; \ No newline at end of file +export type ArtifactProperties = AwsCloudFormationStackProperties +| AssetManifestProperties +| TreeArtifactProperties +| NestedCloudAssemblyProperties; \ No newline at end of file diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/schema.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/schema.ts index 1c4efd0cded5d..1d351364e019d 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/schema.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/schema.ts @@ -25,6 +25,11 @@ export enum ArtifactType { * Manifest for all assets in the Cloud Assembly */ ASSET_MANIFEST = 'cdk:asset-manifest', + + /** + * Nested Cloud Assembly + */ + NESTED_CLOUD_ASSEMBLY = 'cdk:cloud-assembly', } /** diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json index 73319145f8196..8c3e58485b12b 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json @@ -72,6 +72,9 @@ }, { "$ref": "#/definitions/TreeArtifactProperties" + }, + { + "$ref": "#/definitions/NestedCloudAssemblyProperties" } ] } @@ -85,6 +88,7 @@ "enum": [ "aws:cloudformation:stack", "cdk:asset-manifest", + "cdk:cloud-assembly", "cdk:tree", "none" ], @@ -331,6 +335,23 @@ "file" ] }, + "NestedCloudAssemblyProperties": { + "description": "Artifact properties for nested cloud assemblies", + "type": "object", + "properties": { + "directoryName": { + "description": "Relative path to the nested cloud assembly", + "type": "string" + }, + "displayName": { + "description": "Display name for the cloud assembly (Default - The artifact ID)", + "type": "string" + } + }, + "required": [ + "directoryName" + ] + }, "MissingContext": { "description": "Represents a missing piece of context.", "type": "object", diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json index 276fff8f8ba1f..78d33700c0698 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json @@ -1 +1 @@ -{"version":"4.0.0"} +{"version":"5.0.0"} diff --git a/packages/@aws-cdk/cloud-assembly-schema/scripts/update-schema.sh b/packages/@aws-cdk/cloud-assembly-schema/scripts/update-schema.sh index cde2aafa37aad..424e104e1dc85 100755 --- a/packages/@aws-cdk/cloud-assembly-schema/scripts/update-schema.sh +++ b/packages/@aws-cdk/cloud-assembly-schema/scripts/update-schema.sh @@ -1,7 +1,7 @@ #!/bin/bash set -euo pipefail scriptsdir=$(cd $(dirname $0) && pwd) -packagedir=$(realpath ${scriptsdir}/..) +packagedir=$(cd ${scriptsdir}/.. && pwd) # Output OUTPUT_DIR="${packagedir}/schema" diff --git a/packages/@aws-cdk/core/README.md b/packages/@aws-cdk/core/README.md index 2790600cc3da3..65f9067ff32d4 100644 --- a/packages/@aws-cdk/core/README.md +++ b/packages/@aws-cdk/core/README.md @@ -17,6 +17,42 @@ Guide](https://docs.aws.amazon.com/cdk/latest/guide/home.html) for information of most of the capabilities of this library. The rest of this README will only cover topics not already covered in the Developer Guide. +## Stacks and Stages + +A `Stack` is the smallest physical unit of deployment, and maps directly onto +a CloudFormation Stack. You define a Stack by defining a subclass of `Stack` +-- let's call it `MyStack` -- and instantiating the constructs that make up +your application in `MyStack`'s constructor. You then instantiate this stack +one or more times to define different instances of your application. For example, +you can instantiate it once using few and cheap EC2 instances for testing, +and once again using more and bigger EC2 instances for production. + +When your application grows, you may decide that it makes more sense to split it +out across multiple `Stack` classes. This can happen for a number of reasons: + +- You could be starting to reach the maximum number of resources allowed in a single + stack (this is currently 200). +- You could decide you want to separate out stateful resources and stateless resources + into separate stacks, so that it becomes easy to tear down and recreate the stacks + that don't have stateful resources. +- There could be a single stack with resources (like a VPC) that are shared + between multiple instances of other stacks containing your applications. + +As soon as your conceptual application starts to encompass multiple stacks, +it is convenient to wrap them in another construct that represents your +logical application. You can then treat that new unit the same way you used +to be able to treat a single stack: by instantiating it multiple times +for different instances of your application. + +You can define a custom subclass of `Construct`, holding one or more +`Stack`s, to represent a single logical instance of your application. + +As a final note: `Stack`s are not a unit of reuse. They describe physical +deployment layouts, and as such are best left to application builders to +organize their deployments with. If you want to vend a reusable construct, +define it as a subclasses of `Construct`: the consumers of your construct +will decide where to place it in their own stacks. + ## Nested Stacks [Nested stacks](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-nested-stacks.html) are stacks created as part of other stacks. You create a nested stack within another stack by using the `NestedStack` construct. @@ -36,7 +72,7 @@ class MyNestedStack extends cfn.NestedStack { constructor(scope: Construct, id: string, props?: cfn.NestedStackProps) { super(scope, id, props); - new s3.Bucket(this, 'NestedBucket'); + new s3.Bucket(this, 'NestedBucket'); } } @@ -236,7 +272,7 @@ new CustomResource(this, 'MyMagicalResource', { Property2: 'bar' }, - // the ARN of the provider (SNS/Lambda) which handles + // the ARN of the provider (SNS/Lambda) which handles // CREATE, UPDATE or DELETE events for this resource type // see next section for details serviceToken: 'ARN' @@ -292,7 +328,7 @@ function getOrCreate(scope: Construct): sns.Topic { Every time a resource event occurs (CREATE/UPDATE/DELETE), an SNS notification is sent to the SNS topic. Users must process these notifications (e.g. through a fleet of worker hosts) and submit success/failure responses to the -CloudFormation service. +CloudFormation service. Set `serviceToken` to `topic.topicArn` in order to use this provider: @@ -311,7 +347,7 @@ new CustomResource(this, 'MyResource', { An AWS lambda function is called *directly* by CloudFormation for all resource events. The handler must take care of explicitly submitting a success/failure -response to the CloudFormation service and handle various error cases. +response to the CloudFormation service and handle various error cases. Set `serviceToken` to `lambda.functionArn` to use this provider: @@ -361,7 +397,7 @@ exports.handler = async function(event) { const id = event.PhysicalResourceId; // only for "Update" and "Delete" const props = event.ResourceProperties; const oldProps = event.OldResourceProperties; // only for "Update"s - + switch (event.RequestType) { case "Create": // ... @@ -371,7 +407,7 @@ exports.handler = async function(event) { // if an error is thrown, a FAILED response will be submitted to CFN throw new Error('Failed!'); - + case "Delete": // ... } @@ -403,10 +439,10 @@ Here is an complete example of a custom resource that summarizes two numbers: ```js exports.handler = async e => { - return { - Data: { + return { + Data: { Result: e.ResourceProperties.lhs + e.ResourceProperties.rhs - } + } }; }; ``` @@ -463,7 +499,7 @@ Handlers are implemented as AWS Lambda functions, which means that they can be implemented in any Lambda-supported runtime. Furthermore, this provider has an asynchronous mode, which means that users can provide an `isComplete` lambda function which is called periodically until the operation is complete. This -allows implementing providers that can take up to two hours to stabilize. +allows implementing providers that can take up to two hours to stabilize. Set `serviceToken` to `provider.serviceToken` to use this type of provider: @@ -487,7 +523,7 @@ See the [documentation](https://docs.aws.amazon.com/cdk/api/latest/docs/custom-r Every time a resource event occurs (CREATE/UPDATE/DELETE), an SNS notification is sent to the SNS topic. Users must process these notifications (e.g. through a fleet of worker hosts) and submit success/failure responses to the -CloudFormation service. +CloudFormation service. Set `serviceToken` to `topic.topicArn` in order to use this provider: @@ -506,7 +542,7 @@ new CustomResource(this, 'MyResource', { An AWS lambda function is called *directly* by CloudFormation for all resource events. The handler must take care of explicitly submitting a success/failure -response to the CloudFormation service and handle various error cases. +response to the CloudFormation service and handle various error cases. Set `serviceToken` to `lambda.functionArn` to use this provider: @@ -532,7 +568,7 @@ Handlers are implemented as AWS Lambda functions, which means that they can be implemented in any Lambda-supported runtime. Furthermore, this provider has an asynchronous mode, which means that users can provide an `isComplete` lambda function which is called periodically until the operation is complete. This -allows implementing providers that can take up to two hours to stabilize. +allows implementing providers that can take up to two hours to stabilize. Set `serviceToken` to `provider.serviceToken` to use this provider: @@ -827,7 +863,7 @@ to use intrinsic functions in keys. Since JSON map keys must be strings, it is impossible to use intrinsics in keys and `CfnJson` can help. The following example defines an IAM role which can only be assumed by -principals that are tagged with a specific tag. +principals that are tagged with a specific tag. ```ts const tagParam = new CfnParameter(this, 'TagName'); diff --git a/packages/@aws-cdk/core/lib/app.ts b/packages/@aws-cdk/core/lib/app.ts index 0cc7a1a6ed8d1..1546ab19ee53c 100644 --- a/packages/@aws-cdk/core/lib/app.ts +++ b/packages/@aws-cdk/core/lib/app.ts @@ -1,8 +1,6 @@ import * as cxapi from '@aws-cdk/cx-api'; -import { Construct, ConstructNode } from './construct-compat'; -import { prepareApp } from './private/prepare-app'; -import { collectRuntimeInformation } from './private/runtime-info'; import { TreeMetadata } from './private/tree-metadata'; +import { Stage } from './stage'; const APP_SYMBOL = Symbol.for('@aws-cdk/core.App'); @@ -76,8 +74,7 @@ export interface AppProps { * * @see https://docs.aws.amazon.com/cdk/latest/guide/apps.html */ -export class App extends Construct { - +export class App extends Stage { /** * Checks if an object is an instance of the `App` class. * @returns `true` if `obj` is an `App`. @@ -87,16 +84,14 @@ export class App extends Construct { return APP_SYMBOL in obj; } - private _assembly?: cxapi.CloudAssembly; - private readonly runtimeInfo: boolean; - private readonly outdir?: string; - /** * Initializes a CDK application. * @param props initialization properties */ constructor(props: AppProps = {}) { - super(undefined as any, ''); + super(undefined as any, '', { + outdir: props.outdir ?? process.env[cxapi.OUTDIR_ENV], + }); Object.defineProperty(this, APP_SYMBOL, { value: true }); @@ -110,10 +105,6 @@ export class App extends Construct { this.node.setContext(cxapi.DISABLE_VERSION_REPORTING, true); } - // both are reverse logic - this.runtimeInfo = this.node.tryGetContext(cxapi.DISABLE_VERSION_REPORTING) ? false : true; - this.outdir = props.outdir || process.env[cxapi.OUTDIR_ENV]; - const autoSynth = props.autoSynth !== undefined ? props.autoSynth : cxapi.OUTDIR_ENV in process.env; if (autoSynth) { // synth() guarantuees it will only execute once, so a default of 'true' @@ -126,33 +117,6 @@ export class App extends Construct { } } - /** - * Synthesizes a cloud assembly for this app. Emits it to the directory - * specified by `outdir`. - * - * @returns a `CloudAssembly` which can be used to inspect synthesized - * artifacts such as CloudFormation templates and assets. - */ - public synth(): cxapi.CloudAssembly { - // we already have a cloud assembly, no-op for you - if (this._assembly) { - return this._assembly; - } - - const assembly = ConstructNode.synth(this.node, { - outdir: this.outdir, - runtimeInfo: this.runtimeInfo ? collectRuntimeInformation() : undefined, - }); - - this._assembly = assembly; - return assembly; - } - - protected prepare() { - super.prepare(); - prepareApp(this); - } - private loadContext(defaults: { [key: string]: string } = { }) { // prime with defaults passed through constructor for (const [ k, v ] of Object.entries(defaults)) { diff --git a/packages/@aws-cdk/core/lib/construct-compat.ts b/packages/@aws-cdk/core/lib/construct-compat.ts index 341943a748bca..78e57266fe768 100644 --- a/packages/@aws-cdk/core/lib/construct-compat.ts +++ b/packages/@aws-cdk/core/lib/construct-compat.ts @@ -182,6 +182,8 @@ export enum ConstructOrder { /** * Options for synthesis. + * + * @deprecated use `app.synth()` or `stage.synth()` instead */ export interface SynthesisOptions extends cxapi.AssemblyBuildOptions { /** @@ -222,28 +224,25 @@ export class ConstructNode { /** * Synthesizes a CloudAssembly from a construct tree. - * @param root The root of the construct tree. + * @param node The root of the construct tree. * @param options Synthesis options. + * @deprecated Use `app.synth()` or `stage.synth()` instead */ - public static synth(root: ConstructNode, options: SynthesisOptions = { }): cxapi.CloudAssembly { - const builder = new cxapi.CloudAssemblyBuilder(options.outdir); - - root._actualNode.synthesize({ - outdir: builder.outdir, - skipValidation: options.skipValidation, - sessionContext: { - assembly: builder, - }, - }); - - return builder.buildAssembly(options); + public static synth(node: ConstructNode, options: SynthesisOptions = { }): cxapi.CloudAssembly { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const a: typeof import('././private/synthesis') = require('./private/synthesis'); + return a.synthesize(node.root, options); } /** * Invokes "prepare" on all constructs (depth-first, post-order) in the tree under `node`. * @param node The root node + * @deprecated Use `app.synth()` instead */ public static prepare(node: ConstructNode) { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const p: typeof import('./private/prepare-app') = require('./private/prepare-app'); + p.prepareApp(node.root); // resolve cross refs and nested stack assets. return node._actualNode.prepare(); } diff --git a/packages/@aws-cdk/core/lib/deps.ts b/packages/@aws-cdk/core/lib/deps.ts index b26fa28cb187b..c34f11ef2c5a7 100644 --- a/packages/@aws-cdk/core/lib/deps.ts +++ b/packages/@aws-cdk/core/lib/deps.ts @@ -1,5 +1,6 @@ import { CfnResource } from './cfn-resource'; import { Stack } from './stack'; +import { Stage } from './stage'; import { findLastCommonElement, pathToTopLevelStack as pathToRoot } from './util'; type Element = CfnResource | Stack; @@ -31,12 +32,18 @@ export function addDependency(source: T, target: T, reason?: const sourceStack = Stack.of(source); const targetStack = Stack.of(target); + const sourceStage = Stage.of(sourceStack); + const targetStage = Stage.of(targetStack); + if (sourceStage !== targetStage) { + throw new Error(`You cannot add a dependency from '${source.node.path}' (in ${describeStage(sourceStage)}) to '${target.node.path}' (in ${describeStage(targetStage)}): dependency cannot cross stage boundaries`); + } + // find the deepest common stack between the two elements const sourcePath = pathToRoot(sourceStack); const targetPath = pathToRoot(targetStack); const commonStack = findLastCommonElement(sourcePath, targetPath); - // if there is no common stack, then define an assembly-level dependency + // if there is no common stack, then define a assembly-level dependency // between the two top-level stacks if (!commonStack) { const topLevelSource = sourcePath[0]; // first path element is the top-level stack @@ -88,3 +95,12 @@ export function addDependency(source: T, target: T, reason?: return resourceInCommonStackFor(resourceStack); } } + +/** + * Return a string representation of the given assembler, for use in error messages + */ +function describeStage(assembly: Stage | undefined): string { + if (!assembly) { return 'an unrooted construct tree'; } + if (!assembly.parentStage) { return 'the App'; } + return `Stage '${assembly.node.path}'`; +} diff --git a/packages/@aws-cdk/core/lib/index.ts b/packages/@aws-cdk/core/lib/index.ts index 4e55122d5616f..8b238e0c721fd 100644 --- a/packages/@aws-cdk/core/lib/index.ts +++ b/packages/@aws-cdk/core/lib/index.ts @@ -22,6 +22,7 @@ export * from './cfn-resource'; export * from './cfn-resource-policy'; export * from './cfn-rule'; export * from './stack'; +export * from './stage'; export * from './cfn-element'; export * from './cfn-dynamic-reference'; export * from './cfn-tag'; diff --git a/packages/@aws-cdk/core/lib/private/prepare-app.ts b/packages/@aws-cdk/core/lib/private/prepare-app.ts index 6912f29a9fce5..de5ef433fb1ad 100644 --- a/packages/@aws-cdk/core/lib/private/prepare-app.ts +++ b/packages/@aws-cdk/core/lib/private/prepare-app.ts @@ -1,7 +1,8 @@ import { ConstructOrder } from 'constructs'; import { CfnResource } from '../cfn-resource'; -import { Construct, IConstruct } from '../construct-compat'; +import { IConstruct } from '../construct-compat'; import { Stack } from '../stack'; +import { Stage } from '../stage'; import { resolveReferences } from './refs'; /** @@ -14,9 +15,9 @@ import { resolveReferences } from './refs'; * * @param root The root of the construct tree. */ -export function prepareApp(root: Construct) { - if (root.node.scope) { - throw new Error('prepareApp must be called on the root node'); +export function prepareApp(root: IConstruct) { + if (root.node.scope && !Stage.isStage(root)) { + throw new Error('prepareApp can only be called on a stage or a root construct'); } // apply dependencies between resources in depending subtrees @@ -32,7 +33,7 @@ export function prepareApp(root: Construct) { } // depth-first (children first) queue of nested stacks. We will pop a stack - // from the head of this queue to prepare it's template asset. + // from the head of this queue to prepare its template asset. const queue = findAllNestedStacks(root); while (true) { @@ -59,13 +60,23 @@ function defineNestedStackAsset(nestedStack: Stack) { nested._prepareTemplateAsset(); } -function findAllNestedStacks(root: Construct) { +function findAllNestedStacks(root: IConstruct) { const result = new Array(); + const includeStack = (stack: IConstruct): stack is Stack => { + if (!Stack.isStack(stack)) { return false; } + if (!stack.nested) { return false; } + + // test: if we are not within a stage, then include it. + if (!Stage.of(stack)) { return true; } + + return Stage.of(stack) === root; + }; + // create a list of all nested stacks in depth-first post order this means // that we first prepare the leaves and then work our way up. for (const stack of root.node.findAll(ConstructOrder.POSTORDER /* <== important */)) { - if (Stack.isStack(stack) && stack.nested) { + if (includeStack(stack)) { result.push(stack); } } diff --git a/packages/@aws-cdk/core/lib/private/refs.ts b/packages/@aws-cdk/core/lib/private/refs.ts index baa92ff8202e3..62a568f8cd736 100644 --- a/packages/@aws-cdk/core/lib/private/refs.ts +++ b/packages/@aws-cdk/core/lib/private/refs.ts @@ -4,7 +4,7 @@ import { CfnElement } from '../cfn-element'; import { CfnOutput } from '../cfn-output'; import { CfnParameter } from '../cfn-parameter'; -import { Construct } from '../construct-compat'; +import { Construct, IConstruct } from '../construct-compat'; import { Reference } from '../reference'; import { IResolvable } from '../resolvable'; import { Stack } from '../stack'; @@ -18,7 +18,7 @@ import { makeUniqueId } from './uniqueid'; * This is called from the App level to resolve all references defined. Each * reference is resolved based on it's consumption context. */ -export function resolveReferences(scope: Construct): void { +export function resolveReferences(scope: IConstruct): void { const edges = findAllReferences(scope); for (const { source, value } of edges) { @@ -105,7 +105,7 @@ function resolveValue(consumer: Stack, reference: CfnReference): IResolvable { /** * Finds all the CloudFormation references in a construct tree. */ -function findAllReferences(root: Construct) { +function findAllReferences(root: IConstruct) { const result = new Array<{ source: CfnElement, value: CfnReference }>(); for (const consumer of root.node.findAll()) { diff --git a/packages/@aws-cdk/core/lib/private/synthesis.ts b/packages/@aws-cdk/core/lib/private/synthesis.ts new file mode 100644 index 0000000000000..ea6fbf7b05ffa --- /dev/null +++ b/packages/@aws-cdk/core/lib/private/synthesis.ts @@ -0,0 +1,170 @@ +import * as cxapi from '@aws-cdk/cx-api'; +import * as constructs from 'constructs'; +import { Construct, IConstruct, SynthesisOptions, ValidationError } from '../construct-compat'; +import { Stage, StageSynthesisOptions } from '../stage'; +import { prepareApp } from './prepare-app'; + +export function synthesize(root: IConstruct, options: SynthesisOptions = { }): cxapi.CloudAssembly { + // we start by calling "synth" on all nested assemblies (which will take care of all their children) + synthNestedAssemblies(root, options); + + invokeAspects(root); + + // This is mostly here for legacy purposes as the framework itself does not use prepare anymore. + prepareTree(root); + + // resolve references + prepareApp(root); + + // give all children an opportunity to validate now that we've finished prepare + if (!options.skipValidation) { + validateTree(root); + } + + // in unit tests, we support creating free-standing stacks, so we create the + // assembly builder here. + const builder = Stage.isStage(root) + ? root._assemblyBuilder + : new cxapi.CloudAssemblyBuilder(options.outdir); + + // next, we invoke "onSynthesize" on all of our children. this will allow + // stacks to add themselves to the synthesized cloud assembly. + synthesizeTree(root, builder); + + return builder.buildAssembly({ + runtimeInfo: options.runtimeInfo, + }); +} + +/** + * Find Assemblies inside the construct and call 'synth' on them + * + * (They will in turn recurse again) + */ +function synthNestedAssemblies(root: IConstruct, options: StageSynthesisOptions) { + for (const child of root.node.children) { + if (Stage.isStage(child)) { + child.synth(options); + } else { + synthNestedAssemblies(child, options); + } + } +} + +/** + * Invoke aspects on the given construct tree. + * + * Aspects are not propagated across Assembly boundaries. The same Aspect will not be invoked + * twice for the same construct. + */ +function invokeAspects(root: IConstruct) { + recurse(root, []); + + function recurse(construct: IConstruct, inheritedAspects: constructs.IAspect[]) { + // hackery to be able to access some private members with strong types (yack!) + const node: NodeWithAspectPrivatesHangingOut = construct.node._actualNode as any; + + const allAspectsHere = [...inheritedAspects ?? [], ...node._aspects]; + + for (const aspect of allAspectsHere) { + if (node.invokedAspects.includes(aspect)) { continue; } + aspect.visit(construct); + node.invokedAspects.push(aspect); + } + + for (const child of construct.node.children) { + if (!Stage.isStage(child)) { + recurse(child, allAspectsHere); + } + } + } +} + +/** + * Prepare all constructs in the given construct tree in post-order. + * + * Stop at Assembly boundaries. + */ +function prepareTree(root: IConstruct) { + visit(root, 'post', construct => construct.onPrepare()); +} + +/** + * Synthesize children in post-order into the given builder + * + * Stop at Assembly boundaries. + */ +function synthesizeTree(root: IConstruct, builder: cxapi.CloudAssemblyBuilder) { + visit(root, 'post', construct => construct.onSynthesize({ + outdir: builder.outdir, + assembly: builder, + })); +} + +/** + * Validate all constructs in the given construct tree + */ +function validateTree(root: IConstruct) { + const errors = new Array(); + + visit(root, 'pre', construct => { + for (const message of construct.onValidate()) { + errors.push({ message, source: construct as unknown as Construct }); + } + }); + + if (errors.length > 0) { + const errorList = errors.map(e => `[${e.source.node.path}] ${e.message}`).join('\n '); + throw new Error(`Validation failed with the following errors:\n ${errorList}`); + } +} + +/** + * Visit the given construct tree in either pre or post order, stopping at Assemblies + */ +function visit(root: IConstruct, order: 'pre' | 'post', cb: (x: IProtectedConstructMethods) => void) { + if (order === 'pre') { + cb(root as IProtectedConstructMethods); + } + + for (const child of root.node.children) { + if (Stage.isStage(child)) { continue; } + visit(child, order, cb); + } + + if (order === 'post') { + cb(root as IProtectedConstructMethods); + } +} + +/** + * Interface which provides access to special methods of Construct + * + * @experimental + */ +interface IProtectedConstructMethods extends IConstruct { + /** + * Method that gets called when a construct should synthesize itself to an assembly + */ + onSynthesize(session: constructs.ISynthesisSession): void; + + /** + * Method that gets called to validate a construct + */ + onValidate(): string[]; + + /** + * Method that gets called to prepare a construct + */ + onPrepare(): void; +} + +/** + * The constructs Node type, but with some aspects-related fields public. + * + * Hackery! + */ +type NodeWithAspectPrivatesHangingOut = Omit & { + readonly invokedAspects: constructs.IAspect[]; + readonly _aspects: constructs.IAspect[]; +}; \ No newline at end of file diff --git a/packages/@aws-cdk/core/lib/stack.ts b/packages/@aws-cdk/core/lib/stack.ts index 72980dfbfbfe2..eeb65562837ec 100644 --- a/packages/@aws-cdk/core/lib/stack.ts +++ b/packages/@aws-cdk/core/lib/stack.ts @@ -27,8 +27,66 @@ export interface StackProps { /** * The AWS environment (account/region) where this stack will be deployed. * - * @default - The `default-account` and `default-region` context parameters will be - * used. If they are undefined, it will not be possible to deploy the stack. + * Set the `region`/`account` fields of `env` to either a concrete value to + * select the indicated environment (recommended for production stacks), or to + * the values of environment variables + * `CDK_DEFAULT_REGION`/`CDK_DEFAULT_ACCOUNT` to let the target environment + * depend on the AWS credentials/configuration that the CDK CLI is executed + * under (recommended for development stacks). + * + * If the `Stack` is instantiated inside a `Stage`, any undefined + * `region`/`account` fields from `env` will default to the same field on the + * encompassing `Stage`, if configured there. + * + * If either `region` or `account` are not set nor inherited from `Stage`, the + * Stack will be considered "*environment-agnostic*"". Environment-agnostic + * stacks can be deployed to any environment but may not be able to take + * advantage of all features of the CDK. For example, they will not be able to + * use environmental context lookups such as `ec2.Vpc.fromLookup` and will not + * automatically translate Service Principals to the right format based on the + * environment's AWS partition, and other such enhancements. + * + * @example + * + * // Use a concrete account and region to deploy this stack to: + * // `.account` and `.region` will simply return these values. + * new MyStack(app, 'Stack1', { + * env: { + * account: '123456789012', + * region: 'us-east-1' + * }, + * }); + * + * // Use the CLI's current credentials to determine the target environment: + * // `.account` and `.region` will reflect the account+region the CLI + * // is configured to use (based on the user CLI credentials) + * new MyStack(app, 'Stack2', { + * env: { + * account: process.env.CDK_DEFAULT_ACCOUNT, + * region: process.env.CDK_DEFAULT_REGION + * }, + * }); + * + * // Define multiple stacks stage associated with an environment + * const myStage = new Stage(app, 'MyStage', { + * env: { + * account: '123456789012', + * region: 'us-east-1' + * } + * }); + * + * // both of these stavks will use the stage's account/region: + * // `.account` and `.region` will resolve to the concrete values as above + * new MyStack(myStage, 'Stack1'); + * new YourStack(myStage, 'Stack1'); + * + * // Define an environment-agnostic stack: + * // `.account` and `.region` will resolve to `{ "Ref": "AWS::AccountId" }` and `{ "Ref": "AWS::Region" }` respectively. + * // which will only resolve to actual values by CloudFormation during deployment. + * new MyStack(app, 'Stack1'); + * + * @default - The environment of the containing `Stage` if available, + * otherwise create the stack will be environment-agnostic. */ readonly env?: Environment; @@ -265,7 +323,7 @@ export class Stack extends Construct implements ITaggable { this.templateOptions.description = props.description; } - this._stackName = props.stackName !== undefined ? props.stackName : this.generateUniqueId(); + this._stackName = props.stackName !== undefined ? props.stackName : this.generateStackName(); this.tags = new TagManager(TagType.KEY_VALUE, 'aws:cdk:stack', props.tags); if (!VALID_STACK_NAME_REGEX.test(this.stackName)) { @@ -277,8 +335,12 @@ export class Stack extends Construct implements ITaggable { // the same name. however, this behavior is breaking for 1.x so it's only // applied under a feature flag which is applied automatically for new // projects created using `cdk init`. - this.artifactId = this.node.tryGetContext(cxapi.ENABLE_STACK_NAME_DUPLICATES_CONTEXT) - ? this.generateUniqueId() + // + // Also use the new behavior if we are using the new CI/CD-ready synthesizer; that way + // people only have to flip one flag. + // tslint:disable-next-line: max-line-length + this.artifactId = this.node.tryGetContext(cxapi.ENABLE_STACK_NAME_DUPLICATES_CONTEXT) || this.node.tryGetContext(cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT) + ? this.generateStackArtifactId() : this.stackName; this.templateFile = `${this.artifactId}.template.json`; @@ -681,21 +743,6 @@ export class Stack extends Construct implements ITaggable { } } - /** - * Prepare stack - * - * Find all CloudFormation references and tell them we're consuming them. - * - * Find all dependencies as well and add the appropriate DependsOn fields. - */ - protected prepare() { - // if this stack is a roort (e.g. in unit tests), call `prepareApp` so that - // we resolve cross-references and nested stack assets. - if (!this.node.scope) { - prepareApp(this); - } - } - protected synthesize(session: ISynthesisSession): void { // In principle, stack synthesis is delegated to the // StackSynthesis object. @@ -781,12 +828,15 @@ export class Stack extends Construct implements ITaggable { */ private parseEnvironment(env: Environment = {}) { // if an environment property is explicitly specified when the stack is - // created, it will be used. if not, use tokens for account and region but - // they do not need to be scoped, the only situation in which - // export/fn::importvalue would work if { Ref: "AWS::AccountId" } is the - // same for provider and consumer anyway. - const account = env.account || Aws.ACCOUNT_ID; - const region = env.region || Aws.REGION; + // created, it will be used. if not, use tokens for account and region. + // + // (They do not need to be anchored to any construct like resource attributes + // are, because we'll never Export/Fn::ImportValue them -- the only situation + // in which Export/Fn::ImportValue would work is if the value are the same + // between producer and consumer anyway, so we can just assume that they are). + const containingAssembly = Stage.of(this); + const account = env.account ?? containingAssembly?.account ?? Aws.ACCOUNT_ID; + const region = env.region ?? containingAssembly?.region ?? Aws.REGION; // this is the "aws://" env specification that will be written to the cloud assembly // manifest. it will use "unknown-account" and "unknown-region" to indicate @@ -818,24 +868,54 @@ export class Stack extends Construct implements ITaggable { } /** - * Calculcate the stack name based on the construct path + * Calculate the stack name based on the construct path + * + * The stack name is the name under which we'll deploy the stack, + * and incorporates containing Stage names by default. + * + * Generally this looks a lot like how logical IDs are calculated. + * The stack name is calculated based on the construct root path, + * as follows: + * + * - Path is calculated with respect to containing App or Stage (if any) + * - If the path is one component long just use that component, otherwise + * combine them with a hash. + * + * Since the hash is quite ugly and we'd like to avoid it if possible -- but + * we can't anymore in the general case since it has been written into legacy + * stacks. The introduction of Stages makes it possible to make this nicer however. + * When a Stack is nested inside a Stage, we use the path components below the + * Stage, and prefix the path components of the Stage before it. + */ + private generateStackName() { + const assembly = Stage.of(this); + const prefix = (assembly && assembly.stageName) ? `${assembly.stageName}-` : ''; + return `${prefix}${this.generateStackId(assembly)}`; + } + + /** + * The artifact ID for this stack + * + * Stack artifact ID is unique within the App's Cloud Assembly. + */ + private generateStackArtifactId() { + return this.generateStackId(this.node.root); + } + + /** + * Generate an ID with respect to the given container construct. */ - private generateUniqueId() { - // In tests, it's possible for this stack to be the root object, in which case - // we need to use it as part of the root path. - const rootPath = this.node.scope !== undefined ? this.node.scopes.slice(1) : [this]; + private generateStackId(container: IConstruct | undefined) { + const rootPath = rootPathTo(this, container); const ids = rootPath.map(c => c.node.id); - // Special case, if rootPath is length 1 then just use ID (backwards compatibility) - // otherwise use a unique stack name (including hash). This logic is already - // in makeUniqueId, *however* makeUniqueId will also strip dashes from the name, - // which *are* allowed and also used, so we short-circuit it. - if (ids.length === 1) { - // Could be empty in a unit test, so just pretend it's named "Stack" then - return ids[0] || 'Stack'; + // In unit tests our Stack (which is the only component) may not have an + // id, so in that case just pretend it's "Stack". + if (ids.length === 1 && !ids[0]) { + ids[0] = 'Stack'; } - return makeUniqueId(ids); + return makeStackName(ids); } } @@ -950,6 +1030,33 @@ function cfnElements(node: IConstruct, into: CfnElement[] = []): CfnElement[] { return into; } +/** + * Return the construct root path of the given construct relative to the given ancestor + * + * If no ancestor is given or the ancestor is not found, return the entire root path. + */ +export function rootPathTo(construct: IConstruct, ancestor?: IConstruct): IConstruct[] { + const scopes = construct.node.scopes; + for (let i = scopes.length - 2; i >= 0; i--) { + if (scopes[i] === ancestor) { + return scopes.slice(i + 1); + } + } + return scopes; +} + +/** + * makeUniqueId, specialized for Stack names + * + * Stack names may contain '-', so we allow that character if the stack name + * has only one component. Otherwise we fall back to the regular "makeUniqueId" + * behavior. + */ +function makeStackName(components: string[]) { + if (components.length === 1) { return components[0]; } + return makeUniqueId(components); +} + // These imports have to be at the end to prevent circular imports import { Arn, ArnComponents } from './arn'; import { CfnElement } from './cfn-element'; @@ -957,10 +1064,10 @@ import { Fn } from './cfn-fn'; import { Aws, ScopedAws } from './cfn-pseudo'; import { CfnResource, TagType } from './cfn-resource'; import { addDependency } from './deps'; -import { prepareApp } from './private/prepare-app'; import { Reference } from './reference'; import { IResolvable } from './resolvable'; import { DefaultStackSynthesizer, IStackSynthesizer, LegacyStackSynthesizer } from './stack-synthesizers'; +import { Stage } from './stage'; import { ITaggable, TagManager } from './tag-manager'; import { Token } from './token'; diff --git a/packages/@aws-cdk/core/lib/stage.ts b/packages/@aws-cdk/core/lib/stage.ts new file mode 100644 index 0000000000000..59a466499bc9a --- /dev/null +++ b/packages/@aws-cdk/core/lib/stage.ts @@ -0,0 +1,201 @@ +import * as cxapi from '@aws-cdk/cx-api'; +import { Construct, IConstruct } from './construct-compat'; +import { Environment } from './environment'; +import { collectRuntimeInformation } from './private/runtime-info'; +import { synthesize } from './private/synthesis'; + +/** + * Initialization props for a stage. + */ +export interface StageProps { + /** + * Default AWS environment (account/region) for `Stack`s in this `Stage`. + * + * Stacks defined inside this `Stage` with either `region` or `account` missing + * from its env will use the corresponding field given here. + * + * If either `region` or `account`is is not configured for `Stack` (either on + * the `Stack` itself or on the containing `Stage`), the Stack will be + * *environment-agnostic*. + * + * Environment-agnostic stacks can be deployed to any environment, may not be + * able to take advantage of all features of the CDK. For example, they will + * not be able to use environmental context lookups, will not automatically + * translate Service Principals to the right format based on the environment's + * AWS partition, and other such enhancements. + * + * @example + * + * // Use a concrete account and region to deploy this Stage to + * new MyStage(app, 'Stage1', { + * env: { account: '123456789012', region: 'us-east-1' }, + * }); + * + * // Use the CLI's current credentials to determine the target environment + * new MyStage(app, 'Stage2', { + * env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, + * }); + * + * @default - The environments should be configured on the `Stack`s. + */ + readonly env?: Environment; + + /** + * The output directory into which to emit synthesized artifacts. + * + * Can only be specified if this stage is the root stage (the app). If this is + * specified and this stage is nested within another stage, an error will be + * thrown. + * + * @default - for nested stages, outdir will be determined as a relative + * directory to the outdir of the app. For apps, if outdir is not specified, a + * temporary directory will be created. + */ + readonly outdir?: string; +} + +/** + * An abstract application modeling unit consisting of Stacks that should be + * deployed together. + * + * Derive a subclass of `Stage` and use it to model a single instance of your + * application. + * + * You can then instantiate your subclass multiple times to model multiple + * copies of your application which should be be deployed to different + * environments. + */ +export class Stage extends Construct { + /** + * Return the stage this construct is contained with, if available. If called + * on a nested stage, returns its parent. + * + * @experimental + */ + public static of(construct: IConstruct): Stage | undefined { + return construct.node.scopes.reverse().slice(1).find(Stage.isStage); + } + + /** + * Test whether the given construct is a stage. + * + * @experimental + */ + public static isStage(x: any ): x is Stage { + return x !== null && x instanceof Stage; + } + + /** + * The default region for all resources defined within this stage. + * + * @experimental + */ + public readonly region?: string; + + /** + * The default account for all resources defined within this stage. + * + * @experimental + */ + public readonly account?: string; + + /** + * The cloud assembly builder that is being used for this App + * + * @experimental + * @internal + */ + public readonly _assemblyBuilder: cxapi.CloudAssemblyBuilder; + + /** + * The name of the stage. Based on names of the parent stages separated by + * hypens. + * + * @experimental + */ + public readonly stageName: string; + + /** + * The parent stage or `undefined` if this is the app. + * * + * @experimental + */ + public readonly parentStage?: Stage; + + /** + * The cached assembly if it was already built + */ + private assembly?: cxapi.CloudAssembly; + + constructor(scope: Construct, id: string, props: StageProps = {}) { + super(scope, id); + + if (id !== '' && !/^[a-z][a-z0-9\-\_\.]+$/i.test(id)) { + throw new Error(`invalid stage name "${id}". Stage name must start with a letter and contain only alphanumeric characters, hypens ('-'), underscores ('_') and periods ('.')`); + } + + this.parentStage = Stage.of(this); + + this.region = props.env?.region ?? this.parentStage?.region; + this.account = props.env?.account ?? this.parentStage?.account; + + this._assemblyBuilder = this.createBuilder(props.outdir); + this.stageName = [ this.parentStage?.stageName, id ].filter(x => x).join('-'); + } + + /** + * Artifact ID of the assembly if it is a nested stage. The root stage (app) + * will return an empty string. + * + * Derived from the construct path. + * + * @experimental + */ + public get artifactId() { + if (!this.node.path) { return ''; } + return `assembly-${this.node.path.replace(/\//g, '-').replace(/^-+|-+$/g, '')}`; + } + + /** + * Synthesize this stage into a cloud assembly. + * + * Once an assembly has been synthesized, it cannot be modified. Subsequent + * calls will return the same assembly. + */ + public synth(options: StageSynthesisOptions = { }): cxapi.CloudAssembly { + if (!this.assembly) { + const runtimeInfo = this.node.tryGetContext(cxapi.DISABLE_VERSION_REPORTING) ? undefined : collectRuntimeInformation(); + this.assembly = synthesize(this, { + skipValidation: options.skipValidation, + runtimeInfo, + }); + } + + return this.assembly; + } + + private createBuilder(outdir?: string) { + // cannot specify "outdir" if we are a nested stage + if (this.parentStage && outdir) { + throw new Error('"outdir" cannot be specified for nested stages'); + } + + // Need to determine fixed output directory already, because we must know where + // to write sub-assemblies (which must happen before we actually get to this app's + // synthesize() phase). + return this.parentStage + ? this.parentStage._assemblyBuilder.createNestedAssembly(this.artifactId, this.node.path) + : new cxapi.CloudAssemblyBuilder(outdir); + } +} + +/** + * Options for assemly synthesis. + */ +export interface StageSynthesisOptions { + /** + * Should we skip construct validation. + * @default - false + */ + readonly skipValidation?: boolean; +} diff --git a/packages/@aws-cdk/core/test/test.stage.ts b/packages/@aws-cdk/core/test/test.stage.ts new file mode 100644 index 0000000000000..4f5e5f02d0542 --- /dev/null +++ b/packages/@aws-cdk/core/test/test.stage.ts @@ -0,0 +1,304 @@ +import * as cxapi from '@aws-cdk/cx-api'; +import { Test } from 'nodeunit'; +import { App, CfnResource, Construct, IAspect, IConstruct, Stack, Stage } from '../lib'; + +export = { + 'Stack inherits unspecified part of the env from Stage'(test: Test) { + // GIVEN + const app = new App(); + const stage = new Stage(app, 'Stage', { + env: { account: 'account', region: 'region' }, + }); + + // WHEN + const stack1 = new Stack(stage, 'Stack1', { env: { region: 'elsewhere' } }); + const stack2 = new Stack(stage, 'Stack2', { env: { account: 'tnuocca' } }); + + // THEN + test.deepEqual(acctRegion(stack1), ['account', 'elsewhere']); + test.deepEqual(acctRegion(stack2), ['tnuocca', 'region']); + + test.done(); + }, + + 'envs are inherited deeply'(test: Test) { + // GIVEN + const app = new App(); + const outer = new Stage(app, 'Stage', { + env: { account: 'account', region: 'region' }, + }); + + // WHEN + const innerAcct = new Stage(outer, 'Acct', { env: { account: 'tnuocca' }}); + const innerRegion = new Stage(outer, 'Rgn', { env: { region: 'elsewhere' }}); + const innerNeither = new Stage(outer, 'Neither'); + + // THEN + test.deepEqual(acctRegion(new Stack(innerAcct, 'Stack')), ['tnuocca', 'region']); + test.deepEqual(acctRegion(new Stack(innerRegion, 'Stack')), ['account', 'elsewhere']); + test.deepEqual(acctRegion(new Stack(innerNeither, 'Stack')), ['account', 'region']); + + test.done(); + }, + + 'The Stage Assembly is in the app Assembly\'s manifest'(test: Test) { + // WHEN + const app = new App(); + const stage = new Stage(app, 'Stage'); + new BogusStack(stage, 'Stack2'); + + // THEN -- app manifest contains a nested cloud assembly + const appAsm = app.synth(); + + const artifact = appAsm.artifacts.find(x => x instanceof cxapi.NestedCloudAssemblyArtifact); + test.ok(artifact); + + test.done(); + }, + + 'Stacks in Stage are in a different cxasm than Stacks in App'(test: Test) { + // WHEN + const app = new App(); + const stack1 = new BogusStack(app, 'Stack1'); + const stage = new Stage(app, 'Stage'); + const stack2 = new BogusStack(stage, 'Stack2'); + + // THEN + const stageAsm = stage.synth(); + test.deepEqual(stageAsm.stacks.map(s => s.stackName), [stack2.stackName]); + + const appAsm = app.synth(); + test.deepEqual(appAsm.stacks.map(s => s.stackName), [stack1.stackName]); + + test.done(); + }, + + 'Can nest Stages inside other Stages'(test: Test) { + // WHEN + const app = new App(); + const outer = new Stage(app, 'Outer'); + const inner = new Stage(outer, 'Inner'); + const stack = new BogusStack(inner, 'Stack'); + + // WHEN + const appAsm = app.synth(); + const outerAsm = appAsm.getNestedAssembly(outer.artifactId); + const innerAsm = outerAsm.getNestedAssembly(inner.artifactId); + + test.ok(innerAsm.tryGetArtifact(stack.artifactId)); + + test.done(); + }, + + 'Default stack name in Stage objects incorporates the Stage name and no hash'(test: Test) { + // WHEN + const app = new App(); + const stage = new Stage(app, 'MyStage'); + const stack = new BogusStack(stage, 'MyStack'); + + // THEN + test.equal(stage.stageName, 'MyStage'); + test.equal(stack.stackName, 'MyStage-MyStack'); + + test.done(); + }, + + 'Can not have dependencies to stacks outside the nested asm'(test: Test) { + // GIVEN + const app = new App(); + const stack1 = new BogusStack(app, 'Stack1'); + const stage = new Stage(app, 'MyStage'); + const stack2 = new BogusStack(stage, 'Stack2'); + + // WHEN + test.throws(() => { + stack2.addDependency(stack1); + }, /dependency cannot cross stage boundaries/); + + test.done(); + }, + + 'When we synth() a stage, prepare must be called on constructs in the stage'(test: Test) { + // GIVEN + const app = new App(); + let prepared = false; + const stage = new Stage(app, 'MyStage'); + const stack = new BogusStack(stage, 'Stack'); + class HazPrepare extends Construct { + protected prepare() { + prepared = true; + } + } + new HazPrepare(stack, 'Preparable'); + + // WHEN + stage.synth(); + + // THEN + test.equals(prepared, true); + + test.done(); + }, + + 'When we synth() a stage, aspects inside it must have been applied'(test: Test) { + // GIVEN + const app = new App(); + const stage = new Stage(app, 'MyStage'); + const stack = new BogusStack(stage, 'Stack'); + + // WHEN + const aspect = new TouchingAspect(); + stack.node.applyAspect(aspect); + + // THEN + app.synth(); + test.deepEqual(aspect.visits.map(c => c.node.path), [ + 'MyStage/Stack', + 'MyStage/Stack/Resource', + ]); + + test.done(); + }, + + 'Aspects do not apply inside a Stage'(test: Test) { + // GIVEN + const app = new App(); + const stage = new Stage(app, 'MyStage'); + new BogusStack(stage, 'Stack'); + + // WHEN + const aspect = new TouchingAspect(); + app.node.applyAspect(aspect); + + // THEN + app.synth(); + test.deepEqual(aspect.visits.map(c => c.node.path), [ + '', + 'Tree', + ]); + test.done(); + }, + + 'Automatic dependencies inside a stage are available immediately after synth'(test: Test) { + // GIVEN + const app = new App(); + const stage = new Stage(app, 'MyStage'); + const stack1 = new Stack(stage, 'Stack1'); + const stack2 = new Stack(stage, 'Stack2'); + + // WHEN + const resource1 = new CfnResource(stack1, 'Resource', { + type: 'CDK::Test::Resource', + }); + new CfnResource(stack2, 'Resource', { + type: 'CDK::Test::Resource', + properties: { + OtherThing: resource1.ref, + }, + }); + + const asm = stage.synth(); + + // THEN + test.deepEqual( + asm.getStackArtifact(stack2.artifactId).dependencies.map(d => d.id), + [stack1.artifactId]); + + test.done(); + }, + + 'Assemblies can be deeply nested'(test: Test) { + // GIVEN + const app = new App({ runtimeInfo: false, treeMetadata: false }); + + const level1 = new Stage(app, 'StageLevel1'); + const level2 = new Stage(level1, 'StageLevel2'); + new Stage(level2, 'StageLevel3'); + + // WHEN + const rootAssembly = app.synth(); + + // THEN + test.deepEqual(rootAssembly.manifest.artifacts, { + 'assembly-StageLevel1': { + type: 'cdk:cloud-assembly', + properties: { + directoryName: 'assembly-StageLevel1', + displayName: 'StageLevel1', + }, + }, + }); + + const assemblyLevel1 = rootAssembly.getNestedAssembly('assembly-StageLevel1'); + test.deepEqual(assemblyLevel1.manifest.artifacts, { + 'assembly-StageLevel1-StageLevel2': { + type: 'cdk:cloud-assembly', + properties: { + directoryName: 'assembly-StageLevel1-StageLevel2', + displayName: 'StageLevel1/StageLevel2', + }, + }, + }); + + const assemblyLevel2 = assemblyLevel1.getNestedAssembly('assembly-StageLevel1-StageLevel2'); + test.deepEqual(assemblyLevel2.manifest.artifacts, { + 'assembly-StageLevel1-StageLevel2-StageLevel3': { + type: 'cdk:cloud-assembly', + properties: { + directoryName: 'assembly-StageLevel1-StageLevel2-StageLevel3', + displayName: 'StageLevel1/StageLevel2/StageLevel3', + }, + }, + }); + + test.done(); + }, + + 'stage name validation'(test: Test) { + const app = new App(); + + new Stage(app, 'abcd'); + new Stage(app, 'abcd123'); + new Stage(app, 'abcd123-588dfjjk'); + new Stage(app, 'abcd123-588dfjjk.sss'); + new Stage(app, 'abcd123-588dfjjk.sss_ajsid'); + + test.throws(() => new Stage(app, 'abcd123-588dfjjk.sss_ajsid '), /invalid stage name "abcd123-588dfjjk.sss_ajsid "/); + test.throws(() => new Stage(app, 'abcd123-588dfjjk.sss_ajsid/dfo'), /invalid stage name "abcd123-588dfjjk.sss_ajsid\/dfo"/); + test.throws(() => new Stage(app, '&'), /invalid stage name "&"/); + test.throws(() => new Stage(app, '45hello'), /invalid stage name "45hello"/); + test.throws(() => new Stage(app, 'f'), /invalid stage name "f"/); + + test.done(); + }, + + 'outdir cannot be specified for nested stages'(test: Test) { + // WHEN + const app = new App(); + + // THEN + test.throws(() => new Stage(app, 'mystage', { outdir: '/tmp/foo/bar' }), /"outdir" cannot be specified for nested stages/); + test.done(); + }, +}; + +class TouchingAspect implements IAspect { + public readonly visits = new Array(); + public visit(node: IConstruct): void { + this.visits.push(node); + } +} + +class BogusStack extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + new CfnResource(this, 'Resource', { + type: 'CDK::Test::Resource', + }); + } +} + +function acctRegion(s: Stack) { + return [s.account, s.region]; +} \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/design/NESTED_ASSEMBLIES.md b/packages/@aws-cdk/cx-api/design/NESTED_ASSEMBLIES.md new file mode 100644 index 0000000000000..c58caf6ee938b --- /dev/null +++ b/packages/@aws-cdk/cx-api/design/NESTED_ASSEMBLIES.md @@ -0,0 +1,93 @@ +# Nested Assemblies + +For the CI/CD project we need to be able to a final, authoritative, immutable +rendition of part of the construct tree. This is a part of the application +that we can ask the CI/CD system to deploy as a unit, and have it get a fighting +chance of getting it right. This is because: + +- The stacks will be known. +- Their interdependencies will be known, and won't change anymore. + +To that end, we're introducing the concept of an "nested cloud assembly". +This is a part of the construct tree that is finalized independently of the +rest, so that other constructs can reflect on it. + +Constructs of type `Stage` will produce nested cloud assemblies. + +## Restrictions + +### Assets + +Right now, if the same asset is used in multiple cloud assemblies, it will +be staged independently in ever Cloud Assembly (making it take up more +space than necessary). + +This is unfortunate. We can think about sharing the staging directories +between Stages, should be an easy optimization that can be applied later. + +### Dependencies + +It seems that it might be desirable to have dependencies that reach outside +a single `Stage`. Consider the case where we have shared resources that +may be shared between Stages. A typical example would be a VPC: + +``` + ┌───────────────┐ + │ │ + │ VpcStack │ + │ │ + └───────────────┘ + ▲ + │ + │ + ┌─────────────┴─────────────┐ + │ │ +┌───────────────┼──────────┐ ┌──────────┼───────────────┐ +│Stage │ │ │ │ Stage│ +│ │ │ │ │ │ +│ ┌───────────────┐ │ │ ┌───────────────┐ │ +│ │ │ │ │ │ │ │ +│ │ App1Stack │ │ │ │ App2Stack │ │ +│ │ │ │ │ │ │ │ +│ └───────────────┘ │ │ └───────────────┘ │ +│ │ │ │ +└──────────────────────────┘ └──────────────────────────┘ +``` + +This seems like a reasonable thing to want to be able to do. + + +Right now, for practical reasons we're disallowing dependencies outside +nested assemblies. That is not to say that this can never be made to work, +but as it's really rather a significant chunk of work it has not been +implemented yet. Things to consider: + +- Do artifact identifiers need to be globally unique? (Does that destroy + local assumptions around naming that constructs can make?) +- How are artifacts addressed across assembly boundaries? Are they just the + absolute name, wherever in the Cloud Assembly tree the artifact is? Do they + represent a path from the top-level cloud assembly + (`SubAsm/SubAsm/Artifact`)? Are they relative paths (`../SubAsm/Artifact`)? +- Can there be cyclic dependencies between nested assemblies? Is it okay to + have both dependencies `AsmA/Stack1 -> AsmB/Stack1`, and `AsmB/Stack2 -> + AsmA/Stack2`? Why, or why not? How will we ensure that? + +Even if we can make the addressing work at the artifact level, at the +construct tree level we'd be giving up the guarantees we are getting from +having `Stage` constructs produce isolated Cloud Assemblies by having +dependencies outside them. Consider having two stages, `StageA` with `StackA` +and `StageB` with `StackB`. We must `synth()` them in some order, either A or +B first. Let's say A goes first (but the same argument obviously holds in +reverse). What if during the `synth()` of `StageB`, we discover `StackB` +introduces a dependency on `StackA`? By that point, `StageA` has already +synthesized and `StackA` has produced a (so-called "immutable") template. +Obviously we can't change that anymore, so we can't introduce that dependency +anymore. + +Seems like we should be calling `synth()` on multiple stages consumer-first! + +The problem is that we are generally building a Pipeline *producer*-first, since +we are modeling and building it in deployment order, which is the reverse order +the pipeline would `synth()` each of the stages in, in order to build itself. + +Since this is all very tricky, let's consider it out of scope for now. \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/jest.config.js b/packages/@aws-cdk/cx-api/jest.config.js index cd664e1d069e5..d984ff822379b 100644 --- a/packages/@aws-cdk/cx-api/jest.config.js +++ b/packages/@aws-cdk/cx-api/jest.config.js @@ -1,2 +1,10 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); -module.exports = baseConfig; +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + ...baseConfig.coverageThreshold.global, + branches: 75, + }, + }, +}; diff --git a/packages/@aws-cdk/cx-api/lib/asset-manifest-artifact.ts b/packages/@aws-cdk/cx-api/lib/artifacts/asset-manifest-artifact.ts similarity index 90% rename from packages/@aws-cdk/cx-api/lib/asset-manifest-artifact.ts rename to packages/@aws-cdk/cx-api/lib/artifacts/asset-manifest-artifact.ts index 8146a276d7e4f..07414bafe4249 100644 --- a/packages/@aws-cdk/cx-api/lib/asset-manifest-artifact.ts +++ b/packages/@aws-cdk/cx-api/lib/artifacts/asset-manifest-artifact.ts @@ -1,7 +1,7 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as path from 'path'; -import { CloudArtifact } from './cloud-artifact'; -import { CloudAssembly } from './cloud-assembly'; +import { CloudArtifact } from '../cloud-artifact'; +import { CloudAssembly } from '../cloud-assembly'; /** * Asset manifest is a description of a set of assets which need to be built and published diff --git a/packages/@aws-cdk/cx-api/lib/cloudformation-artifact.ts b/packages/@aws-cdk/cx-api/lib/artifacts/cloudformation-artifact.ts similarity index 89% rename from packages/@aws-cdk/cx-api/lib/cloudformation-artifact.ts rename to packages/@aws-cdk/cx-api/lib/artifacts/cloudformation-artifact.ts index 2373e45e0eabc..e22bc5764a798 100644 --- a/packages/@aws-cdk/cx-api/lib/cloudformation-artifact.ts +++ b/packages/@aws-cdk/cx-api/lib/artifacts/cloudformation-artifact.ts @@ -1,16 +1,11 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as fs from 'fs'; import * as path from 'path'; -import { CloudArtifact } from './cloud-artifact'; -import { CloudAssembly } from './cloud-assembly'; -import { Environment, EnvironmentUtils } from './environment'; +import { CloudArtifact } from '../cloud-artifact'; +import { CloudAssembly } from '../cloud-assembly'; +import { Environment, EnvironmentUtils } from '../environment'; export class CloudFormationStackArtifact extends CloudArtifact { - /** - * The CloudFormation template for this stack. - */ - public readonly template: any; - /** * The file name of the template. */ @@ -87,6 +82,8 @@ export class CloudFormationStackArtifact extends CloudArtifact { */ public readonly terminationProtection?: boolean; + private _template: any | undefined; + constructor(assembly: CloudAssembly, artifactId: string, artifact: cxschema.ArtifactManifest) { super(assembly, artifactId, artifact); @@ -107,7 +104,6 @@ export class CloudFormationStackArtifact extends CloudArtifact { this.terminationProtection = properties.terminationProtection; this.stackName = properties.stackName || artifactId; - this.template = JSON.parse(fs.readFileSync(path.join(this.assembly.directory, this.templateFile), 'utf-8')); this.assets = this.findMetadataByType(cxschema.ArtifactMetadataEntryType.ASSET).map(e => e.data as cxschema.AssetMetadataEntry); this.displayName = this.stackName === artifactId @@ -117,4 +113,14 @@ export class CloudFormationStackArtifact extends CloudArtifact { this.name = this.stackName; // backwards compat this.originalName = this.stackName; } + + /** + * The CloudFormation template for this stack. + */ + public get template(): any { + if (this._template === undefined) { + this._template = JSON.parse(fs.readFileSync(path.join(this.assembly.directory, this.templateFile), 'utf-8')); + } + return this._template; + } } diff --git a/packages/@aws-cdk/cx-api/lib/artifacts/nested-cloud-assembly-artifact.ts b/packages/@aws-cdk/cx-api/lib/artifacts/nested-cloud-assembly-artifact.ts new file mode 100644 index 0000000000000..bf3e378774d96 --- /dev/null +++ b/packages/@aws-cdk/cx-api/lib/artifacts/nested-cloud-assembly-artifact.ts @@ -0,0 +1,49 @@ +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import * as path from 'path'; +import { CloudArtifact } from '../cloud-artifact'; +import { CloudAssembly } from '../cloud-assembly'; + +/** + * Asset manifest is a description of a set of assets which need to be built and published + */ +export class NestedCloudAssemblyArtifact extends CloudArtifact { + /** + * The relative directory name of the asset manifest + */ + public readonly directoryName: string; + + /** + * Display name + */ + public readonly displayName: string; + + /** + * Cache for the inner assembly loading + */ + private _nestedAssembly?: CloudAssembly; + + constructor(assembly: CloudAssembly, name: string, artifact: cxschema.ArtifactManifest) { + super(assembly, name, artifact); + + const properties = (this.manifest.properties || {}) as cxschema.NestedCloudAssemblyProperties; + this.directoryName = properties.directoryName; + this.displayName = properties.displayName ?? name; + } + + /** + * Full path to the nested assembly directory + */ + public get fullPath(): string { + return path.join(this.assembly.directory, this.directoryName); + } + + /** + * The nested Assembly + */ + public get nestedAssembly(): CloudAssembly { + if (!this._nestedAssembly) { + this._nestedAssembly = new CloudAssembly(this.fullPath); + } + return this._nestedAssembly; + } +} diff --git a/packages/@aws-cdk/cx-api/lib/tree-cloud-artifact.ts b/packages/@aws-cdk/cx-api/lib/artifacts/tree-cloud-artifact.ts similarity index 83% rename from packages/@aws-cdk/cx-api/lib/tree-cloud-artifact.ts rename to packages/@aws-cdk/cx-api/lib/artifacts/tree-cloud-artifact.ts index 142671e882e23..689f3468ca252 100644 --- a/packages/@aws-cdk/cx-api/lib/tree-cloud-artifact.ts +++ b/packages/@aws-cdk/cx-api/lib/artifacts/tree-cloud-artifact.ts @@ -1,6 +1,6 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; -import { CloudArtifact } from './cloud-artifact'; -import { CloudAssembly } from './cloud-assembly'; +import { CloudArtifact } from '../cloud-artifact'; +import { CloudAssembly } from '../cloud-assembly'; export class TreeCloudArtifact extends CloudArtifact { public readonly file: string; diff --git a/packages/@aws-cdk/cx-api/lib/cloud-artifact.ts b/packages/@aws-cdk/cx-api/lib/cloud-artifact.ts index 55cd7567e1612..9abfdb8d660d5 100644 --- a/packages/@aws-cdk/cx-api/lib/cloud-artifact.ts +++ b/packages/@aws-cdk/cx-api/lib/cloud-artifact.ts @@ -49,6 +49,8 @@ export class CloudArtifact { return new TreeCloudArtifact(assembly, id, artifact); case cxschema.ArtifactType.ASSET_MANIFEST: return new AssetManifestArtifact(assembly, id, artifact); + case cxschema.ArtifactType.NESTED_CLOUD_ASSEMBLY: + return new NestedCloudAssemblyArtifact(assembly, id, artifact); default: return undefined; } @@ -88,7 +90,7 @@ export class CloudArtifact { if (this._deps) { return this._deps; } this._deps = this._dependencyIDs.map(id => { - const dep = this.assembly.artifacts.find(a => a.id === id); + const dep = this.assembly.tryGetArtifact(id); if (!dep) { throw new Error(`Artifact ${this.id} depends on non-existing artifact ${id}`); } @@ -143,6 +145,7 @@ export class CloudArtifact { } // needs to be defined at the end to avoid a cyclic dependency -import { AssetManifestArtifact } from './asset-manifest-artifact'; -import { CloudFormationStackArtifact } from './cloudformation-artifact'; -import { TreeCloudArtifact } from './tree-cloud-artifact'; \ No newline at end of file +import { AssetManifestArtifact } from './artifacts/asset-manifest-artifact'; +import { CloudFormationStackArtifact } from './artifacts/cloudformation-artifact'; +import { NestedCloudAssemblyArtifact } from './artifacts/nested-cloud-assembly-artifact'; +import { TreeCloudArtifact } from './artifacts/tree-cloud-artifact'; \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/lib/cloud-assembly.ts b/packages/@aws-cdk/cx-api/lib/cloud-assembly.ts index 0cf2e3d2ea9e0..b12c8a52ccdb6 100644 --- a/packages/@aws-cdk/cx-api/lib/cloud-assembly.ts +++ b/packages/@aws-cdk/cx-api/lib/cloud-assembly.ts @@ -2,10 +2,11 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; +import { CloudFormationStackArtifact } from './artifacts/cloudformation-artifact'; +import { NestedCloudAssemblyArtifact } from './artifacts/nested-cloud-assembly-artifact'; +import { TreeCloudArtifact } from './artifacts/tree-cloud-artifact'; import { CloudArtifact } from './cloud-artifact'; -import { CloudFormationStackArtifact } from './cloudformation-artifact'; import { topologicalSort } from './toposort'; -import { TreeCloudArtifact } from './tree-cloud-artifact'; /** * The name of the root manifest file of the assembly. @@ -69,6 +70,8 @@ export class CloudAssembly { /** * Returns a CloudFormation stack artifact from this assembly. * + * Will only search the current assembly. + * * @param stackName the name of the CloudFormation stack. * @throws if there is no stack artifact by that name * @throws if there is more than one stack with the same stack name. You can @@ -116,6 +119,33 @@ export class CloudAssembly { return artifact; } + /** + * Returns a nested assembly artifact. + * + * @param artifactId The artifact ID of the nested assembly + */ + public getNestedAssemblyArtifact(artifactId: string): NestedCloudAssemblyArtifact { + const artifact = this.tryGetArtifact(artifactId); + if (!artifact) { + throw new Error(`Unable to find artifact with id "${artifactId}"`); + } + + if (!(artifact instanceof NestedCloudAssemblyArtifact)) { + throw new Error(`Found artifact '${artifactId}' but it's not a nested cloud assembly`); + } + + return artifact; + } + + /** + * Returns a nested assembly. + * + * @param artifactId The artifact ID of the nested assembly + */ + public getNestedAssembly(artifactId: string): CloudAssembly { + return this.getNestedAssemblyArtifact(artifactId).nestedAssembly; + } + /** * Returns the tree metadata artifact from this assembly. * @throws if there is no metadata artifact by that name @@ -186,7 +216,7 @@ export class CloudAssemblyBuilder { * @param outdir The output directory, uses temporary directory if undefined */ constructor(outdir?: string) { - this.outdir = outdir || fs.mkdtempSync(path.join(os.tmpdir(), 'cdk.out')); + this.outdir = determineOutputDirectory(outdir); // we leverage the fact that outdir is long-lived to avoid staging assets into it // that were already staged (copying can be expensive). this is achieved by the fact @@ -198,7 +228,7 @@ export class CloudAssemblyBuilder { throw new Error(`${this.outdir} must be a directory`); } } else { - fs.mkdirSync(this.outdir); + fs.mkdirSync(this.outdir, { recursive: true }); } } @@ -251,6 +281,23 @@ export class CloudAssemblyBuilder { return new CloudAssembly(this.outdir); } + /** + * Creates a nested cloud assembly + */ + public createNestedAssembly(artifactId: string, displayName: string) { + const directoryName = artifactId; + const innerAsmDir = path.join(this.outdir, directoryName); + + this.addArtifact(artifactId, { + type: cxschema.ArtifactType.NESTED_CLOUD_ASSEMBLY, + properties: { + directoryName, + displayName, + } as cxschema.NestedCloudAssemblyProperties, + }); + + return new CloudAssemblyBuilder(innerAsmDir); + } } /** @@ -341,3 +388,10 @@ function filterUndefined(obj: any): any { function ignore(_x: any) { return; } + +/** + * Turn the given optional output directory into a fixed output directory + */ +function determineOutputDirectory(outdir?: string) { + return outdir ?? fs.mkdtempSync(path.join(os.tmpdir(), 'cdk.out')); +} diff --git a/packages/@aws-cdk/cx-api/lib/index.ts b/packages/@aws-cdk/cx-api/lib/index.ts index 916ee80b068d4..a6ac4977a6d17 100644 --- a/packages/@aws-cdk/cx-api/lib/index.ts +++ b/packages/@aws-cdk/cx-api/lib/index.ts @@ -4,9 +4,10 @@ export * from './context/ami'; export * from './context/availability-zones'; export * from './context/endpoint-service-availability-zones'; export * from './cloud-artifact'; -export * from './asset-manifest-artifact'; -export * from './cloudformation-artifact'; -export * from './tree-cloud-artifact'; +export * from './artifacts/asset-manifest-artifact'; +export * from './artifacts/cloudformation-artifact'; +export * from './artifacts/tree-cloud-artifact'; +export * from './artifacts/nested-cloud-assembly-artifact'; export * from './cloud-assembly'; export * from './assets'; export * from './environment'; diff --git a/packages/@aws-cdk/cx-api/test/cloud-assembly-builder.test.ts b/packages/@aws-cdk/cx-api/test/cloud-assembly-builder.test.ts index bc348d9442188..1512c86ff5044 100644 --- a/packages/@aws-cdk/cx-api/test/cloud-assembly-builder.test.ts +++ b/packages/@aws-cdk/cx-api/test/cloud-assembly-builder.test.ts @@ -2,12 +2,12 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { CloudAssemblyBuilder } from '../lib'; +import * as cxapi from '../lib'; test('cloud assembly builder', () => { // GIVEN const outdir = fs.mkdtempSync(path.join(os.tmpdir(), 'cloud-assembly-builder-tests')); - const session = new CloudAssemblyBuilder(outdir); + const session = new cxapi.CloudAssemblyBuilder(outdir); const templateFile = 'foo.template.json'; // WHEN @@ -121,12 +121,12 @@ test('cloud assembly builder', () => { }); test('outdir must be a directory', () => { - expect(() => new CloudAssemblyBuilder(__filename)).toThrow('must be a directory'); + expect(() => new cxapi.CloudAssemblyBuilder(__filename)).toThrow('must be a directory'); }); test('duplicate missing values with the same key are only reported once', () => { const outdir = fs.mkdtempSync(path.join(os.tmpdir(), 'cloud-assembly-builder-tests')); - const session = new CloudAssemblyBuilder(outdir); + const session = new cxapi.CloudAssemblyBuilder(outdir); const props: cxschema.ContextQueryProperties = { account: '1234', @@ -141,3 +141,29 @@ test('duplicate missing values with the same key are only reported once', () => expect(assembly.manifest.missing!.length).toEqual(1); }); + +test('write and read nested cloud assembly artifact', () => { + // GIVEN + const outdir = fs.mkdtempSync(path.join(os.tmpdir(), 'cloud-assembly-builder-tests')); + const session = new cxapi.CloudAssemblyBuilder(outdir); + + const innerAsmDir = path.join(outdir, 'hello'); + new cxapi.CloudAssemblyBuilder(innerAsmDir).buildAssembly(); + + // WHEN + session.addArtifact('Assembly', { + type: cxschema.ArtifactType.NESTED_CLOUD_ASSEMBLY, + properties: { + directoryName: 'hello', + } as cxschema.NestedCloudAssemblyProperties, + }); + const asm = session.buildAssembly(); + + // THEN + const art = asm.tryGetArtifact('Assembly') as cxapi.NestedCloudAssemblyArtifact | undefined; + expect(art).toBeInstanceOf(cxapi.NestedCloudAssemblyArtifact); + expect(art?.fullPath).toEqual(path.join(outdir, 'hello')); + + const nested = art?.nestedAssembly; + expect(nested?.artifacts.length).toEqual(0); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/test/placeholders.test.ts b/packages/@aws-cdk/cx-api/test/placeholders.test.ts index 9b39478abd611..658d8a4670433 100644 --- a/packages/@aws-cdk/cx-api/test/placeholders.test.ts +++ b/packages/@aws-cdk/cx-api/test/placeholders.test.ts @@ -1,4 +1,4 @@ -import { EnvironmentPlaceholders, IEnvironmentPlaceholderProvider } from '../lib'; +import { EnvironmentPlaceholders, EnvironmentPlaceholderValues, IEnvironmentPlaceholderProvider } from '../lib'; test('complex placeholder substitution', async () => { const replacer: IEnvironmentPlaceholderProvider = { @@ -25,3 +25,30 @@ test('complex placeholder substitution', async () => { }, }); }); + +test('sync placeholder substitution', () => { + const replacer: EnvironmentPlaceholderValues = { + accountId: 'current_account', + region: 'current_region', + partition: 'current_partition', + }; + + expect(EnvironmentPlaceholders.replace({ + destinations: { + theDestination: { + assumeRoleArn: 'arn:${AWS::Partition}:role-${AWS::AccountId}', + bucketName: 'some_bucket-${AWS::AccountId}-${AWS::Region}', + objectKey: 'some_key-${AWS::AccountId}-${AWS::Region}', + }, + }, + }, replacer)).toEqual({ + destinations: { + theDestination: { + assumeRoleArn: 'arn:current_partition:role-current_account', + bucketName: 'some_bucket-current_account-current_region', + objectKey: 'some_key-current_account-current_region', + }, + }, + }); + +}); From 32df7997b4c5d0761b09f3a27517c08c5244b14a Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2020 09:47:44 +0000 Subject: [PATCH 15/32] chore(deps): bump aws-sdk from 2.689.0 to 2.691.0 (#8419) Bumps [aws-sdk](https://github.com/aws/aws-sdk-js) from 2.689.0 to 2.691.0. - [Release notes](https://github.com/aws/aws-sdk-js/releases) - [Changelog](https://github.com/aws/aws-sdk-js/blob/master/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js/compare/v2.689.0...v2.691.0) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- packages/@aws-cdk/aws-cloudfront/package.json | 2 +- packages/@aws-cdk/aws-cloudtrail/package.json | 2 +- packages/@aws-cdk/aws-codebuild/package.json | 2 +- packages/@aws-cdk/aws-codecommit/package.json | 2 +- packages/@aws-cdk/aws-dynamodb/package.json | 2 +- packages/@aws-cdk/aws-eks/package.json | 2 +- packages/@aws-cdk/aws-events-targets/package.json | 2 +- packages/@aws-cdk/aws-lambda/package.json | 2 +- packages/@aws-cdk/aws-route53/package.json | 2 +- packages/@aws-cdk/aws-sqs/package.json | 2 +- packages/@aws-cdk/custom-resources/package.json | 2 +- packages/aws-cdk/package.json | 2 +- packages/cdk-assets/package.json | 2 +- yarn.lock | 8 ++++---- 14 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudfront/package.json b/packages/@aws-cdk/aws-cloudfront/package.json index 2384382bc90b2..2fafc2b6653ee 100644 --- a/packages/@aws-cdk/aws-cloudfront/package.json +++ b/packages/@aws-cdk/aws-cloudfront/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.689.0", + "aws-sdk": "^2.691.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudtrail/package.json b/packages/@aws-cdk/aws-cloudtrail/package.json index 2ed0d52f9378a..e0ee07263ef09 100644 --- a/packages/@aws-cdk/aws-cloudtrail/package.json +++ b/packages/@aws-cdk/aws-cloudtrail/package.json @@ -64,7 +64,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "aws-sdk": "^2.689.0", + "aws-sdk": "^2.691.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codebuild/package.json b/packages/@aws-cdk/aws-codebuild/package.json index f380c06de5364..45ddf7bbf51f0 100644 --- a/packages/@aws-cdk/aws-codebuild/package.json +++ b/packages/@aws-cdk/aws-codebuild/package.json @@ -70,7 +70,7 @@ "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.689.0", + "aws-sdk": "^2.691.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codecommit/package.json b/packages/@aws-cdk/aws-codecommit/package.json index 3290ef3d3f408..01da8326c2cfe 100644 --- a/packages/@aws-cdk/aws-codecommit/package.json +++ b/packages/@aws-cdk/aws-codecommit/package.json @@ -70,7 +70,7 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.689.0", + "aws-sdk": "^2.691.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-dynamodb/package.json b/packages/@aws-cdk/aws-dynamodb/package.json index 65075f253bcff..00d8eb67b9398 100644 --- a/packages/@aws-cdk/aws-dynamodb/package.json +++ b/packages/@aws-cdk/aws-dynamodb/package.json @@ -65,7 +65,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/jest": "^25.2.3", - "aws-sdk": "^2.689.0", + "aws-sdk": "^2.691.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index 2aa311168dc6a..3fc137fce439f 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.689.0", + "aws-sdk": "^2.691.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-events-targets/package.json b/packages/@aws-cdk/aws-events-targets/package.json index 4bdff4663018d..b9f9efad57c95 100644 --- a/packages/@aws-cdk/aws-events-targets/package.json +++ b/packages/@aws-cdk/aws-events-targets/package.json @@ -68,7 +68,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-codecommit": "0.0.0", - "aws-sdk": "^2.689.0", + "aws-sdk": "^2.691.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index e6bcad26594e9..0bba808919445 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -71,7 +71,7 @@ "@types/lodash": "^4.14.155", "@types/nodeunit": "^0.0.31", "@types/sinon": "^9.0.4", - "aws-sdk": "^2.689.0", + "aws-sdk": "^2.691.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-route53/package.json b/packages/@aws-cdk/aws-route53/package.json index c99f3de12f7a1..55bc1fec26d11 100644 --- a/packages/@aws-cdk/aws-route53/package.json +++ b/packages/@aws-cdk/aws-route53/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.689.0", + "aws-sdk": "^2.691.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-sqs/package.json b/packages/@aws-cdk/aws-sqs/package.json index ef28cad08a9ee..7436e4722811b 100644 --- a/packages/@aws-cdk/aws-sqs/package.json +++ b/packages/@aws-cdk/aws-sqs/package.json @@ -65,7 +65,7 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.689.0", + "aws-sdk": "^2.691.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/custom-resources/package.json b/packages/@aws-cdk/custom-resources/package.json index 82fba93c7691a..7cd16b421cb1b 100644 --- a/packages/@aws-cdk/custom-resources/package.json +++ b/packages/@aws-cdk/custom-resources/package.json @@ -74,7 +74,7 @@ "@types/aws-lambda": "^8.10.39", "@types/fs-extra": "^8.1.0", "@types/sinon": "^9.0.4", - "aws-sdk": "^2.689.0", + "aws-sdk": "^2.691.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index cec9c904d18d9..488b57c0d0a13 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -71,7 +71,7 @@ "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/region-info": "0.0.0", "archiver": "^4.0.1", - "aws-sdk": "^2.689.0", + "aws-sdk": "^2.691.0", "camelcase": "^6.0.0", "cdk-assets": "0.0.0", "colors": "^1.4.0", diff --git a/packages/cdk-assets/package.json b/packages/cdk-assets/package.json index 043b55c56f2bd..eb67aa7d3b6f8 100644 --- a/packages/cdk-assets/package.json +++ b/packages/cdk-assets/package.json @@ -48,7 +48,7 @@ "@aws-cdk/cdk-assets-schema": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "archiver": "^4.0.1", - "aws-sdk": "^2.689.0", + "aws-sdk": "^2.691.0", "glob": "^7.1.6", "yargs": "^15.3.1" }, diff --git a/yarn.lock b/yarn.lock index 104cf74c0ae1c..17bee28e90a05 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2134,10 +2134,10 @@ aws-sdk-mock@^5.1.0: sinon "^9.0.1" traverse "^0.6.6" -aws-sdk@^2.637.0, aws-sdk@^2.689.0: - version "2.689.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.689.0.tgz#f8833031afd773bfc9503f8d6325186a985d019c" - integrity sha512-l9kbgZtIbR9dux4JHoxZ3vDWAfGtp34KpDDf5cwYHC5jDTTJoe6XhBBlEDSruwKh1+5DONpSZWNVhDZ6E02ojg== +aws-sdk@^2.637.0, aws-sdk@^2.691.0: + version "2.691.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.691.0.tgz#92361b63117e94d065dad2f215296f5a19fe0c70" + integrity sha512-HV/iANH5PJvexubWr/oDmWMKtV/n1shtrACrLIUa5vTXIT6O7CzUouExNOvOtFMZw8zJkLmyEpa/0bDpMmo0Zg== dependencies: buffer "4.9.2" events "1.1.1" From 8fc37513477f4d9a8a37e4b6979a79e8ba6a1efd Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Mon, 8 Jun 2020 11:36:08 +0100 Subject: [PATCH 16/32] fix(apigateway): methodArn not replacing path parameters with asterisks (#8206) Path parameters in API Gateway allows for paths to contain the resource id, such as `/pets/{petId}/comments/{commentId}`. When generating the ARN for a Method to this Resource, the path parameters should be placed with asterisks, such as `/pets/*/comments/*`. fixes #8036 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-apigateway/lib/method.ts | 8 +++- ...pi.latebound-deploymentstage.expected.json | 4 +- .../test/integ.restapi.books.expected.json | 8 ++-- .../aws-apigateway/test/test.method.ts | 46 +++++++++++++++++++ ...nteg.api-gateway-domain-name.expected.json | 4 +- .../test/__snapshots__/synth.test.js.snap | 4 +- 6 files changed, 62 insertions(+), 12 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/method.ts b/packages/@aws-cdk/aws-apigateway/lib/method.ts index baafd1be53242..58c504ab4aa8a 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/method.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/method.ts @@ -236,7 +236,7 @@ export class Method extends Resource { } const stage = this.restApi.deploymentStage.stageName.toString(); - return this.restApi.arnForExecuteApi(this.httpMethod, this.resource.path, stage); + return this.restApi.arnForExecuteApi(this.httpMethod, pathForArn(this.resource.path), stage); } /** @@ -244,7 +244,7 @@ export class Method extends Resource { * This stage is used by the AWS Console UI when testing the method. */ public get testMethodArn(): string { - return this.restApi.arnForExecuteApi(this.httpMethod, this.resource.path, 'test-invoke-stage'); + return this.restApi.arnForExecuteApi(this.httpMethod, pathForArn(this.resource.path), 'test-invoke-stage'); } private renderIntegration(integration?: Integration): CfnMethod.IntegrationProperty { @@ -380,3 +380,7 @@ export enum AuthorizationType { */ COGNITO = 'COGNITO_USER_POOLS', } + +function pathForArn(path: string): string { + return path.replace(/\{[^\}]*\}/g, '*'); // replace path parameters (like '{bookId}') with asterisk +} diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.lambda-api.latebound-deploymentstage.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.lambda-api.latebound-deploymentstage.expected.json index eda41e36751f6..17dd7ccf222e8 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.lambda-api.latebound-deploymentstage.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.lambda-api.latebound-deploymentstage.expected.json @@ -151,7 +151,7 @@ { "Ref": "stage0661E4AC" }, - "/*/{proxy+}" + "/*/*" ] ] } @@ -188,7 +188,7 @@ { "Ref": "lambdarestapiF559E4F2" }, - "/test-invoke-stage/*/{proxy+}" + "/test-invoke-stage/*/*" ] ] } diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json index 8b679bd6c6239..91af30b6ef8d4 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json @@ -650,7 +650,7 @@ { "Ref": "booksapiDeploymentStageprod55D8E03E" }, - "/GET/books/{book_id}" + "/GET/books/*" ] ] } @@ -687,7 +687,7 @@ { "Ref": "booksapiE1885304" }, - "/test-invoke-stage/GET/books/{book_id}" + "/test-invoke-stage/GET/books/*" ] ] } @@ -768,7 +768,7 @@ { "Ref": "booksapiDeploymentStageprod55D8E03E" }, - "/DELETE/books/{book_id}" + "/DELETE/books/*" ] ] } @@ -805,7 +805,7 @@ { "Ref": "booksapiE1885304" }, - "/test-invoke-stage/DELETE/books/{book_id}" + "/test-invoke-stage/DELETE/books/*" ] ] } diff --git a/packages/@aws-cdk/aws-apigateway/test/test.method.ts b/packages/@aws-cdk/aws-apigateway/test/test.method.ts index b31333617dde7..e4383ecf768ac 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.method.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.method.ts @@ -220,6 +220,52 @@ export = { test.done(); }, + '"methodArn" and "testMethodArn" replace path parameters with asterisks'(test: Test) { + const stack = new cdk.Stack(); + const api = new apigw.RestApi(stack, 'test-api'); + const petId = api.root.addResource('pets').addResource('{petId}'); + const commentId = petId.addResource('comments').addResource('{commentId}'); + const method = commentId.addMethod('GET'); + + test.deepEqual(stack.resolve(method.methodArn), { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':execute-api:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':', + { Ref: 'testapiD6451F70' }, + '/', + { Ref: 'testapiDeploymentStageprod5C9E92A4' }, + '/GET/pets/*/comments/*', + ], + ], + }); + + test.deepEqual(stack.resolve(method.testMethodArn), { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':execute-api:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':', + { Ref: 'testapiD6451F70' }, + '/test-invoke-stage/GET/pets/*/comments/*', + ], + ], + }); + + test.done(); + }, + 'integration "credentialsRole" can be used to assume a role when calling backend'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-route53-targets/test/integ.api-gateway-domain-name.expected.json b/packages/@aws-cdk/aws-route53-targets/test/integ.api-gateway-domain-name.expected.json index c2b0ab49b975a..098e7e113b58c 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/integ.api-gateway-domain-name.expected.json +++ b/packages/@aws-cdk/aws-route53-targets/test/integ.api-gateway-domain-name.expected.json @@ -177,7 +177,7 @@ { "Ref": "apiDeploymentStageprod896C8101" }, - "/*/{proxy+}" + "/*/*" ] ] } @@ -214,7 +214,7 @@ { "Ref": "apiC8550315" }, - "/test-invoke-stage/*/{proxy+}" + "/test-invoke-stage/*/*" ] ] } diff --git a/packages/decdk/test/__snapshots__/synth.test.js.snap b/packages/decdk/test/__snapshots__/synth.test.js.snap index 37e846ae9053c..69a486c67530d 100644 --- a/packages/decdk/test/__snapshots__/synth.test.js.snap +++ b/packages/decdk/test/__snapshots__/synth.test.js.snap @@ -480,7 +480,7 @@ Object { Object { "Ref": "MyApi49610EDF", }, - "/test-invoke-stage/*/{proxy+}", + "/test-invoke-stage/*/*", ], ], }, @@ -521,7 +521,7 @@ Object { Object { "Ref": "MyApiDeploymentStageprodE1054AF0", }, - "/*/{proxy+}", + "/*/*", ], ], }, From 5c3a739ff39916f00281a1849a8fb0c0fda87807 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 8 Jun 2020 13:22:48 +0200 Subject: [PATCH 17/32] docs(ecs): fix an inline code sample in ECS (#8426) Was missing arguments to `addTargets()`. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-ecs/lib/base/base-service.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index ba6aa7451cdf2..6e37c99b2250a 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -411,10 +411,13 @@ export abstract class BaseService extends Resource * * @example * - * listener.addTargets(service.loadBalancerTarget({ - * containerName: 'MyContainer', - * containerPort: 1234 - * })); + * listener.addTargets('ECS', { + * port: 80, + * targets: [service.loadBalancerTarget({ + * containerName: 'MyContainer', + * containerPort: 1234, + * })], + * }); */ public loadBalancerTarget(options: LoadBalancerTargetOptions): IEcsLoadBalancerTarget { const self = this; From 1fabd9819d4dbe64d175e73400078e435235d1d2 Mon Sep 17 00:00:00 2001 From: Arnulfo Solis Ramirez Date: Mon, 8 Jun 2020 14:24:16 +0200 Subject: [PATCH 18/32] feat(cognito): allow mutable attributes for requiredAttributes (#7754) I've taken the liberty to implement a preview, refer to https://github.com/aws/aws-cdk/issues/7752 Any feedback is welcome! BREAKING CHANGE: `requiredAttributes` on `UserPool` construct is now replaced with `standardAttributes` with a slightly modified signature. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-cognito/README.md | 16 ++- .../aws-cognito/lib/user-pool-attr.ts | 130 +++++++++++------- .../@aws-cdk/aws-cognito/lib/user-pool.ts | 104 +++++++------- ...teg.user-pool-explicit-props.expected.json | 12 +- .../test/integ.user-pool-explicit-props.ts | 13 +- .../aws-cognito/test/user-pool-attr.test.ts | 2 +- .../aws-cognito/test/user-pool.test.ts | 112 +++++++++++++-- 7 files changed, 251 insertions(+), 138 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 8ee0f57c7db5a..2cca87a32e726 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -162,15 +162,21 @@ attributes. Besides these, additional attributes can be further defined, and are Learn more on [attributes in Cognito's documentation](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html). -The following code sample configures a user pool with two standard attributes (name and address) as required, and adds -four optional attributes. +The following code configures a user pool with two standard attributes (name and address) as required and mutable, and adds +four custom attributes. ```ts new UserPool(this, 'myuserpool', { // ... - requiredAttributes: { - fullname: true, - address: true, + standardAttributes: { + fullname: { + required: true, + mutable: false, + }, + address: { + required: false, + mutable: true, + }, }, customAttributes: { 'myappid': new StringAttribute({ minLen: 5, maxLen: 15, mutable: false }), diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts index c6fde417d1e4e..60c011fd9a71b 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts @@ -1,112 +1,136 @@ import { Token } from '@aws-cdk/core'; /** - * The set of standard attributes that can be marked as required. + * The set of standard attributes that can be marked as required or mutable. * * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#cognito-user-pools-standard-attributes */ -export interface RequiredAttributes { +export interface StandardAttributes { /** - * Whether the user's postal address is a required attribute. - * @default false + * The user's postal address. + * @default - see the defaults under `StandardAttribute` */ - readonly address?: boolean; + readonly address?: StandardAttribute; /** - * Whether the user's birthday, represented as an ISO 8601:2004 format, is a required attribute. - * @default false + * The user's birthday, represented as an ISO 8601:2004 format. + * @default - see the defaults under `StandardAttribute` */ - readonly birthdate?: boolean; + readonly birthdate?: StandardAttribute; /** - * Whether the user's e-mail address, represented as an RFC 5322 [RFC5322] addr-spec, is a required attribute. - * @default false + * The user's e-mail address, represented as an RFC 5322 [RFC5322] addr-spec. + * @default - see the defaults under `StandardAttribute` */ - readonly email?: boolean; + readonly email?: StandardAttribute; /** - * Whether the surname or last name of the user is a required attribute. - * @default false + * The surname or last name of the user. + * @default - see the defaults under `StandardAttribute` */ - readonly familyName?: boolean; + readonly familyName?: StandardAttribute; /** - * Whether the user's gender is a required attribute. - * @default false + * The user's gender. + * @default - see the defaults under `StandardAttribute` */ - readonly gender?: boolean; + readonly gender?: StandardAttribute; /** - * Whether the user's first name or give name is a required attribute. - * @default false + * The user's first name or give name. + * @default - see the defaults under `StandardAttribute` */ - readonly givenName?: boolean; + readonly givenName?: StandardAttribute; /** - * Whether the user's locale, represented as a BCP47 [RFC5646] language tag, is a required attribute. - * @default false + * The user's locale, represented as a BCP47 [RFC5646] language tag. + * @default - see the defaults under `StandardAttribute` */ - readonly locale?: boolean; + readonly locale?: StandardAttribute; /** - * Whether the user's middle name is a required attribute. - * @default false + * The user's middle name. + * @default - see the defaults under `StandardAttribute` */ - readonly middleName?: boolean; + readonly middleName?: StandardAttribute; /** - * Whether user's full name in displayable form, including all name parts, titles and suffixes, is a required attibute. - * @default false + * The user's full name in displayable form, including all name parts, titles and suffixes. + * @default - see the defaults under `StandardAttribute` */ - readonly fullname?: boolean; + readonly fullname?: StandardAttribute; /** - * Whether the user's nickname or casual name is a required attribute. - * @default false + * The user's nickname or casual name. + * @default - see the defaults under `StandardAttribute` */ - readonly nickname?: boolean; + readonly nickname?: StandardAttribute; /** - * Whether the user's telephone number is a required attribute. - * @default false + * The user's telephone number. + * @default - see the defaults under `StandardAttribute` */ - readonly phoneNumber?: boolean; + readonly phoneNumber?: StandardAttribute; /** - * Whether the URL to the user's profile picture is a required attribute. - * @default false + * The URL to the user's profile picture. + * @default - see the defaults under `StandardAttribute` */ - readonly profilePicture?: boolean; + readonly profilePicture?: StandardAttribute; /** - * Whether the user's preffered username, different from the immutable user name, is a required attribute. - * @default false + * The user's preffered username, different from the immutable user name. + * @default - see the defaults under `StandardAttribute` */ - readonly preferredUsername?: boolean; + readonly preferredUsername?: StandardAttribute; /** - * Whether the URL to the user's profile page is a required attribute. - * @default false + * The URL to the user's profile page. + * @default - see the defaults under `StandardAttribute` */ - readonly profilePage?: boolean; + readonly profilePage?: StandardAttribute; /** - * Whether the user's time zone is a required attribute. - * @default false + * The user's time zone. + * @default - see the defaults under `StandardAttribute` */ - readonly timezone?: boolean; + readonly timezone?: StandardAttribute; /** - * Whether the time, the user's information was last updated, is a required attribute. - * @default false + * The time, the user's information was last updated. + * @default - see the defaults under `StandardAttribute` */ - readonly lastUpdateTime?: boolean; + readonly lastUpdateTime?: StandardAttribute; /** - * Whether the URL to the user's web page or blog is a required attribute. + * The URL to the user's web page or blog. + * @default - see the defaults under `StandardAttribute` + */ + readonly website?: StandardAttribute; +} + +/** + * Standard attribute that can be marked as required or mutable. + * + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#cognito-user-pools-standard-attributes + */ +export interface StandardAttribute { + /** + * Specifies whether the value of the attribute can be changed. + * For any user pool attribute that's mapped to an identity provider attribute, this must be set to `true`. + * Amazon Cognito updates mapped attributes when users sign in to your application through an identity provider. + * If an attribute is immutable, Amazon Cognito throws an error when it attempts to update the attribute. + * + * @default true + */ + readonly mutable?: boolean; + /** + * Specifies whether the attribute is required upon user registration. + * If the attribute is required and the user does not provide a value, registration or sign-in will fail. + * * @default false */ - readonly website?: boolean; + readonly required?: boolean; } /** @@ -152,7 +176,7 @@ export interface CustomAttributeConfig { * * @default false */ - readonly mutable?: boolean + readonly mutable?: boolean; } /** diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index a0f25c13cd58b..4f7b29a22e325 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -2,7 +2,7 @@ import { IRole, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from ' import * as lambda from '@aws-cdk/aws-lambda'; import { Construct, Duration, IResource, Lazy, Resource, Stack, Token } from '@aws-cdk/core'; import { CfnUserPool } from './cognito.generated'; -import { ICustomAttribute, RequiredAttributes } from './user-pool-attr'; +import { ICustomAttribute, StandardAttribute, StandardAttributes } from './user-pool-attr'; import { UserPoolClient, UserPoolClientOptions } from './user-pool-client'; import { UserPoolDomain, UserPoolDomainOptions } from './user-pool-domain'; import { IUserPoolIdentityProvider } from './user-pool-idp'; @@ -457,9 +457,9 @@ export interface UserPoolProps { * The set of attributes that are required for every user in the user pool. * Read more on attributes here - https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html * - * @default - No attributes are required. + * @default - All standard attributes are optional and mutable. */ - readonly requiredAttributes?: RequiredAttributes; + readonly standardAttributes?: StandardAttributes; /** * Define a set of custom attributes that can be configured for each user in the user pool. @@ -762,24 +762,24 @@ export class UserPool extends UserPoolBase { if (signIn.username) { aliasAttrs = []; - if (signIn.email) { aliasAttrs.push(StandardAttribute.EMAIL); } - if (signIn.phone) { aliasAttrs.push(StandardAttribute.PHONE_NUMBER); } - if (signIn.preferredUsername) { aliasAttrs.push(StandardAttribute.PREFERRED_USERNAME); } + if (signIn.email) { aliasAttrs.push(StandardAttributeNames.email); } + if (signIn.phone) { aliasAttrs.push(StandardAttributeNames.phoneNumber); } + if (signIn.preferredUsername) { aliasAttrs.push(StandardAttributeNames.preferredUsername); } if (aliasAttrs.length === 0) { aliasAttrs = undefined; } } else { usernameAttrs = []; - if (signIn.email) { usernameAttrs.push(StandardAttribute.EMAIL); } - if (signIn.phone) { usernameAttrs.push(StandardAttribute.PHONE_NUMBER); } + if (signIn.email) { usernameAttrs.push(StandardAttributeNames.email); } + if (signIn.phone) { usernameAttrs.push(StandardAttributeNames.phoneNumber); } } if (props.autoVerify) { autoVerifyAttrs = []; - if (props.autoVerify.email) { autoVerifyAttrs.push(StandardAttribute.EMAIL); } - if (props.autoVerify.phone) { autoVerifyAttrs.push(StandardAttribute.PHONE_NUMBER); } + if (props.autoVerify.email) { autoVerifyAttrs.push(StandardAttributeNames.email); } + if (props.autoVerify.phone) { autoVerifyAttrs.push(StandardAttributeNames.phoneNumber); } } else if (signIn.email || signIn.phone) { autoVerifyAttrs = []; - if (signIn.email) { autoVerifyAttrs.push(StandardAttribute.EMAIL); } - if (signIn.phone) { autoVerifyAttrs.push(StandardAttribute.PHONE_NUMBER); } + if (signIn.email) { autoVerifyAttrs.push(StandardAttributeNames.email); } + if (signIn.phone) { autoVerifyAttrs.push(StandardAttributeNames.phoneNumber); } } return { usernameAttrs, aliasAttrs, autoVerifyAttrs }; @@ -863,30 +863,16 @@ export class UserPool extends UserPoolBase { private schemaConfiguration(props: UserPoolProps): CfnUserPool.SchemaAttributeProperty[] | undefined { const schema: CfnUserPool.SchemaAttributeProperty[] = []; - if (props.requiredAttributes) { - const stdAttributes: StandardAttribute[] = []; - - if (props.requiredAttributes.address) { stdAttributes.push(StandardAttribute.ADDRESS); } - if (props.requiredAttributes.birthdate) { stdAttributes.push(StandardAttribute.BIRTHDATE); } - if (props.requiredAttributes.email) { stdAttributes.push(StandardAttribute.EMAIL); } - if (props.requiredAttributes.familyName) { stdAttributes.push(StandardAttribute.FAMILY_NAME); } - if (props.requiredAttributes.fullname) { stdAttributes.push(StandardAttribute.NAME); } - if (props.requiredAttributes.gender) { stdAttributes.push(StandardAttribute.GENDER); } - if (props.requiredAttributes.givenName) { stdAttributes.push(StandardAttribute.GIVEN_NAME); } - if (props.requiredAttributes.lastUpdateTime) { stdAttributes.push(StandardAttribute.LAST_UPDATE_TIME); } - if (props.requiredAttributes.locale) { stdAttributes.push(StandardAttribute.LOCALE); } - if (props.requiredAttributes.middleName) { stdAttributes.push(StandardAttribute.MIDDLE_NAME); } - if (props.requiredAttributes.nickname) { stdAttributes.push(StandardAttribute.NICKNAME); } - if (props.requiredAttributes.phoneNumber) { stdAttributes.push(StandardAttribute.PHONE_NUMBER); } - if (props.requiredAttributes.preferredUsername) { stdAttributes.push(StandardAttribute.PREFERRED_USERNAME); } - if (props.requiredAttributes.profilePage) { stdAttributes.push(StandardAttribute.PROFILE_URL); } - if (props.requiredAttributes.profilePicture) { stdAttributes.push(StandardAttribute.PICTURE_URL); } - if (props.requiredAttributes.timezone) { stdAttributes.push(StandardAttribute.TIMEZONE); } - if (props.requiredAttributes.website) { stdAttributes.push(StandardAttribute.WEBSITE); } - - schema.push(...stdAttributes.map((attr) => { - return { name: attr, required: true }; - })); + if (props.standardAttributes) { + const stdAttributes = (Object.entries(props.standardAttributes) as Array<[keyof StandardAttributes, StandardAttribute]>) + .filter(([, attr]) => !!attr) + .map(([attrName, attr]) => ({ + name: StandardAttributeNames[attrName], + mutable: attr.mutable ?? true, + required: attr.required ?? false, + })); + + schema.push(...stdAttributes); } if (props.customAttributes) { @@ -904,8 +890,12 @@ export class UserPool extends UserPoolBase { return { name: attrName, attributeDataType: attrConfig.dataType, - numberAttributeConstraints: (attrConfig.numberConstraints) ? numberConstraints : undefined, - stringAttributeConstraints: (attrConfig.stringConstraints) ? stringConstraints : undefined, + numberAttributeConstraints: attrConfig.numberConstraints + ? numberConstraints + : undefined, + stringAttributeConstraints: attrConfig.stringConstraints + ? stringConstraints + : undefined, mutable: attrConfig.mutable, }; }); @@ -919,25 +909,25 @@ export class UserPool extends UserPoolBase { } } -const enum StandardAttribute { - ADDRESS = 'address', - BIRTHDATE = 'birthdate', - EMAIL = 'email', - FAMILY_NAME = 'family_name', - GENDER = 'gender', - GIVEN_NAME = 'given_name', - LOCALE = 'locale', - MIDDLE_NAME = 'middle_name', - NAME = 'name', - NICKNAME = 'nickname', - PHONE_NUMBER = 'phone_number', - PICTURE_URL = 'picture', - PREFERRED_USERNAME = 'preferred_username', - PROFILE_URL = 'profile', - TIMEZONE = 'zoneinfo', - LAST_UPDATE_TIME = 'updated_at', - WEBSITE = 'website', -} +const StandardAttributeNames: Record = { + address: 'address', + birthdate: 'birthdate', + email: 'email', + familyName: 'family_name', + gender: 'gender', + givenName: 'given_name', + locale: 'locale', + middleName: 'middle_name', + fullname: 'name', + nickname: 'nickname', + phoneNumber: 'phone_number', + profilePicture: 'picture', + preferredUsername: 'preferred_username', + profilePage: 'profile', + timezone: 'zoneinfo', + lastUpdateTime: 'updated_at', + website: 'website', +}; function undefinedIfNoKeys(struct: object): object | undefined { const allUndefined = Object.values(struct).reduce((acc, v) => acc && (v === undefined), true); diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.expected.json index 82f29c93ead24..7625b4a9a80d7 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.expected.json @@ -780,12 +780,14 @@ }, "Schema": [ { - "Name": "email", - "Required": true + "Name": "name", + "Required": true, + "Mutable": true }, { - "Name": "name", - "Required": true + "Name": "email", + "Required": true, + "Mutable": true }, { "AttributeDataType": "String", @@ -873,4 +875,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.ts index 262fbb8670638..1f4f7fe8193c5 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.ts +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.ts @@ -26,9 +26,14 @@ const userpool = new UserPool(stack, 'myuserpool', { email: true, phone: true, }, - requiredAttributes: { - fullname: true, - email: true, + standardAttributes: { + fullname: { + required: true, + mutable: true, + }, + email: { + required: true, + }, }, customAttributes: { 'some-string-attr': new StringAttribute(), @@ -90,4 +95,4 @@ function dummyTrigger(name: string): IFunction { runtime: Runtime.NODEJS_12_X, code: Code.fromInline('foo'), }); -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts index 212f6835cb508..43ef1a48d5dd1 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts @@ -178,4 +178,4 @@ describe('User Pool Attributes', () => { expect(bound.numberConstraints).toBeUndefined(); }); }); -}); \ No newline at end of file +}); diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts index 9fad806f888ad..61eb7a0ed229c 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts @@ -503,15 +503,19 @@ describe('User Pool', () => { }); }); - test('required attributes', () => { + test('standard attributes default to mutable', () => { // GIVEN const stack = new Stack(); // WHEN new UserPool(stack, 'Pool', { - requiredAttributes: { - fullname: true, - timezone: true, + standardAttributes: { + fullname: { + required: true, + }, + timezone: { + required: true, + }, }, }); @@ -521,41 +525,123 @@ describe('User Pool', () => { { Name: 'name', Required: true, + Mutable: true, }, { Name: 'zoneinfo', Required: true, + Mutable: true, }, ], }); }); - test('schema is absent when required attributes are specified but as false', () => { + test('mutable standard attributes', () => { // GIVEN const stack = new Stack(); // WHEN + new UserPool(stack, 'Pool', { + userPoolName: 'Pool', + standardAttributes: { + fullname: { + required: true, + mutable: true, + }, + timezone: { + required: true, + mutable: true, + }, + }, + }); + new UserPool(stack, 'Pool1', { userPoolName: 'Pool1', - }); - new UserPool(stack, 'Pool2', { - userPoolName: 'Pool2', - requiredAttributes: { - familyName: false, + standardAttributes: { + fullname: { + mutable: false, + }, + timezone: { + mutable: false, + }, }, }); // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPool', { + UserPoolName: 'Pool', + Schema: [ + { + Mutable: true, + Name: 'name', + Required: true, + }, + { + Mutable: true, + Name: 'zoneinfo', + Required: true, + }, + ], + }); + expect(stack).toHaveResourceLike('AWS::Cognito::UserPool', { UserPoolName: 'Pool1', - Schema: ABSENT, + Schema: [ + { + Name: 'name', + Required: false, + Mutable: false, + }, + { + Name: 'zoneinfo', + Required: false, + Mutable: false, + }, + ], }); + }); + + test('schema is absent when attributes are not specified', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', { userPoolName: 'Pool' }); + + // THEN expect(stack).toHaveResourceLike('AWS::Cognito::UserPool', { - UserPoolName: 'Pool2', + UserPoolName: 'Pool', Schema: ABSENT, }); }); + test('optional mutable standardAttributes', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', { + userPoolName: 'Pool', + standardAttributes: { + timezone: { + mutable: true, + }, + }, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPool', { + UserPoolName: 'Pool', + Schema: [ + { + Mutable: true, + Required: false, + Name: 'zoneinfo', + }, + ], + }); + }); + test('custom attributes with default constraints', () => { // GIVEN const stack = new Stack(); @@ -888,4 +974,4 @@ function fooFunction(scope: Construct, name: string): lambda.IFunction { runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', }); -} \ No newline at end of file +} From 00884c752d6746864f2a71d093502d4fb2422037 Mon Sep 17 00:00:00 2001 From: Romain Marcadier Date: Mon, 8 Jun 2020 17:00:44 +0200 Subject: [PATCH 19/32] fix(dynamodb): old global table replicas cannot be deleted (#8224) The permissions required to clean up old DynamoDB Global Tables replicas were set up in such a way that removing a replication region, or dropping replication entirely (or when causing a table replacement), they were removed before CloudFormation gets to the `CLEAN_UP` phase, causing a clean up failure (and old tables would remain there). This changes the way permissions are granted to the replication handler resource so that they are added using a separate `iam.Policy` resource, so that deleted permissions are also removed during the `CLEAN_UP` phase after the resources depending on them have been deleted. The tradeoff is that two additional resources are added to the stack that defines the DynamoDB Global Tables, where previously those permissions were mastered in the nested stack that holds the replication handler. Unofrtunately, the nested stack gets it's `CLEAN_UP` phase executed as part of the nested stack resource update, not during it's parent stack's `CLEAN_UP` phase. Fixes #7189 ---- *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-dynamodb/lib/table.ts | 65 ++++++- .../test/integ.global.expected.json | 169 ++++++++++++++++-- 2 files changed, 210 insertions(+), 24 deletions(-) diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index 1c1802f039153..d2594c95fa9b2 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -2,7 +2,10 @@ import * as appscaling from '@aws-cdk/aws-applicationautoscaling'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; -import { Aws, CfnCondition, CfnCustomResource, Construct, CustomResource, Fn, IResource, Lazy, RemovalPolicy, Resource, Stack, Token } from '@aws-cdk/core'; +import { + Aws, CfnCondition, CfnCustomResource, Construct, CustomResource, Fn, + IResource, Lazy, RemovalPolicy, Resource, Stack, Token, +} from '@aws-cdk/core'; import { CfnTable, CfnTableProps } from './dynamodb.generated'; import * as perms from './perms'; import { ReplicaProvider } from './replica-provider'; @@ -931,7 +934,7 @@ export class Table extends TableBase { this.tableSortKey = props.sortKey; } - if (props.replicationRegions) { + if (props.replicationRegions && props.replicationRegions.length > 0) { this.createReplicaTables(props.replicationRegions); } } @@ -1245,9 +1248,12 @@ export class Table extends TableBase { // Documentation at https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/V2gt_IAM.html // is currently incorrect. AWS Support recommends `dynamodb:*` in both source and destination regions + const onEventHandlerPolicy = new SourceTableAttachedPolicy(this, provider.onEventHandler.role!); + const isCompleteHandlerPolicy = new SourceTableAttachedPolicy(this, provider.isCompleteHandler.role!); + // Permissions in the source region - this.grant(provider.onEventHandler, 'dynamodb:*'); - this.grant(provider.isCompleteHandler, 'dynamodb:DescribeTable'); + this.grant(onEventHandlerPolicy, 'dynamodb:*'); + this.grant(isCompleteHandlerPolicy, 'dynamodb:DescribeTable'); let previousRegion; for (const region of new Set(regions)) { // Remove duplicates @@ -1261,6 +1267,10 @@ export class Table extends TableBase { Region: region, }, }); + currentRegion.node.addDependency( + onEventHandlerPolicy.policy, + isCompleteHandlerPolicy.policy, + ); // Deploy time check to prevent from creating a replica in the region // where this stack is deployed. Only needed for environment agnostic @@ -1292,7 +1302,7 @@ export class Table extends TableBase { // Permissions in the destination regions (outside of the loop to // minimize statements in the policy) - provider.onEventHandler.addToRolePolicy(new iam.PolicyStatement({ + onEventHandlerPolicy.grantPrincipal.addToPolicy(new iam.PolicyStatement({ actions: ['dynamodb:*'], resources: this.regionalArns, })); @@ -1428,3 +1438,48 @@ interface ScalableAttributePair { scalableReadAttribute?: ScalableTableAttribute; scalableWriteAttribute?: ScalableTableAttribute; } + +/** + * An inline policy that is logically bound to the source table of a DynamoDB Global Tables + * "cluster". This is here to ensure permissions are removed as part of (and not before) the + * CleanUp phase of a stack update, when a replica is removed (or the entire "cluster" gets + * replaced). + * + * If statements are added directly to the handler roles (as opposed to in a separate inline + * policy resource), new permissions are in effect before clean up happens, and so replicas that + * need to be dropped can no longer be due to lack of permissions. + */ +class SourceTableAttachedPolicy extends Construct implements iam.IGrantable { + public readonly grantPrincipal: iam.IPrincipal; + public readonly policy: iam.IPolicy; + + public constructor(sourceTable: Table, role: iam.IRole) { + super(sourceTable, `SourceTableAttachedPolicy-${role.node.uniqueId}`); + + const policy = new iam.Policy(this, 'Resource', { roles: [role] }); + this.policy = policy; + this.grantPrincipal = new SourceTableAttachedPrincipal(role, policy); + } +} + +/** + * An `IPrincipal` entity that can be used as the target of `grant` calls, used by the + * `SourceTableAttachedPolicy` class so it can act as an `IGrantable`. + */ +class SourceTableAttachedPrincipal extends iam.PrincipalBase { + public constructor(private readonly role: iam.IRole, private readonly policy: iam.Policy) { + super(); + } + + public get policyFragment(): iam.PrincipalPolicyFragment { + return this.role.policyFragment; + } + + public addToPrincipalPolicy(statement: iam.PolicyStatement): iam.AddToPrincipalPolicyResult { + this.policy.addStatements(statement); + return { + policyDependable: this.policy, + statementAdded: true, + }; + } +} diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json b/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json index dc4b5ce676ce6..9057e8c7ae31b 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json @@ -41,6 +41,140 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, + "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4AA4E210EA": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "dynamodb:*", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "TableCD117FA1", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "TableCD117FA1", + "Arn" + ] + }, + "/index/*" + ] + ] + } + ] + }, + { + "Action": "dynamodb:*", + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":dynamodb:eu-west-2:", + { + "Ref": "AWS::AccountId" + }, + ":table/", + { + "Ref": "TableCD117FA1" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":dynamodb:eu-central-1:", + { + "Ref": "AWS::AccountId" + }, + ":table/", + { + "Ref": "TableCD117FA1" + } + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4AA4E210EA", + "Roles": [ + { + "Fn::GetAtt": [ + "awscdkawsdynamodbReplicaProviderNestedStackawscdkawsdynamodbReplicaProviderNestedStackResource18E3F12D", + "Outputs.cdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole3E8625F3Ref" + ] + } + ] + } + }, + "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole397161288F61AAFA": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "dynamodb:DescribeTable", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "TableCD117FA1", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "TableCD117FA1", + "Arn" + ] + }, + "/index/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "leSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole397161288F61AAFA", + "Roles": [ + { + "Fn::GetAtt": [ + "awscdkawsdynamodbReplicaProviderNestedStackawscdkawsdynamodbReplicaProviderNestedStackResource18E3F12D", + "Outputs.cdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole2F936EC4Ref" + ] + } + ] + } + }, "TableReplicaeuwest290D3CD3A": { "Type": "Custom::DynamoDBReplica", "Properties": { @@ -55,6 +189,10 @@ }, "Region": "eu-west-2" }, + "DependsOn": [ + "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole397161288F61AAFA", + "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4AA4E210EA" + ], "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, @@ -73,7 +211,9 @@ "Region": "eu-central-1" }, "DependsOn": [ - "TableReplicaeuwest290D3CD3A" + "TableReplicaeuwest290D3CD3A", + "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole397161288F61AAFA", + "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4AA4E210EA" ], "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -91,7 +231,7 @@ }, "/", { - "Ref": "AssetParameters1e7110d85a2e13b58c2a0fb09f018c144489abfafc62bf10f8ab3561a9cb8510S3BucketCE06C497" + "Ref": "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947S3Bucket5148F39F" }, "/", { @@ -101,7 +241,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1e7110d85a2e13b58c2a0fb09f018c144489abfafc62bf10f8ab3561a9cb8510S3VersionKey6B6B0A66" + "Ref": "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947S3VersionKey0618C4C3" } ] } @@ -114,7 +254,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1e7110d85a2e13b58c2a0fb09f018c144489abfafc62bf10f8ab3561a9cb8510S3VersionKey6B6B0A66" + "Ref": "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947S3VersionKey0618C4C3" } ] } @@ -124,15 +264,6 @@ ] }, "Parameters": { - "referencetocdkdynamodbglobal20191121TableB640876BArn": { - "Fn::GetAtt": [ - "TableCD117FA1", - "Arn" - ] - }, - "referencetocdkdynamodbglobal20191121TableB640876BRef": { - "Ref": "TableCD117FA1" - }, "referencetocdkdynamodbglobal20191121AssetParameters012c6b101abc4ea1f510921af61a3e08e05f30f84d7b35c40ca4adb1ace60746S3BucketE0999323Ref": { "Ref": "AssetParameters012c6b101abc4ea1f510921af61a3e08e05f30f84d7b35c40ca4adb1ace60746S3BucketBDDEC9DD" }, @@ -174,17 +305,17 @@ "Type": "String", "Description": "Artifact hash for asset \"5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1\"" }, - "AssetParameters1e7110d85a2e13b58c2a0fb09f018c144489abfafc62bf10f8ab3561a9cb8510S3BucketCE06C497": { + "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947S3Bucket5148F39F": { "Type": "String", - "Description": "S3 bucket for asset \"1e7110d85a2e13b58c2a0fb09f018c144489abfafc62bf10f8ab3561a9cb8510\"" + "Description": "S3 bucket for asset \"ffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947\"" }, - "AssetParameters1e7110d85a2e13b58c2a0fb09f018c144489abfafc62bf10f8ab3561a9cb8510S3VersionKey6B6B0A66": { + "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947S3VersionKey0618C4C3": { "Type": "String", - "Description": "S3 key for asset version \"1e7110d85a2e13b58c2a0fb09f018c144489abfafc62bf10f8ab3561a9cb8510\"" + "Description": "S3 key for asset version \"ffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947\"" }, - "AssetParameters1e7110d85a2e13b58c2a0fb09f018c144489abfafc62bf10f8ab3561a9cb8510ArtifactHashAB28BC52": { + "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947ArtifactHashBF6B619B": { "Type": "String", - "Description": "Artifact hash for asset \"1e7110d85a2e13b58c2a0fb09f018c144489abfafc62bf10f8ab3561a9cb8510\"" + "Description": "Artifact hash for asset \"ffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947\"" } } } \ No newline at end of file From 8d5b801971ddaba82e0767c74fe7640d3e802c2f Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Mon, 8 Jun 2020 18:07:00 +0100 Subject: [PATCH 20/32] fix(aws-s3-deployment): Set proper s-maxage Cache Control header (#8434) Both the aws-s3-deployment and aws-codepipeline-actions CacheControl class uses "s-max-age" instead of the correct "s-maxage". This change fixes to the correct header value. fixes #6292 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-codepipeline-actions/lib/s3/deploy-action.ts | 2 +- packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts | 2 +- .../@aws-cdk/aws-s3-deployment/test/test.bucket-deployment.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/deploy-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/deploy-action.ts index 9a77638bf0f0a..7168719bde5f6 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/deploy-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/deploy-action.ts @@ -32,7 +32,7 @@ export class CacheControl { /** The 'max-age' cache control directive. */ public static maxAge(t: Duration) { return new CacheControl(`max-age: ${t.toSeconds()}`); } /** The 's-max-age' cache control directive. */ - public static sMaxAge(t: Duration) { return new CacheControl(`s-max-age: ${t.toSeconds()}`); } + public static sMaxAge(t: Duration) { return new CacheControl(`s-maxage: ${t.toSeconds()}`); } /** * Allows you to create an arbitrary cache control directive, * in case our support is missing a method for a particular directive. diff --git a/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts b/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts index 07ea57699633a..e8f4fda42651b 100644 --- a/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts +++ b/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts @@ -283,7 +283,7 @@ export class CacheControl { public static setPrivate() { return new CacheControl('private'); } public static proxyRevalidate() { return new CacheControl('proxy-revalidate'); } public static maxAge(t: cdk.Duration) { return new CacheControl(`max-age=${t.toSeconds()}`); } - public static sMaxAge(t: cdk.Duration) { return new CacheControl(`s-max-age=${t.toSeconds()}`); } + public static sMaxAge(t: cdk.Duration) { return new CacheControl(`s-maxage=${t.toSeconds()}`); } public static fromString(s: string) { return new CacheControl(s); } private constructor(public readonly value: any) {} diff --git a/packages/@aws-cdk/aws-s3-deployment/test/test.bucket-deployment.ts b/packages/@aws-cdk/aws-s3-deployment/test/test.bucket-deployment.ts index 11cf55ad65118..0850702dbf414 100644 --- a/packages/@aws-cdk/aws-s3-deployment/test/test.bucket-deployment.ts +++ b/packages/@aws-cdk/aws-s3-deployment/test/test.bucket-deployment.ts @@ -369,7 +369,7 @@ export = { test.equal(s3deploy.CacheControl.setPrivate().value, 'private'); test.equal(s3deploy.CacheControl.proxyRevalidate().value, 'proxy-revalidate'); test.equal(s3deploy.CacheControl.maxAge(cdk.Duration.minutes(1)).value, 'max-age=60'); - test.equal(s3deploy.CacheControl.sMaxAge(cdk.Duration.minutes(1)).value, 's-max-age=60'); + test.equal(s3deploy.CacheControl.sMaxAge(cdk.Duration.minutes(1)).value, 's-maxage=60'); test.equal(s3deploy.CacheControl.fromString('only-if-cached').value, 'only-if-cached'); test.done(); From bdb4ca54525b2e65da169465fecce98d79194664 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2020 20:09:10 +0000 Subject: [PATCH 21/32] chore(deps): bump fast-deep-equal from 3.1.1 to 3.1.3 (#8439) Bumps [fast-deep-equal](https://github.com/epoberezkin/fast-deep-equal) from 3.1.1 to 3.1.3. - [Release notes](https://github.com/epoberezkin/fast-deep-equal/releases) - [Commits](https://github.com/epoberezkin/fast-deep-equal/compare/v3.1.1...v3.1.3) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- packages/@aws-cdk/cloudformation-diff/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/cloudformation-diff/package.json b/packages/@aws-cdk/cloudformation-diff/package.json index bb31931b64aac..88342e0ba3835 100644 --- a/packages/@aws-cdk/cloudformation-diff/package.json +++ b/packages/@aws-cdk/cloudformation-diff/package.json @@ -24,7 +24,7 @@ "@aws-cdk/cfnspec": "0.0.0", "colors": "^1.4.0", "diff": "^4.0.2", - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", "string-width": "^4.2.0", "table": "^5.4.6" }, diff --git a/yarn.lock b/yarn.lock index 17bee28e90a05..92848de826535 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4146,10 +4146,10 @@ fast-deep-equal@^2.0.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= -fast-deep-equal@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" - integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-glob@^2.2.6: version "2.2.7" From 4781f94ee530ef66488fbf7b3728a753fa5718cd Mon Sep 17 00:00:00 2001 From: Pahud Hsieh Date: Tue, 9 Jun 2020 05:02:10 +0800 Subject: [PATCH 22/32] feat(cloud9): support AWS CodeCommit repository clone on launch (#8205) feat(cloud9): support AWS CodeCommit repository clone on launch Add a new `repositories` property to allow users to clone AWS CodeCommit repositories on environment launch. Closes #8204 ---- *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-cloud9/README.md | 22 ++++++++++ .../@aws-cdk/aws-cloud9/lib/environment.ts | 38 +++++++++++++++-- packages/@aws-cdk/aws-cloud9/package.json | 7 +++- .../test/cloud9.environment.test.ts | 42 ++++++++++++++++++- .../test/integ.cloud9.expected.json | 17 ++++++++ .../@aws-cdk/aws-cloud9/test/integ.cloud9.ts | 16 ++++++- 6 files changed, 134 insertions(+), 8 deletions(-) diff --git a/packages/@aws-cdk/aws-cloud9/README.md b/packages/@aws-cdk/aws-cloud9/README.md index 5f46fa558e85c..6f7ca79297807 100644 --- a/packages/@aws-cdk/aws-cloud9/README.md +++ b/packages/@aws-cdk/aws-cloud9/README.md @@ -49,3 +49,25 @@ const c9env = new cloud9.Ec2Environment(this, 'Cloud9Env3', { new cdk.CfnOutput(this, 'URL', { value: c9env.ideUrl }); ``` +### Cloning Repositories + +Use `clonedRepositories` to clone one or multiple AWS Codecommit repositories into the environment: + +```ts +// create a codecommit repository to clone into the cloud9 environment +const repoNew = new codecommit.Repository(this, 'RepoNew', { + repositoryName: 'new-repo', +}); + +// import an existing codecommit repository to clone into the cloud9 environment +const repoExisting = codecommit.Repository.fromRepositoryName(stack, 'RepoExisting', 'existing-repo'); + +// create a new Cloud9 environment and clone the two repositories +new cloud9.Ec2Environment(stack, 'C9Env', { + vpc, + clonedRepositories: [ + cloud9.CloneRepository.fromCodeCommit(repoNew, '/src/new-repo'), + cloud9.CloneRepository.fromCodeCommit(repoExisting, '/src/existing-repo'), + ], +}); +``` diff --git a/packages/@aws-cdk/aws-cloud9/lib/environment.ts b/packages/@aws-cdk/aws-cloud9/lib/environment.ts index 45ed441cd4e5f..d414069e2788b 100644 --- a/packages/@aws-cdk/aws-cloud9/lib/environment.ts +++ b/packages/@aws-cdk/aws-cloud9/lib/environment.ts @@ -1,3 +1,4 @@ +import * as codecommit from '@aws-cdk/aws-codecommit'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import { CfnEnvironmentEC2 } from '../lib/cloud9.generated'; @@ -20,7 +21,6 @@ export interface IEc2Environment extends cdk.IResource { * @attribute environmentE2Arn */ readonly ec2EnvironmentArn: string; - } /** @@ -61,6 +61,14 @@ export interface Ec2EnvironmentProps { * @default - no description */ readonly description?: string; + + /** + * The AWS CodeCommit repository to be cloned + * + * @default - do not clone any repository + */ + // readonly clonedRepositories?: Cloud9Repository[]; + readonly clonedRepositories?: CloneRepository[]; } /** @@ -125,11 +133,35 @@ export class Ec2Environment extends cdk.Resource implements IEc2Environment { name: props.ec2EnvironmentName, description: props.description, instanceType: props.instanceType?.toString() ?? ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.MICRO).toString(), - subnetId: this.vpc.selectSubnets(vpcSubnets).subnetIds[0] , + subnetId: this.vpc.selectSubnets(vpcSubnets).subnetIds[0], + repositories: props.clonedRepositories ? props.clonedRepositories.map(r => ({ + repositoryUrl: r.repositoryUrl, + pathComponent: r.pathComponent, + })) : undefined, }); this.environmentId = c9env.ref; this.ec2EnvironmentArn = c9env.getAtt('Arn').toString(); this.ec2EnvironmentName = c9env.getAtt('Name').toString(); this.ideUrl = `https://${this.stack.region}.console.aws.amazon.com/cloud9/ide/${this.environmentId}`; } -} \ No newline at end of file +} + +/** + * The class for different repository providers + */ +export class CloneRepository { + /** + * import repository to cloud9 environment from AWS CodeCommit + * + * @param repository the codecommit repository to clone from + * @param path the target path in cloud9 environment + */ + public static fromCodeCommit(repository: codecommit.IRepository, path: string): CloneRepository { + return { + repositoryUrl: repository.repositoryCloneUrlHttp, + pathComponent: path, + }; + } + + private constructor(public readonly repositoryUrl: string, public readonly pathComponent: string) {} +} diff --git a/packages/@aws-cdk/aws-cloud9/package.json b/packages/@aws-cdk/aws-cloud9/package.json index 4e5ce1f32e1e3..8bb9f055c5d9d 100644 --- a/packages/@aws-cdk/aws-cloud9/package.json +++ b/packages/@aws-cdk/aws-cloud9/package.json @@ -64,6 +64,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", + "@aws-cdk/aws-codecommit": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -71,12 +72,14 @@ }, "dependencies": { "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "constructs": "^3.0.2" }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "constructs": "^3.0.2" }, @@ -87,7 +90,9 @@ "exclude": [ "resource-attribute:@aws-cdk/aws-cloud9.Ec2Environment.environmentEc2Arn", "resource-attribute:@aws-cdk/aws-cloud9.Ec2Environment.environmentEc2Name", - "props-physical-name:@aws-cdk/aws-cloud9.Ec2EnvironmentProps" + "props-physical-name:@aws-cdk/aws-cloud9.Ec2EnvironmentProps", + "docs-public-apis:@aws-cdk/aws-cloud9.CloneRepository.pathComponent", + "docs-public-apis:@aws-cdk/aws-cloud9.CloneRepository.repositoryUrl" ] }, "stability": "experimental", diff --git a/packages/@aws-cdk/aws-cloud9/test/cloud9.environment.test.ts b/packages/@aws-cdk/aws-cloud9/test/cloud9.environment.test.ts index 2d41d86032371..d2a1a43fa0755 100644 --- a/packages/@aws-cdk/aws-cloud9/test/cloud9.environment.test.ts +++ b/packages/@aws-cdk/aws-cloud9/test/cloud9.environment.test.ts @@ -1,4 +1,5 @@ -import { expect as expectCDK, haveResource } from '@aws-cdk/assert'; +import { expect as expectCDK, haveResource, haveResourceLike } from '@aws-cdk/assert'; +import * as codecommit from '@aws-cdk/aws-codecommit'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import * as cloud9 from '../lib'; @@ -66,4 +67,41 @@ test('throw error when subnetSelection not specified and the provided VPC has no instanceType: ec2.InstanceType.of(ec2.InstanceClass.C5, ec2.InstanceSize.LARGE), }); }).toThrow(/no subnetSelection specified and no public subnet found in the vpc, please specify subnetSelection/); -}); \ No newline at end of file +}); + +test('can use CodeCommit repositories', () => { + // WHEN + const repo = codecommit.Repository.fromRepositoryName(stack, 'Repo', 'foo'); + + new cloud9.Ec2Environment(stack, 'C9Env', { + vpc, + clonedRepositories: [ + cloud9.CloneRepository.fromCodeCommit(repo, '/src'), + ], + }); + // THEN + expectCDK(stack).to(haveResourceLike('AWS::Cloud9::EnvironmentEC2', { + InstanceType: 't2.micro', + Repositories: [ + { + PathComponent: '/src', + RepositoryUrl: { + 'Fn::Join': [ + '', + [ + 'https://git-codecommit.', + { + Ref: 'AWS::Region', + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + '/v1/repos/foo', + ], + ], + }, + }, + ], + })); +}); diff --git a/packages/@aws-cdk/aws-cloud9/test/integ.cloud9.expected.json b/packages/@aws-cdk/aws-cloud9/test/integ.cloud9.expected.json index 13431fa4c95fb..86777d556cc3d 100644 --- a/packages/@aws-cdk/aws-cloud9/test/integ.cloud9.expected.json +++ b/packages/@aws-cdk/aws-cloud9/test/integ.cloud9.expected.json @@ -323,10 +323,27 @@ } } }, + "Repo02AC86CF": { + "Type": "AWS::CodeCommit::Repository", + "Properties": { + "RepositoryName": "foo" + } + }, "C9EnvF05FC3BE": { "Type": "AWS::Cloud9::EnvironmentEC2", "Properties": { "InstanceType": "t2.micro", + "Repositories": [ + { + "PathComponent": "/foo", + "RepositoryUrl": { + "Fn::GetAtt": [ + "Repo02AC86CF", + "CloneUrlHttp" + ] + } + } + ], "SubnetId": { "Ref": "VPCPublicSubnet1SubnetB4246D30" } diff --git a/packages/@aws-cdk/aws-cloud9/test/integ.cloud9.ts b/packages/@aws-cdk/aws-cloud9/test/integ.cloud9.ts index 369f037b32c7f..d2d008687f429 100644 --- a/packages/@aws-cdk/aws-cloud9/test/integ.cloud9.ts +++ b/packages/@aws-cdk/aws-cloud9/test/integ.cloud9.ts @@ -1,3 +1,4 @@ +import * as codecommit from '@aws-cdk/aws-codecommit'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import * as cloud9 from '../lib'; @@ -11,8 +12,19 @@ export class Cloud9Env extends cdk.Stack { natGateways: 1, }); + // create a codecommit repository to clone into the cloud9 environment + const repo = new codecommit.Repository(this, 'Repo', { + repositoryName: 'foo', + }); + // create a cloud9 ec2 environment in a new VPC - const c9env = new cloud9.Ec2Environment(this, 'C9Env', { vpc }); + const c9env = new cloud9.Ec2Environment(this, 'C9Env', { + vpc, + // clone repositories into the environment + clonedRepositories: [ + cloud9.CloneRepository.fromCodeCommit(repo, '/foo'), + ], + }); new cdk.CfnOutput(this, 'URL', { value: c9env.ideUrl }); new cdk.CfnOutput(this, 'ARN', { value: c9env.ec2EnvironmentArn }); } @@ -20,4 +32,4 @@ export class Cloud9Env extends cdk.Stack { const app = new cdk.App(); -new Cloud9Env(app, 'C9Stack'); \ No newline at end of file +new Cloud9Env(app, 'C9Stack'); From 02ddab8c1e76c59ccaff4f45986de68d538d54eb Mon Sep 17 00:00:00 2001 From: Pahud Hsieh Date: Tue, 9 Jun 2020 05:47:54 +0800 Subject: [PATCH 23/32] feat(codestar): support the GitHubRepository resource (#8209) feat(codestar): support the GitHubRepository resource This PR allows to create github repositories with the new `GitHubRepository` resource Closes #8210 *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-codestar/README.md | 23 +++- .../aws-codestar/lib/github-repository.ts | 126 ++++++++++++++++++ packages/@aws-cdk/aws-codestar/lib/index.ts | 1 + packages/@aws-cdk/aws-codestar/package.json | 10 +- .../aws-codestar/test/codestar.test.ts | 56 +++++++- 5 files changed, 211 insertions(+), 5 deletions(-) create mode 100644 packages/@aws-cdk/aws-codestar/lib/github-repository.ts diff --git a/packages/@aws-cdk/aws-codestar/README.md b/packages/@aws-cdk/aws-codestar/README.md index 3f423f3b8a5ab..87c1685ad6ffd 100644 --- a/packages/@aws-cdk/aws-codestar/README.md +++ b/packages/@aws-cdk/aws-codestar/README.md @@ -6,11 +6,32 @@ > All classes with the `Cfn` prefix in this module ([CFN Resources](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) are always stable and safe to use. +![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 module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. +## GitHub Repository + +To create a new GitHub Repository and commit the assets from S3 bucket into the repository after it is created: ```ts import * as codestar from '@aws-cdk/aws-codestar'; +import * as s3 from '@aws-cdk/aws-s3' + +new codestar.GitHubRepository(stack, 'GitHubRepo', { + owner: 'aws', + repositoryName: 'aws-cdk', + accessToken: cdk.SecretValue.secretsManager('my-github-token', { + jsonField: 'token', + }), + contentsBucket: s3.Bucket.fromBucketName(stack, 'Bucket', 'bucket-name'), + contentsKey: 'import.zip', +}); ``` + +## Update or Delete the GitHubRepository + +At this moment, updates to the `GitHubRepository` are not supported and the repository will not be deleted upon the deletion of the CloudFormation stack. You will need to update or delete the GitHub repository manually. diff --git a/packages/@aws-cdk/aws-codestar/lib/github-repository.ts b/packages/@aws-cdk/aws-codestar/lib/github-repository.ts new file mode 100644 index 0000000000000..0afd45eb0c826 --- /dev/null +++ b/packages/@aws-cdk/aws-codestar/lib/github-repository.ts @@ -0,0 +1,126 @@ +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import * as codestar from './codestar.generated'; + +/** + * GitHubRepository resource interface + */ +export interface IGitHubRepository extends cdk.IResource { + /** + * the repository owner + */ + readonly owner: string + + /** + * the repository name + */ + readonly repo: string +} + +/** + * Construction properties of {@link GitHubRepository}. + */ +export interface GitHubRepositoryProps { + /** + * The GitHub user name for the owner of the GitHub repository to be created. If this + * repository should be owned by a GitHub organization, provide its name + */ + readonly owner: string; + + /** + * The name of the repository you want to create in GitHub with AWS CloudFormation stack creation + */ + readonly repositoryName: string; + + /** + * The GitHub user's personal access token for the GitHub repository + */ + readonly accessToken: cdk.SecretValue; + + /** + * The name of the Amazon S3 bucket that contains the ZIP file with the content to be committed to the new repository + */ + readonly contentsBucket: s3.IBucket; + + /** + * The S3 object key or file name for the ZIP file + */ + readonly contentsKey: string; + + /** + * The object version of the ZIP file, if versioning is enabled for the Amazon S3 bucket + * + * @default - not specified + */ + readonly contentsS3Version?: string; + + /** + * Indicates whether to enable issues for the GitHub repository. You can use GitHub issues to track information + * and bugs for your repository. + * + * @default true + */ + readonly enableIssues?: boolean; + + /** + * Indicates whether the GitHub repository is a private repository. If so, you choose who can see and commit to + * this repository. + * + * @default RepositoryVisibility.PUBLIC + */ + readonly visibility?: RepositoryVisibility; + + /** + * A comment or description about the new repository. This description is displayed in GitHub after the repository + * is created. + * + * @default - no description + */ + readonly description?: string; +} + +/** + * The GitHubRepository resource + */ +export class GitHubRepository extends cdk.Resource implements IGitHubRepository { + + public readonly owner: string; + public readonly repo: string; + + constructor(scope: cdk.Construct, id: string, props: GitHubRepositoryProps) { + super(scope, id); + + const resource = new codestar.CfnGitHubRepository(this, 'Resource', { + repositoryOwner: props.owner, + repositoryName: props.repositoryName, + repositoryAccessToken: props.accessToken.toString(), + code: { + s3: { + bucket: props.contentsBucket.bucketName, + key: props.contentsKey, + objectVersion: props.contentsS3Version, + }, + }, + enableIssues: props.enableIssues ?? true, + isPrivate: props.visibility === RepositoryVisibility.PRIVATE ? true : false, + repositoryDescription: props.description, + }); + + this.owner = cdk.Fn.select(0, cdk.Fn.split('/', resource.ref)); + this.repo = cdk.Fn.select(1, cdk.Fn.split('/', resource.ref)); + } +} + +/** + * Visibility of the GitHubRepository + */ +export enum RepositoryVisibility { + /** + * private repository + */ + PRIVATE, + /** + * public repository + */ + PUBLIC, +} diff --git a/packages/@aws-cdk/aws-codestar/lib/index.ts b/packages/@aws-cdk/aws-codestar/lib/index.ts index 4114892b944da..ff8a544388441 100644 --- a/packages/@aws-cdk/aws-codestar/lib/index.ts +++ b/packages/@aws-cdk/aws-codestar/lib/index.ts @@ -1,2 +1,3 @@ // AWS::CodeStar CloudFormation Resources: export * from './codestar.generated'; +export * from './github-repository'; diff --git a/packages/@aws-cdk/aws-codestar/package.json b/packages/@aws-cdk/aws-codestar/package.json index d0c07ad1c6489..2b1d9cfc7773c 100644 --- a/packages/@aws-cdk/aws-codestar/package.json +++ b/packages/@aws-cdk/aws-codestar/package.json @@ -67,22 +67,30 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0" }, "dependencies": { + "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, "peerDependencies": { + "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, + "awslint": { + "exclude": [ + "props-physical-name:@aws-cdk/aws-codestar.GitHubRepositoryProps" + ] + }, "stability": "experimental", - "maturity": "cfn-only", + "maturity": "experimental", "awscdkio": { "announce": false } diff --git a/packages/@aws-cdk/aws-codestar/test/codestar.test.ts b/packages/@aws-cdk/aws-codestar/test/codestar.test.ts index e394ef336bfb4..bc551f25a41d3 100644 --- a/packages/@aws-cdk/aws-codestar/test/codestar.test.ts +++ b/packages/@aws-cdk/aws-codestar/test/codestar.test.ts @@ -1,6 +1,56 @@ import '@aws-cdk/assert/jest'; -import {} from '../lib'; +import { Bucket } from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import { GitHubRepository, RepositoryVisibility } from '../lib'; -test('No tests are specified for this package', () => { - expect(true).toBe(true); +describe('GitHub Repository', () => { + let stack: cdk.Stack; + + beforeEach(() => { + const app = new cdk.App(); + stack = new cdk.Stack(app, 'GitHubDemo'); + }); + + test('create', () => { + new GitHubRepository(stack, 'GitHubRepo', { + owner: 'foo', + repositoryName: 'bar', + accessToken: cdk.SecretValue.secretsManager('my-github-token', { + jsonField: 'token', + }), + contentsBucket: Bucket.fromBucketName(stack, 'Bucket', 'bucket-name'), + contentsKey: 'import.zip', + }); + + expect(stack).toHaveResource('AWS::CodeStar::GitHubRepository', { + RepositoryAccessToken: '{{resolve:secretsmanager:my-github-token:SecretString:token::}}', + RepositoryName: 'bar', + RepositoryOwner: 'foo', + Code: { + S3: { + Bucket: 'bucket-name', + Key: 'import.zip', + }, + }, + }); + }); + + test('enable issues and private', () => { + new GitHubRepository(stack, 'GitHubRepo', { + owner: 'foo', + repositoryName: 'bar', + accessToken: cdk.SecretValue.secretsManager('my-github-token', { + jsonField: 'token', + }), + contentsBucket: Bucket.fromBucketName(stack, 'Bucket', 'bucket-name'), + contentsKey: 'import.zip', + enableIssues: true, + visibility: RepositoryVisibility.PRIVATE, + }); + + expect(stack).toHaveResourceLike('AWS::CodeStar::GitHubRepository', { + EnableIssues: true, + IsPrivate: true, + }); + }); }); From f6fe36a0281a60ad65474b6ce0e22d0182ed2bea Mon Sep 17 00:00:00 2001 From: Yutaka Kohada Date: Tue, 9 Jun 2020 07:33:57 +0900 Subject: [PATCH 24/32] feat(secretsmanager): deletionPolicy for secretsmanager (#8188) We often store important values on secretsmanager.Secret. But, without DeletionPolicy(Retain), it can be deleted by human error. So, add DeletionPolicy to secretsmanager.Secret's initialization Props. closes: #6527 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-secretsmanager/README.md | 2 ++ .../@aws-cdk/aws-secretsmanager/lib/secret.ts | 13 +++++++++++- .../aws-secretsmanager/test/test.secret.ts | 21 ++++++++++++++++++- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-secretsmanager/README.md b/packages/@aws-cdk/aws-secretsmanager/README.md index 920b4d4146bf5..fb3a61e920725 100644 --- a/packages/@aws-cdk/aws-secretsmanager/README.md +++ b/packages/@aws-cdk/aws-secretsmanager/README.md @@ -39,6 +39,8 @@ const secret = secretsmanager.Secret.fromSecretAttributes(scope, 'ImportedSecret SecretsManager secret values can only be used in select set of properties. For the list of properties, see [the CloudFormation Dynamic References documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.html). +A secret can set `RemovalPolicy`. If it set to `RETAIN`, that removing a secret will fail. + ### Grant permission to use the secret to a role You must grant permission to a resource for that resource to be allowed to diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts index b44c44206a0b3..91cf18a7a8229 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts @@ -1,6 +1,6 @@ import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; -import { Construct, IResource, Resource, SecretValue, Stack } from '@aws-cdk/core'; +import { Construct, IResource, RemovalPolicy, Resource, SecretValue, Stack } from '@aws-cdk/core'; import { ResourcePolicy } from './policy'; import { RotationSchedule, RotationScheduleOptions } from './rotation-schedule'; import * as secretsmanager from './secretsmanager.generated'; @@ -102,6 +102,13 @@ export interface SecretProps { * @default - A name is generated by CloudFormation. */ readonly secretName?: string; + + /** + * Policy to apply when the secret is removed from this stack. + * + * @default - Not set. + */ + readonly removalPolicy?: RemovalPolicy; } /** @@ -260,6 +267,10 @@ export class Secret extends SecretBase { name: this.physicalName, }); + if (props.removalPolicy) { + resource.applyRemovalPolicy(props.removalPolicy); + } + this.secretArn = this.getResourceArnAttribute(resource.ref, { service: 'secretsmanager', resource: 'secret', diff --git a/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts b/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts index 606bc33d9ec8b..1b10443ff69e2 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts @@ -1,4 +1,4 @@ -import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; +import { expect, haveResource, haveResourceLike, ResourcePart } from '@aws-cdk/assert'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as lambda from '@aws-cdk/aws-lambda'; @@ -22,6 +22,25 @@ export = { test.done(); }, + 'set removalPolicy to secret'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new secretsmanager.Secret(stack, 'Secret', { + removalPolicy: cdk.RemovalPolicy.RETAIN, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::SecretsManager::Secret', + { + DeletionPolicy: 'Retain', + }, ResourcePart.CompleteDefinition, + )); + + test.done(); + }, + 'secret with kms'(test: Test) { // GIVEN const stack = new cdk.Stack(); From ae90cba34179ed890ddea07685c86e8be6e52e96 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Tue, 9 Jun 2020 05:44:05 +0100 Subject: [PATCH 25/32] chore: re-enable using current pkg versions for api compat checks (#8427) This was originally added in commit https://github.com/aws/aws-cdk/commit/3dd21b9b212f96720519ac12f9cb538c697e9343. However, the script fails during a bump build when the package version in lerna.json is ahead of the latest published in NPM. This was worked around by turning this feature off - https://github.com/aws/aws-cdk/commit/09a1f33f975e49f92bfa708e3d3d03984863a28d. Re-enable this feature and handle version in lerna.json may be ahead of NPM. ### Testing Manually tested three cases - * When version in `lerna.json` is <= package published in NPM * When version in `lerna.json` is > package published in NPM * When `DOWNLOAD_LATEST` is set to `true`. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- build.sh | 2 +- scripts/check-api-compatibility.sh | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/build.sh b/build.sh index 44c020454216a..b58fa80e1c563 100755 --- a/build.sh +++ b/build.sh @@ -60,6 +60,6 @@ echo "========================================================================== echo "building..." time lerna run $bail --stream $runtarget || fail -DOWNLOAD_LATEST=true /bin/bash scripts/check-api-compatibility.sh +/bin/bash scripts/check-api-compatibility.sh touch $BUILD_INDICATOR diff --git a/scripts/check-api-compatibility.sh b/scripts/check-api-compatibility.sh index a63ccb5f7fa9b..ca15553418f72 100755 --- a/scripts/check-api-compatibility.sh +++ b/scripts/check-api-compatibility.sh @@ -13,7 +13,9 @@ package_name() { # # Doesn't use 'npm view' as that is slow. Direct curl'ing npmjs is better package_exists_on_npm() { - curl -I 2>/dev/null https://registry.npmjs.org/$1 | head -n 1 | grep 200 >/dev/null + pkg=$1 + ver=$2 # optional + curl -I 2>/dev/null https://registry.npmjs.org/$pkg/$ver | head -n 1 | grep 200 >/dev/null } @@ -53,10 +55,13 @@ if ! ${SKIP_DOWNLOAD:-false}; then existing_names=$(echo "$jsii_package_dirs" | xargs -n1 -P4 -I {} bash -c 'dirs_to_existing_names "$@"' _ {}) echo " Done." >&2 - if ! ${DOWNLOAD_LATEST:-false}; then - current_version=$(node -p 'require("./lerna.json").version') + current_version=$(node -p 'require("./lerna.json").version') + echo "Current version in lerna.json is $current_version" + if ! ${DOWNLOAD_LATEST:-false} && package_exists_on_npm aws-cdk $current_version; then echo "Using package version ${current_version} as baseline" existing_names=$(echo "$existing_names" | sed -e "s/$/@$current_version/") + else + echo "However, using the latest version from NPM as the baseline" fi rm -rf $tmpdir From 480d4c004122f37533c22a14c6ecb89b5da07011 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Mon, 8 Jun 2020 22:58:33 -0700 Subject: [PATCH 26/32] feat(stepfunctions-tasks): task constructs for creating and transforming SageMaker jobs (#8391) replacement for the current implementation of `SageMaker` service integration and state level properties are merged and represented as a construct. The previous implementation that implemented `IStepFunctionsTask` has been removed. The previously existing classes were directly converted to constructs as these were marked **experimental** and still require further iterations as they are not backed by a SageMaker L2 (does not exist yet). In the interest of pragmatism, I decided to move them to leverage the newer pattern so we can deprecate the `Task` construct. Note that I have left the unit and integration tests verbatim. The integration test requires some additional steps as there are pre-requisites to running a training job such as creating and configuring input data that are not currently included. BREAKING CHANGE: constructs for `SageMakerCreateTrainingJob` and `SageMakerCreateTransformJob` replace previous implementation that implemented `IStepFunctionsTask`. * **stepfunctions-tasks:** `volumeSizeInGB` property in `ResourceConfig` for SageMaker tasks are now type `core.Size` * **stepfunctions-tasks:** `maxPayload` property in `SagemakerTransformProps` is now type `core.Size` * **stepfunctions-tasks:** `volumeKmsKeyId` property in `SageMakerCreateTrainingJob` is now `volumeEncryptionKey` ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-stepfunctions-tasks/README.md | 94 +++--- .../aws-stepfunctions-tasks/lib/index.ts | 6 +- ...maker-task-base-types.ts => base-types.ts} | 8 +- ...r-train-task.ts => create-training-job.ts} | 296 +++++++++--------- ...nsform-task.ts => create-transform-job.ts} | 175 +++++------ ...ob.test.ts => create-training-job.test.ts} | 32 +- ...b.test.ts => create-transform-job.test.ts} | 27 +- ...> integ.create-training-job.expected.json} | 35 ++- .../sagemaker/integ.create-training-job.ts | 53 ++++ .../test/sagemaker/integ.sagemaker.ts | 34 -- 10 files changed, 387 insertions(+), 373 deletions(-) rename packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/{sagemaker-task-base-types.ts => base-types.ts} (98%) rename packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/{sagemaker-train-task.ts => create-training-job.ts} (52%) rename packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/{sagemaker-transform-task.ts => create-transform-job.ts} (51%) rename packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/{sagemaker-training-job.test.ts => create-training-job.test.ts} (92%) rename packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/{sagemaker-transform-job.test.ts => create-transform-job.test.ts} (88%) rename packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/{integ.sagemaker.expected.json => integ.create-training-job.expected.json} (94%) create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.create-training-job.ts delete mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.sagemaker.ts diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index c8482f9e57f09..e0e89b4ecd924 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -617,37 +617,33 @@ Step Functions supports [AWS SageMaker](https://docs.aws.amazon.com/step-functio You can call the [`CreateTrainingJob`](https://docs.aws.amazon.com/sagemaker/latest/dg/API_CreateTrainingJob.html) API from a `Task` state. ```ts -new sfn.Task(stack, 'TrainSagemaker', { - task: new tasks.SagemakerTrainTask({ - trainingJobName: sfn.Data.stringAt('$.JobName'), - role, - algorithmSpecification: { - algorithmName: 'BlazingText', - trainingInputMode: tasks.InputMode.FILE, - }, - inputDataConfig: [ - { - channelName: 'train', - dataSource: { - s3DataSource: { - s3DataType: tasks.S3DataType.S3_PREFIX, - s3Location: tasks.S3Location.fromJsonExpression('$.S3Bucket'), - }, - }, +new sfn.SagemakerTrainTask(this, 'TrainSagemaker', { + trainingJobName: sfn.Data.stringAt('$.JobName'), + role, + algorithmSpecification: { + algorithmName: 'BlazingText', + trainingInputMode: tasks.InputMode.FILE, + }, + inputDataConfig: [{ + channelName: 'train', + dataSource: { + s3DataSource: { + s3DataType: tasks.S3DataType.S3_PREFIX, + s3Location: tasks.S3Location.fromJsonExpression('$.S3Bucket'), }, - ], - outputDataConfig: { - s3OutputLocation: tasks.S3Location.fromBucket(s3.Bucket.fromBucketName(stack, 'Bucket', 'mybucket'), 'myoutputpath'), - }, - resourceConfig: { - instanceCount: 1, - instanceType: ec2.InstanceType.of(ec2.InstanceClass.P3, ec2.InstanceSize.XLARGE2), - volumeSizeInGB: 50, }, - stoppingCondition: { - maxRuntime: cdk.Duration.hours(1), - }, - }), + }], + outputDataConfig: { + s3OutputLocation: tasks.S3Location.fromBucket(s3.Bucket.fromBucketName(stack, 'Bucket', 'mybucket'), 'myoutputpath'), + }, + resourceConfig: { + instanceCount: 1, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.P3, ec2.InstanceSize.XLARGE2), + volumeSize: cdk.Size.gibibytes(50), + }, + stoppingCondition: { + maxRuntime: cdk.Duration.hours(1), + }, }); ``` @@ -656,29 +652,27 @@ new sfn.Task(stack, 'TrainSagemaker', { You can call the [`CreateTransformJob`](https://docs.aws.amazon.com/sagemaker/latest/dg/API_CreateTransformJob.html) API from a `Task` state. ```ts -const transformJob = new tasks.SagemakerTransformTask( - transformJobName: "MyTransformJob", - modelName: "MyModelName", - role, - transformInput: { - transformDataSource: { - s3DataSource: { - s3Uri: 's3://inputbucket/train', - s3DataType: S3DataType.S3Prefix, - } - } - }, - transformOutput: { - s3OutputPath: 's3://outputbucket/TransformJobOutputPath', - }, - transformResources: { - instanceCount: 1, - instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.XLarge), +new sfn.SagemakerTransformTask(this, 'Batch Inference', { + transformJobName: 'MyTransformJob', + modelName: 'MyModelName', + role, + transformInput: { + transformDataSource: { + s3DataSource: { + s3Uri: 's3://inputbucket/train', + s3DataType: S3DataType.S3Prefix, + } + } + }, + transformOutput: { + s3OutputPath: 's3://outputbucket/TransformJobOutputPath', + }, + transformResources: { + instanceCount: 1, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.XLarge), + } }); -const task = new sfn.Task(this, 'Batch Inference', { - task: transformJob -}); ``` ## SNS diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts index b9a5cd0a9f062..4dad4bf2c295c 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts @@ -10,9 +10,9 @@ export * from './sqs/send-to-queue'; export * from './sqs/send-message'; export * from './ecs/run-ecs-ec2-task'; export * from './ecs/run-ecs-fargate-task'; -export * from './sagemaker/sagemaker-task-base-types'; -export * from './sagemaker/sagemaker-train-task'; -export * from './sagemaker/sagemaker-transform-task'; +export * from './sagemaker/base-types'; +export * from './sagemaker/create-training-job'; +export * from './sagemaker/create-transform-job'; export * from './start-execution'; export * from './stepfunctions/start-execution'; export * from './evaluate-expression'; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/sagemaker-task-base-types.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/base-types.ts similarity index 98% rename from packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/sagemaker-task-base-types.ts rename to packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/base-types.ts index 1db442e348f75..6f1c5f03dcc37 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/sagemaker-task-base-types.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/base-types.ts @@ -5,13 +5,13 @@ import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as s3 from '@aws-cdk/aws-s3'; import * as sfn from '@aws-cdk/aws-stepfunctions'; -import { Construct, Duration } from '@aws-cdk/core'; +import { Construct, Duration, Size } from '@aws-cdk/core'; /** * Task to train a machine learning model using Amazon SageMaker * @experimental */ -export interface ISageMakerTask extends sfn.IStepFunctionsTask, iam.IGrantable {} +export interface ISageMakerTask extends iam.IGrantable {} /** * Specify the training algorithm and algorithm-specific metadata @@ -230,7 +230,7 @@ export interface ResourceConfig { * * @default 10 GB EBS volume. */ - readonly volumeSizeInGB: number; + readonly volumeSize: Size; } /** @@ -622,7 +622,7 @@ export interface TransformResources { * * @default - None */ - readonly volumeKmsKeyId?: kms.Key; + readonly volumeEncryptionKey?: kms.IKey; } /** diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/sagemaker-train-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-training-job.ts similarity index 52% rename from packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/sagemaker-train-task.ts rename to packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-training-job.ts index 758e8a065dc8c..f541a0e692a4f 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/sagemaker-train-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-training-job.ts @@ -1,18 +1,16 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as sfn from '@aws-cdk/aws-stepfunctions'; -import { Duration, Lazy, Stack } from '@aws-cdk/core'; -import { getResourceArn } from '../resource-arn-suffix'; -import { AlgorithmSpecification, Channel, InputMode, OutputDataConfig, ResourceConfig, - S3DataType, StoppingCondition, VpcConfig } from './sagemaker-task-base-types'; +import { Construct, Duration, Lazy, Size, Stack } from '@aws-cdk/core'; +import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; +import { AlgorithmSpecification, Channel, InputMode, OutputDataConfig, ResourceConfig, S3DataType, StoppingCondition, VpcConfig } from './base-types'; /** * Properties for creating an Amazon SageMaker training job * * @experimental */ -export interface SagemakerTrainTaskProps { - +export interface SageMakerCreateTrainingJobProps extends sfn.TaskStateBaseProps { /** * Training Job Name. */ @@ -24,19 +22,10 @@ export interface SagemakerTrainTaskProps { * * See https://docs.aws.amazon.com/fr_fr/sagemaker/latest/dg/sagemaker-roles.html#sagemaker-roles-createtrainingjob-perms * - * @default - a role with appropriate permissions will be created. + * @default - a role will be created. */ readonly role?: iam.IRole; - /** - * The service integration pattern indicates different ways to call SageMaker APIs. - * - * The valid value is either FIRE_AND_FORGET or SYNC. - * - * @default FIRE_AND_FORGET - */ - readonly integrationPattern?: sfn.ServiceIntegrationPattern; - /** * Identifies the training algorithm to use. */ @@ -49,7 +38,7 @@ export interface SagemakerTrainTaskProps { * * @default - No hyperparameters */ - readonly hyperparameters?: {[key: string]: any}; + readonly hyperparameters?: { [key: string]: any }; /** * Describes the various datasets (e.g. train, validation, test) and the Amazon S3 location where stored. @@ -61,7 +50,7 @@ export interface SagemakerTrainTaskProps { * * @default - No tags */ - readonly tags?: {[key: string]: string}; + readonly tags?: { [key: string]: string }; /** * Identifies the Amazon S3 location where you want Amazon SageMaker to save the results of model training. @@ -95,13 +84,20 @@ export interface SagemakerTrainTaskProps { * * @experimental */ -export class SagemakerTrainTask implements iam.IGrantable, ec2.IConnectable, sfn.IStepFunctionsTask { +export class SageMakerCreateTrainingJob extends sfn.TaskStateBase implements iam.IGrantable, ec2.IConnectable { + private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ + sfn.IntegrationPattern.REQUEST_RESPONSE, + sfn.IntegrationPattern.RUN_JOB, + ]; /** * Allows specify security group connections for instances of this fleet. */ public readonly connections: ec2.Connections = new ec2.Connections(); + protected readonly taskPolicies?: iam.PolicyStatement[]; + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + /** * The Algorithm Specification */ @@ -126,27 +122,21 @@ export class SagemakerTrainTask implements iam.IGrantable, ec2.IConnectable, sfn private securityGroup?: ec2.ISecurityGroup; private readonly securityGroups: ec2.ISecurityGroup[] = []; private readonly subnets?: string[]; - private readonly integrationPattern: sfn.ServiceIntegrationPattern; + private readonly integrationPattern: sfn.IntegrationPattern; private _role?: iam.IRole; private _grantPrincipal?: iam.IPrincipal; - constructor(private readonly props: SagemakerTrainTaskProps) { - this.integrationPattern = props.integrationPattern || sfn.ServiceIntegrationPattern.FIRE_AND_FORGET; + constructor(scope: Construct, id: string, private readonly props: SageMakerCreateTrainingJobProps) { + super(scope, id, props); - const supportedPatterns = [ - sfn.ServiceIntegrationPattern.FIRE_AND_FORGET, - sfn.ServiceIntegrationPattern.SYNC, - ]; - - if (!supportedPatterns.includes(this.integrationPattern)) { - throw new Error(`Invalid Service Integration Pattern: ${this.integrationPattern} is not supported to call SageMaker.`); - } + this.integrationPattern = props.integrationPattern || sfn.IntegrationPattern.REQUEST_RESPONSE; + validatePatternSupported(this.integrationPattern, SageMakerCreateTrainingJob.SUPPORTED_INTEGRATION_PATTERNS); // set the default resource config if not defined. this.resourceConfig = props.resourceConfig || { instanceCount: 1, instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.XLARGE), - volumeSizeInGB: 10, + volumeSize: Size.gibibytes(10), }; // set the stopping condition if not defined @@ -155,20 +145,22 @@ export class SagemakerTrainTask implements iam.IGrantable, ec2.IConnectable, sfn }; // check that either algorithm name or image is defined - if ((!props.algorithmSpecification.algorithmName) && (!props.algorithmSpecification.trainingImage)) { + if (!props.algorithmSpecification.algorithmName && !props.algorithmSpecification.trainingImage) { throw new Error('Must define either an algorithm name or training image URI in the algorithm specification'); } // set the input mode to 'File' if not defined - this.algorithmSpecification = ( props.algorithmSpecification.trainingInputMode ) ? - ( props.algorithmSpecification ) : - ( { ...props.algorithmSpecification, trainingInputMode: InputMode.FILE } ); + this.algorithmSpecification = props.algorithmSpecification.trainingInputMode + ? props.algorithmSpecification + : { ...props.algorithmSpecification, trainingInputMode: InputMode.FILE }; // set the S3 Data type of the input data config objects to be 'S3Prefix' if not defined - this.inputDataConfig = props.inputDataConfig.map(config => { + this.inputDataConfig = props.inputDataConfig.map((config) => { if (!config.dataSource.s3DataSource.s3DataType) { - return Object.assign({}, config, { dataSource: { s3DataSource: - { ...config.dataSource.s3DataSource, s3DataType: S3DataType.S3_PREFIX } } }); + return { + ...config, + dataSource: { s3DataSource: { ...config.dataSource.s3DataSource, s3DataType: S3DataType.S3_PREFIX } }, + }; } else { return config; } @@ -177,9 +169,10 @@ export class SagemakerTrainTask implements iam.IGrantable, ec2.IConnectable, sfn // add the security groups to the connections object if (props.vpcConfig) { this.vpc = props.vpcConfig.vpc; - this.subnets = (props.vpcConfig.subnets) ? - (this.vpc.selectSubnets(props.vpcConfig.subnets).subnetIds) : this.vpc.selectSubnets().subnetIds; + this.subnets = props.vpcConfig.subnets ? this.vpc.selectSubnets(props.vpcConfig.subnets).subnetIds : this.vpc.selectSubnets().subnetIds; } + + this.taskPolicies = this.makePolicyStatements(); } /** @@ -211,137 +204,84 @@ export class SagemakerTrainTask implements iam.IGrantable, ec2.IConnectable, sfn this.securityGroups.push(securityGroup); } - public bind(task: sfn.Task): sfn.StepFunctionsTaskConfig { - // set the sagemaker role or create new one - this._grantPrincipal = this._role = this.props.role || new iam.Role(task, 'SagemakerRole', { - assumedBy: new iam.ServicePrincipal('sagemaker.amazonaws.com'), - inlinePolicies: { - CreateTrainingJob: new iam.PolicyDocument({ - statements: [ - new iam.PolicyStatement({ - actions: [ - 'cloudwatch:PutMetricData', - 'logs:CreateLogStream', - 'logs:PutLogEvents', - 'logs:CreateLogGroup', - 'logs:DescribeLogStreams', - 'ecr:GetAuthorizationToken', - ...this.props.vpcConfig - ? [ - 'ec2:CreateNetworkInterface', - 'ec2:CreateNetworkInterfacePermission', - 'ec2:DeleteNetworkInterface', - 'ec2:DeleteNetworkInterfacePermission', - 'ec2:DescribeNetworkInterfaces', - 'ec2:DescribeVpcs', - 'ec2:DescribeDhcpOptions', - 'ec2:DescribeSubnets', - 'ec2:DescribeSecurityGroups', - ] - : [], - ], - resources: ['*'], // Those permissions cannot be resource-scoped - }), - ], - }), - }, - }); - - if (this.props.outputDataConfig.encryptionKey) { - this.props.outputDataConfig.encryptionKey.grantEncrypt(this._role); - } - - if (this.props.resourceConfig && this.props.resourceConfig.volumeEncryptionKey) { - this.props.resourceConfig.volumeEncryptionKey.grant(this._role, 'kms:CreateGrant'); - } - - // create a security group if not defined - if (this.vpc && this.securityGroup === undefined) { - this.securityGroup = new ec2.SecurityGroup(task, 'TrainJobSecurityGroup', { - vpc: this.vpc, - }); - this.connections.addSecurityGroup(this.securityGroup); - this.securityGroups.push(this.securityGroup); - } - + protected renderTask(): any { return { - resourceArn: getResourceArn('sagemaker', 'createTrainingJob', this.integrationPattern), - parameters: this.renderParameters(), - policyStatements: this.makePolicyStatements(task), + Resource: integrationResourceArn('sagemaker', 'createTrainingJob', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject(this.renderParameters()), }; } - private renderParameters(): {[key: string]: any} { + private renderParameters(): { [key: string]: any } { return { TrainingJobName: this.props.trainingJobName, RoleArn: this._role!.roleArn, - ...(this.renderAlgorithmSpecification(this.algorithmSpecification)), - ...(this.renderInputDataConfig(this.inputDataConfig)), - ...(this.renderOutputDataConfig(this.props.outputDataConfig)), - ...(this.renderResourceConfig(this.resourceConfig)), - ...(this.renderStoppingCondition(this.stoppingCondition)), - ...(this.renderHyperparameters(this.props.hyperparameters)), - ...(this.renderTags(this.props.tags)), - ...(this.renderVpcConfig(this.props.vpcConfig)), + ...this.renderAlgorithmSpecification(this.algorithmSpecification), + ...this.renderInputDataConfig(this.inputDataConfig), + ...this.renderOutputDataConfig(this.props.outputDataConfig), + ...this.renderResourceConfig(this.resourceConfig), + ...this.renderStoppingCondition(this.stoppingCondition), + ...this.renderHyperparameters(this.props.hyperparameters), + ...this.renderTags(this.props.tags), + ...this.renderVpcConfig(this.props.vpcConfig), }; } - private renderAlgorithmSpecification(spec: AlgorithmSpecification): {[key: string]: any} { + private renderAlgorithmSpecification(spec: AlgorithmSpecification): { [key: string]: any } { return { AlgorithmSpecification: { TrainingInputMode: spec.trainingInputMode, - ...(spec.trainingImage) ? { TrainingImage: spec.trainingImage.bind(this).imageUri } : {}, - ...(spec.algorithmName) ? { AlgorithmName: spec.algorithmName } : {}, - ...(spec.metricDefinitions) ? - { MetricDefinitions: spec.metricDefinitions - .map(metric => ({ Name: metric.name, Regex: metric.regex })) } : {}, + ...(spec.trainingImage ? { TrainingImage: spec.trainingImage.bind(this).imageUri } : {}), + ...(spec.algorithmName ? { AlgorithmName: spec.algorithmName } : {}), + ...(spec.metricDefinitions + ? { MetricDefinitions: spec.metricDefinitions.map((metric) => ({ Name: metric.name, Regex: metric.regex })) } + : {}), }, }; } - private renderInputDataConfig(config: Channel[]): {[key: string]: any} { + private renderInputDataConfig(config: Channel[]): { [key: string]: any } { return { - InputDataConfig: config.map(channel => ({ + InputDataConfig: config.map((channel) => ({ ChannelName: channel.channelName, DataSource: { S3DataSource: { S3Uri: channel.dataSource.s3DataSource.s3Location.bind(this, { forReading: true }).uri, S3DataType: channel.dataSource.s3DataSource.s3DataType, - ...(channel.dataSource.s3DataSource.s3DataDistributionType) ? - { S3DataDistributionType: channel.dataSource.s3DataSource.s3DataDistributionType} : {}, - ...(channel.dataSource.s3DataSource.attributeNames) ? - { AtttributeNames: channel.dataSource.s3DataSource.attributeNames } : {}, + ...(channel.dataSource.s3DataSource.s3DataDistributionType + ? { S3DataDistributionType: channel.dataSource.s3DataSource.s3DataDistributionType } + : {}), + ...(channel.dataSource.s3DataSource.attributeNames ? { AtttributeNames: channel.dataSource.s3DataSource.attributeNames } : {}), }, }, - ...(channel.compressionType) ? { CompressionType: channel.compressionType } : {}, - ...(channel.contentType) ? { ContentType: channel.contentType } : {}, - ...(channel.inputMode) ? { InputMode: channel.inputMode } : {}, - ...(channel.recordWrapperType) ? { RecordWrapperType: channel.recordWrapperType } : {}, + ...(channel.compressionType ? { CompressionType: channel.compressionType } : {}), + ...(channel.contentType ? { ContentType: channel.contentType } : {}), + ...(channel.inputMode ? { InputMode: channel.inputMode } : {}), + ...(channel.recordWrapperType ? { RecordWrapperType: channel.recordWrapperType } : {}), })), }; } - private renderOutputDataConfig(config: OutputDataConfig): {[key: string]: any} { + private renderOutputDataConfig(config: OutputDataConfig): { [key: string]: any } { return { OutputDataConfig: { S3OutputPath: config.s3OutputLocation.bind(this, { forWriting: true }).uri, - ...(config.encryptionKey) ? { KmsKeyId: config.encryptionKey.keyArn } : {}, + ...(config.encryptionKey ? { KmsKeyId: config.encryptionKey.keyArn } : {}), }, }; } - private renderResourceConfig(config: ResourceConfig): {[key: string]: any} { + private renderResourceConfig(config: ResourceConfig): { [key: string]: any } { return { ResourceConfig: { InstanceCount: config.instanceCount, InstanceType: 'ml.' + config.instanceType, - VolumeSizeInGB: config.volumeSizeInGB, - ...(config.volumeEncryptionKey) ? { VolumeKmsKeyId: config.volumeEncryptionKey.keyArn } : {}, + VolumeSizeInGB: config.volumeSize.toGibibytes(), + ...(config.volumeEncryptionKey ? { VolumeKmsKeyId: config.volumeEncryptionKey.keyArn } : {}), }, }; } - private renderStoppingCondition(config: StoppingCondition): {[key: string]: any} { + private renderStoppingCondition(config: StoppingCondition): { [key: string]: any } { return { StoppingCondition: { MaxRuntimeInSeconds: config.maxRuntime && config.maxRuntime.toSeconds(), @@ -349,23 +289,81 @@ export class SagemakerTrainTask implements iam.IGrantable, ec2.IConnectable, sfn }; } - private renderHyperparameters(params: {[key: string]: any} | undefined): {[key: string]: any} { - return (params) ? { HyperParameters: params } : {}; + private renderHyperparameters(params: { [key: string]: any } | undefined): { [key: string]: any } { + return params ? { HyperParameters: params } : {}; } - private renderTags(tags: {[key: string]: any} | undefined): {[key: string]: any} { - return (tags) ? { Tags: Object.keys(tags).map(key => ({ Key: key, Value: tags[key] })) } : {}; + private renderTags(tags: { [key: string]: any } | undefined): { [key: string]: any } { + return tags ? { Tags: Object.keys(tags).map((key) => ({ Key: key, Value: tags[key] })) } : {}; } - private renderVpcConfig(config: VpcConfig | undefined): {[key: string]: any} { - return (config) ? { VpcConfig: { - SecurityGroupIds: Lazy.listValue({ produce: () => (this.securityGroups.map(sg => (sg.securityGroupId))) }), - Subnets: this.subnets, - }} : {}; + private renderVpcConfig(config: VpcConfig | undefined): { [key: string]: any } { + return config + ? { + VpcConfig: { + SecurityGroupIds: Lazy.listValue({ produce: () => this.securityGroups.map((sg) => sg.securityGroupId) }), + Subnets: this.subnets, + }, + } + : {}; } - private makePolicyStatements(task: sfn.Task): iam.PolicyStatement[] { - const stack = Stack.of(task); + private makePolicyStatements(): iam.PolicyStatement[] { + // set the sagemaker role or create new one + this._grantPrincipal = this._role = + this.props.role || + new iam.Role(this, 'SagemakerRole', { + assumedBy: new iam.ServicePrincipal('sagemaker.amazonaws.com'), + inlinePolicies: { + CreateTrainingJob: new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + actions: [ + 'cloudwatch:PutMetricData', + 'logs:CreateLogStream', + 'logs:PutLogEvents', + 'logs:CreateLogGroup', + 'logs:DescribeLogStreams', + 'ecr:GetAuthorizationToken', + ...(this.props.vpcConfig + ? [ + 'ec2:CreateNetworkInterface', + 'ec2:CreateNetworkInterfacePermission', + 'ec2:DeleteNetworkInterface', + 'ec2:DeleteNetworkInterfacePermission', + 'ec2:DescribeNetworkInterfaces', + 'ec2:DescribeVpcs', + 'ec2:DescribeDhcpOptions', + 'ec2:DescribeSubnets', + 'ec2:DescribeSecurityGroups', + ] + : []), + ], + resources: ['*'], // Those permissions cannot be resource-scoped + }), + ], + }), + }, + }); + + if (this.props.outputDataConfig.encryptionKey) { + this.props.outputDataConfig.encryptionKey.grantEncrypt(this._role); + } + + if (this.props.resourceConfig && this.props.resourceConfig.volumeEncryptionKey) { + this.props.resourceConfig.volumeEncryptionKey.grant(this._role, 'kms:CreateGrant'); + } + + // create a security group if not defined + if (this.vpc && this.securityGroup === undefined) { + this.securityGroup = new ec2.SecurityGroup(this, 'TrainJobSecurityGroup', { + vpc: this.vpc, + }); + this.connections.addSecurityGroup(this.securityGroup); + this.securityGroups.push(this.securityGroup); + } + + const stack = Stack.of(this); // https://docs.aws.amazon.com/step-functions/latest/dg/sagemaker-iam.html const policyStatements = [ @@ -393,15 +391,19 @@ export class SagemakerTrainTask implements iam.IGrantable, ec2.IConnectable, sfn }), ]; - if (this.integrationPattern === sfn.ServiceIntegrationPattern.SYNC) { - policyStatements.push(new iam.PolicyStatement({ - actions: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], - resources: [stack.formatArn({ - service: 'events', - resource: 'rule', - resourceName: 'StepFunctionsGetEventsForSageMakerTrainingJobsRule', - })], - })); + if (this.integrationPattern === sfn.IntegrationPattern.RUN_JOB) { + policyStatements.push( + new iam.PolicyStatement({ + actions: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], + resources: [ + stack.formatArn({ + service: 'events', + resource: 'rule', + resourceName: 'StepFunctionsGetEventsForSageMakerTrainingJobsRule', + }), + ], + }), + ); } return policyStatements; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/sagemaker-transform-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-transform-job.ts similarity index 51% rename from packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/sagemaker-transform-task.ts rename to packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-transform-job.ts index 5d4449d052a17..111a15500443e 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/sagemaker-transform-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-transform-job.ts @@ -1,17 +1,16 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as sfn from '@aws-cdk/aws-stepfunctions'; -import { Stack } from '@aws-cdk/core'; -import { getResourceArn } from '../resource-arn-suffix'; -import { BatchStrategy, S3DataType, TransformInput, TransformOutput, TransformResources } from './sagemaker-task-base-types'; +import { Construct, Size, Stack } from '@aws-cdk/core'; +import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; +import { BatchStrategy, S3DataType, TransformInput, TransformOutput, TransformResources } from './base-types'; /** * Properties for creating an Amazon SageMaker training job task * * @experimental */ -export interface SagemakerTransformProps { - +export interface SageMakerCreateTransformJobProps extends sfn.TaskStateBaseProps { /** * Training Job Name. */ @@ -24,15 +23,6 @@ export interface SagemakerTransformProps { */ readonly role?: iam.IRole; - /** - * The service integration pattern indicates different ways to call SageMaker APIs. - * - * The valid value is either FIRE_AND_FORGET or SYNC. - * - * @default FIRE_AND_FORGET - */ - readonly integrationPattern?: sfn.ServiceIntegrationPattern; - /** * Number of records to include in a mini-batch for an HTTP inference request. * @@ -45,7 +35,7 @@ export interface SagemakerTransformProps { * * @default - No environment variables */ - readonly environment?: {[key: string]: string}; + readonly environment?: { [key: string]: string }; /** * Maximum number of parallel requests that can be sent to each instance in a transform job. @@ -60,7 +50,7 @@ export interface SagemakerTransformProps { * * @default 6 */ - readonly maxPayloadInMB?: number; + readonly maxPayload?: Size; /** * Name of the model that you want to use for the transform job. @@ -72,7 +62,7 @@ export interface SagemakerTransformProps { * * @default - No tags */ - readonly tags?: {[key: string]: string}; + readonly tags?: { [key: string]: string }; /** * Dataset to be transformed and the Amazon S3 location where it is stored. @@ -97,7 +87,14 @@ export interface SagemakerTransformProps { * * @experimental */ -export class SagemakerTransformTask implements sfn.IStepFunctionsTask { +export class SageMakerCreateTransformJob extends sfn.TaskStateBase { + private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ + sfn.IntegrationPattern.REQUEST_RESPONSE, + sfn.IntegrationPattern.RUN_JOB, + ]; + + protected readonly taskPolicies?: iam.PolicyStatement[]; + protected readonly taskMetrics?: sfn.TaskMetricsConfig; /** * Dataset to be transformed and the Amazon S3 location where it is stored. @@ -108,20 +105,13 @@ export class SagemakerTransformTask implements sfn.IStepFunctionsTask { * ML compute instances for the transform job. */ private readonly transformResources: TransformResources; - private readonly integrationPattern: sfn.ServiceIntegrationPattern; + private readonly integrationPattern: sfn.IntegrationPattern; private _role?: iam.IRole; - constructor(private readonly props: SagemakerTransformProps) { - this.integrationPattern = props.integrationPattern || sfn.ServiceIntegrationPattern.FIRE_AND_FORGET; - - const supportedPatterns = [ - sfn.ServiceIntegrationPattern.FIRE_AND_FORGET, - sfn.ServiceIntegrationPattern.SYNC, - ]; - - if (!supportedPatterns.includes(this.integrationPattern)) { - throw new Error(`Invalid Service Integration Pattern: ${this.integrationPattern} is not supported to call SageMaker.`); - } + constructor(scope: Construct, id: string, private readonly props: SageMakerCreateTransformJobProps) { + super(scope, id, props); + this.integrationPattern = props.integrationPattern || sfn.IntegrationPattern.REQUEST_RESPONSE; + validatePatternSupported(this.integrationPattern, SageMakerCreateTransformJob.SUPPORTED_INTEGRATION_PATTERNS); // set the sagemaker role or create new one if (props.role) { @@ -129,38 +119,25 @@ export class SagemakerTransformTask implements sfn.IStepFunctionsTask { } // set the S3 Data type of the input data config objects to be 'S3Prefix' if not defined - this.transformInput = (props.transformInput.transformDataSource.s3DataSource.s3DataType) ? (props.transformInput) : - Object.assign({}, props.transformInput, - { transformDataSource: - { s3DataSource: - { ...props.transformInput.transformDataSource.s3DataSource, - s3DataType: S3DataType.S3_PREFIX, - }, - }, - }); + this.transformInput = props.transformInput.transformDataSource.s3DataSource.s3DataType + ? props.transformInput + : Object.assign({}, props.transformInput, { + transformDataSource: { s3DataSource: { ...props.transformInput.transformDataSource.s3DataSource, s3DataType: S3DataType.S3_PREFIX } }, + }); // set the default value for the transform resources this.transformResources = props.transformResources || { instanceCount: 1, instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.XLARGE), }; - } - public bind(task: sfn.Task): sfn.StepFunctionsTaskConfig { - // create new role if doesn't exist - if (this._role === undefined) { - this._role = new iam.Role(task, 'SagemakerTransformRole', { - assumedBy: new iam.ServicePrincipal('sagemaker.amazonaws.com'), - managedPolicies: [ - iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSageMakerFullAccess'), - ], - }); - } + this.taskPolicies = this.makePolicyStatements(); + } + protected renderTask(): any { return { - resourceArn: getResourceArn('sagemaker', 'createTransformJob', this.integrationPattern), - parameters: this.renderParameters(), - policyStatements: this.makePolicyStatements(task), + Resource: integrationResourceArn('sagemaker', 'createTransformJob', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject(this.renderParameters()), }; } @@ -176,78 +153,88 @@ export class SagemakerTransformTask implements sfn.IStepFunctionsTask { return this._role; } - private renderParameters(): {[key: string]: any} { + private renderParameters(): { [key: string]: any } { return { - ...(this.props.batchStrategy) ? { BatchStrategy: this.props.batchStrategy } : {}, - ...(this.renderEnvironment(this.props.environment)), - ...(this.props.maxConcurrentTransforms) ? { MaxConcurrentTransforms: this.props.maxConcurrentTransforms } : {}, - ...(this.props.maxPayloadInMB) ? { MaxPayloadInMB: this.props.maxPayloadInMB } : {}, + ...(this.props.batchStrategy ? { BatchStrategy: this.props.batchStrategy } : {}), + ...this.renderEnvironment(this.props.environment), + ...(this.props.maxConcurrentTransforms ? { MaxConcurrentTransforms: this.props.maxConcurrentTransforms } : {}), + ...(this.props.maxPayload ? { MaxPayloadInMB: this.props.maxPayload.toMebibytes() } : {}), ModelName: this.props.modelName, - ...(this.renderTags(this.props.tags)), - ...(this.renderTransformInput(this.transformInput)), + ...this.renderTags(this.props.tags), + ...this.renderTransformInput(this.transformInput), TransformJobName: this.props.transformJobName, - ...(this.renderTransformOutput(this.props.transformOutput)), - ...(this.renderTransformResources(this.transformResources)), + ...this.renderTransformOutput(this.props.transformOutput), + ...this.renderTransformResources(this.transformResources), }; } - private renderTransformInput(input: TransformInput): {[key: string]: any} { + private renderTransformInput(input: TransformInput): { [key: string]: any } { return { TransformInput: { - ...(input.compressionType) ? { CompressionType: input.compressionType } : {}, - ...(input.contentType) ? { ContentType: input.contentType } : {}, + ...(input.compressionType ? { CompressionType: input.compressionType } : {}), + ...(input.contentType ? { ContentType: input.contentType } : {}), DataSource: { S3DataSource: { S3Uri: input.transformDataSource.s3DataSource.s3Uri, S3DataType: input.transformDataSource.s3DataSource.s3DataType, }, }, - ...(input.splitType) ? { SplitType: input.splitType } : {}, + ...(input.splitType ? { SplitType: input.splitType } : {}), }, }; } - private renderTransformOutput(output: TransformOutput): {[key: string]: any} { + private renderTransformOutput(output: TransformOutput): { [key: string]: any } { return { TransformOutput: { S3OutputPath: output.s3OutputPath, - ...(output.encryptionKey) ? { KmsKeyId: output.encryptionKey.keyArn } : {}, - ...(output.accept) ? { Accept: output.accept } : {}, - ...(output.assembleWith) ? { AssembleWith: output.assembleWith } : {}, + ...(output.encryptionKey ? { KmsKeyId: output.encryptionKey.keyArn } : {}), + ...(output.accept ? { Accept: output.accept } : {}), + ...(output.assembleWith ? { AssembleWith: output.assembleWith } : {}), }, }; } - private renderTransformResources(resources: TransformResources): {[key: string]: any} { + private renderTransformResources(resources: TransformResources): { [key: string]: any } { return { TransformResources: { InstanceCount: resources.instanceCount, InstanceType: 'ml.' + resources.instanceType, - ...(resources.volumeKmsKeyId) ? { VolumeKmsKeyId: resources.volumeKmsKeyId.keyArn } : {}, + ...(resources.volumeEncryptionKey ? { VolumeKmsKeyId: resources.volumeEncryptionKey.keyArn } : {}), }, }; } - private renderEnvironment(environment: {[key: string]: any} | undefined): {[key: string]: any} { - return (environment) ? { Environment: environment } : {}; + private renderEnvironment(environment: { [key: string]: any } | undefined): { [key: string]: any } { + return environment ? { Environment: environment } : {}; } - private renderTags(tags: {[key: string]: any} | undefined): {[key: string]: any} { - return (tags) ? { Tags: Object.keys(tags).map(key => ({ Key: key, Value: tags[key] })) } : {}; + private renderTags(tags: { [key: string]: any } | undefined): { [key: string]: any } { + return tags ? { Tags: Object.keys(tags).map((key) => ({ Key: key, Value: tags[key] })) } : {}; } - private makePolicyStatements(task: sfn.Task): iam.PolicyStatement[] { - const stack = Stack.of(task); + private makePolicyStatements(): iam.PolicyStatement[] { + const stack = Stack.of(this); + + // create new role if doesn't exist + if (this._role === undefined) { + this._role = new iam.Role(this, 'SagemakerTransformRole', { + assumedBy: new iam.ServicePrincipal('sagemaker.amazonaws.com'), + managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSageMakerFullAccess')], + }); + } // https://docs.aws.amazon.com/step-functions/latest/dg/sagemaker-iam.html const policyStatements = [ new iam.PolicyStatement({ actions: ['sagemaker:CreateTransformJob', 'sagemaker:DescribeTransformJob', 'sagemaker:StopTransformJob'], - resources: [stack.formatArn({ - service: 'sagemaker', - resource: 'transform-job', - resourceName: '*', - })], + resources: [ + stack.formatArn({ + service: 'sagemaker', + resource: 'transform-job', + resourceName: '*', + }), + ], }), new iam.PolicyStatement({ actions: ['sagemaker:ListTags'], @@ -262,15 +249,19 @@ export class SagemakerTransformTask implements sfn.IStepFunctionsTask { }), ]; - if (this.integrationPattern === sfn.ServiceIntegrationPattern.SYNC) { - policyStatements.push(new iam.PolicyStatement({ - actions: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], - resources: [stack.formatArn({ - service: 'events', - resource: 'rule', - resourceName: 'StepFunctionsGetEventsForSageMakerTransformJobsRule', - }) ], - })); + if (this.integrationPattern === sfn.IntegrationPattern.RUN_JOB) { + policyStatements.push( + new iam.PolicyStatement({ + actions: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], + resources: [ + stack.formatArn({ + service: 'events', + resource: 'rule', + resourceName: 'StepFunctionsGetEventsForSageMakerTransformJobsRule', + }), + ], + }), + ); } return policyStatements; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/sagemaker-training-job.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-training-job.test.ts similarity index 92% rename from packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/sagemaker-training-job.test.ts rename to packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-training-job.test.ts index 58b7d314b535d..4f02f9ac048a1 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/sagemaker-training-job.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-training-job.test.ts @@ -6,6 +6,7 @@ import * as s3 from '@aws-cdk/aws-s3'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; import * as tasks from '../../lib'; +import { SageMakerCreateTrainingJob } from '../../lib/sagemaker/create-training-job'; let stack: cdk.Stack; @@ -16,7 +17,7 @@ beforeEach(() => { test('create basic training job', () => { // WHEN - const task = new sfn.Task(stack, 'TrainSagemaker', { task: new tasks.SagemakerTrainTask({ + const task = new SageMakerCreateTrainingJob(stack, 'TrainSagemaker', { trainingJobName: 'MyTrainJob', algorithmSpecification: { algorithmName: 'BlazingText', @@ -34,7 +35,7 @@ test('create basic training job', () => { outputDataConfig: { s3OutputLocation: tasks.S3Location.fromBucket(s3.Bucket.fromBucketName(stack, 'OutputBucket', 'mybucket'), 'myoutputpath'), }, - })}); + }); // THEN expect(stack.resolve(task.toStateJson())).toEqual({ @@ -91,8 +92,8 @@ test('create basic training job', () => { test('Task throws if WAIT_FOR_TASK_TOKEN is supplied as service integration pattern', () => { expect(() => { - new sfn.Task(stack, 'TrainSagemaker', { task: new tasks.SagemakerTrainTask({ - integrationPattern: sfn.ServiceIntegrationPattern.WAIT_FOR_TASK_TOKEN, + new SageMakerCreateTrainingJob(stack, 'TrainSagemaker', { + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, trainingJobName: 'MyTrainJob', algorithmSpecification: { algorithmName: 'BlazingText', @@ -110,8 +111,8 @@ test('Task throws if WAIT_FOR_TASK_TOKEN is supplied as service integration patt outputDataConfig: { s3OutputLocation: tasks.S3Location.fromBucket(s3.Bucket.fromBucketName(stack, 'OutputBucket', 'mybucket'), 'myoutputpath'), }, - })}); - }).toThrow(/Invalid Service Integration Pattern: WAIT_FOR_TASK_TOKEN is not supported to call SageMaker./i); + }); + }).toThrow(/Unsupported service integration pattern. Supported Patterns: REQUEST_RESPONSE,RUN_JOB. Received: WAIT_FOR_TASK_TOKEN/i); }); test('create complex training job', () => { @@ -128,9 +129,9 @@ test('create complex training job', () => { ], }); - const trainTask = new tasks.SagemakerTrainTask({ + const trainTask = new SageMakerCreateTrainingJob(stack, 'TrainSagemaker', { trainingJobName: 'MyTrainJob', - integrationPattern: sfn.ServiceIntegrationPattern.SYNC, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, role, algorithmSpecification: { algorithmName: 'BlazingText', @@ -177,7 +178,7 @@ test('create complex training job', () => { resourceConfig: { instanceCount: 1, instanceType: ec2.InstanceType.of(ec2.InstanceClass.P3, ec2.InstanceSize.XLARGE2), - volumeSizeInGB: 50, + volumeSize: cdk.Size.gibibytes(50), volumeEncryptionKey: kmsKey, }, stoppingCondition: { @@ -191,10 +192,9 @@ test('create complex training job', () => { }, }); trainTask.addSecurityGroup(securityGroup); - const task = new sfn.Task(stack, 'TrainSagemaker', { task: trainTask }); // THEN - expect(stack.resolve(task.toStateJson())).toEqual({ + expect(stack.resolve(trainTask.toStateJson())).toEqual({ Type: 'Task', Resource: { 'Fn::Join': [ @@ -272,8 +272,8 @@ test('create complex training job', () => { ], VpcConfig: { SecurityGroupIds: [ - { 'Fn::GetAtt': [ 'SecurityGroupDD263621', 'GroupId' ] }, { 'Fn::GetAtt': [ 'TrainSagemakerTrainJobSecurityGroup7C858EB9', 'GroupId' ] }, + { 'Fn::GetAtt': [ 'SecurityGroupDD263621', 'GroupId' ] }, ], Subnets: [ { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, @@ -293,7 +293,7 @@ test('pass param to training job', () => { ], }); - const task = new sfn.Task(stack, 'TrainSagemaker', { task: new tasks.SagemakerTrainTask({ + const task = new SageMakerCreateTrainingJob(stack, 'TrainSagemaker', { trainingJobName: sfn.Data.stringAt('$.JobName'), role, algorithmSpecification: { @@ -317,12 +317,12 @@ test('pass param to training job', () => { resourceConfig: { instanceCount: 1, instanceType: ec2.InstanceType.of(ec2.InstanceClass.P3, ec2.InstanceSize.XLARGE2), - volumeSizeInGB: 50, + volumeSize: cdk.Size.gibibytes(50), }, stoppingCondition: { maxRuntime: cdk.Duration.hours(1), }, - })}); + }); // THEN expect(stack.resolve(task.toStateJson())).toEqual({ @@ -377,7 +377,7 @@ test('pass param to training job', () => { test('Cannot create a SageMaker train task with both algorithm name and image name missing', () => { - expect(() => new tasks.SagemakerTrainTask({ + expect(() => new SageMakerCreateTrainingJob(stack, 'SageMakerTrainingTask', { trainingJobName: 'myTrainJob', algorithmSpecification: {}, inputDataConfig: [ diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/sagemaker-transform-job.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-transform-job.test.ts similarity index 88% rename from packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/sagemaker-transform-job.test.ts rename to packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-transform-job.test.ts index c08a28bb0c973..c53233523cfa7 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/sagemaker-transform-job.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-transform-job.test.ts @@ -5,6 +5,7 @@ import * as kms from '@aws-cdk/aws-kms'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; import * as tasks from '../../lib'; +import { SageMakerCreateTransformJob } from '../../lib/sagemaker/create-transform-job'; let stack: cdk.Stack; let role: iam.Role; @@ -22,7 +23,7 @@ beforeEach(() => { test('create basic transform job', () => { // WHEN - const task = new sfn.Task(stack, 'TransformTask', { task: new tasks.SagemakerTransformTask({ + const task = new SageMakerCreateTransformJob(stack, 'TransformTask', { transformJobName: 'MyTransformJob', modelName: 'MyModelName', transformInput: { @@ -35,7 +36,7 @@ test('create basic transform job', () => { transformOutput: { s3OutputPath: 's3://outputbucket/prefix', }, - }) }); + }); // THEN expect(stack.resolve(task.toStateJson())).toEqual({ @@ -77,8 +78,8 @@ test('create basic transform job', () => { test('Task throws if WAIT_FOR_TASK_TOKEN is supplied as service integration pattern', () => { expect(() => { - new sfn.Task(stack, 'TransformTask', { task: new tasks.SagemakerTransformTask({ - integrationPattern: sfn.ServiceIntegrationPattern.WAIT_FOR_TASK_TOKEN, + new SageMakerCreateTransformJob(stack, 'TransformTask', { + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, transformJobName: 'MyTransformJob', modelName: 'MyModelName', transformInput: { @@ -91,17 +92,17 @@ test('Task throws if WAIT_FOR_TASK_TOKEN is supplied as service integration patt transformOutput: { s3OutputPath: 's3://outputbucket/prefix', }, - }) }); - }).toThrow(/Invalid Service Integration Pattern: WAIT_FOR_TASK_TOKEN is not supported to call SageMaker./i); + }); + }).toThrow(/Unsupported service integration pattern. Supported Patterns: REQUEST_RESPONSE,RUN_JOB. Received: WAIT_FOR_TASK_TOKEN/); }); test('create complex transform job', () => { // WHEN const kmsKey = new kms.Key(stack, 'Key'); - const task = new sfn.Task(stack, 'TransformTask', { task: new tasks.SagemakerTransformTask({ + const task = new SageMakerCreateTransformJob(stack, 'TransformTask', { transformJobName: 'MyTransformJob', modelName: 'MyModelName', - integrationPattern: sfn.ServiceIntegrationPattern.SYNC, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, role, transformInput: { transformDataSource: { @@ -118,7 +119,7 @@ test('create complex transform job', () => { transformResources: { instanceCount: 1, instanceType: ec2.InstanceType.of(ec2.InstanceClass.P3, ec2.InstanceSize.XLARGE2), - volumeKmsKeyId: kmsKey, + volumeEncryptionKey: kmsKey, }, tags: { Project: 'MyProject', @@ -128,8 +129,8 @@ test('create complex transform job', () => { SOMEVAR: 'myvalue', }, maxConcurrentTransforms: 3, - maxPayloadInMB: 100, - }) }); + maxPayload: cdk.Size.mebibytes(100), + }); // THEN expect(stack.resolve(task.toStateJson())).toEqual({ @@ -182,7 +183,7 @@ test('create complex transform job', () => { test('pass param to transform job', () => { // WHEN - const task = new sfn.Task(stack, 'TransformTask', { task: new tasks.SagemakerTransformTask({ + const task = new SageMakerCreateTransformJob(stack, 'TransformTask', { transformJobName: sfn.Data.stringAt('$.TransformJobName'), modelName: sfn.Data.stringAt('$.ModelName'), role, @@ -201,7 +202,7 @@ test('pass param to transform job', () => { instanceCount: 1, instanceType: ec2.InstanceType.of(ec2.InstanceClass.P3, ec2.InstanceSize.XLARGE2), }, - }) }); + }); // THEN expect(stack.resolve(task.toStateJson())).toEqual({ diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.sagemaker.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.create-training-job.expected.json similarity index 94% rename from packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.sagemaker.expected.json rename to packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.create-training-job.expected.json index 52aeac4dc5de3..cf95e9f59a16e 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.sagemaker.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.create-training-job.expected.json @@ -304,7 +304,7 @@ { "Ref": "AWS::AccountId" }, - ":training-job/MyTrainingJob*" + ":training-job/mytrainingjob*" ] ] } @@ -343,18 +343,28 @@ "StateMachine2E01A3A5": { "Type": "AWS::StepFunctions::StateMachine", "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, "DefinitionString": { "Fn::Join": [ "", [ - "{\"StartAt\":\"TrainTask\",\"States\":{\"TrainTask\":{\"End\":true,\"Parameters\":{\"TrainingJobName\":\"MyTrainingJob\",\"RoleArn\":\"", + "{\"StartAt\":\"TrainTask\",\"States\":{\"TrainTask\":{\"End\":true,\"Type\":\"Task\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::sagemaker:createTrainingJob\",\"Parameters\":{\"TrainingJobName\":\"mytrainingjob\",\"RoleArn\":\"", { "Fn::GetAtt": [ "TrainTaskSagemakerRole0A9B1CDD", "Arn" ] }, - "\",\"AlgorithmSpecification\":{\"TrainingInputMode\":\"File\",\"AlgorithmName\":\"GRADIENT_ASCENT\"},\"InputDataConfig\":[{\"ChannelName\":\"InputData\",\"DataSource\":{\"S3DataSource\":{\"S3Uri\":\"https://s3.", + "\",\"AlgorithmSpecification\":{\"TrainingInputMode\":\"File\",\"AlgorithmName\":\"arn:aws:sagemaker:us-east-1:865070037744:algorithm/scikit-decision-trees-15423055-57b73412d2e93e9239e4e16f83298b8f\"},\"InputDataConfig\":[{\"ChannelName\":\"InputData\",\"DataSource\":{\"S3DataSource\":{\"S3Uri\":\"https://s3.", { "Ref": "AWS::Region" }, @@ -378,19 +388,9 @@ { "Ref": "TrainingData3FDB6D34" }, - "/result/\"},\"ResourceConfig\":{\"InstanceCount\":1,\"InstanceType\":\"ml.m4.xlarge\",\"VolumeSizeInGB\":10},\"StoppingCondition\":{\"MaxRuntimeInSeconds\":3600}},\"Type\":\"Task\",\"Resource\":\"arn:", - { - "Ref": "AWS::Partition" - }, - ":states:::sagemaker:createTrainingJob\"}}}" + "/result/\"},\"ResourceConfig\":{\"InstanceCount\":1,\"InstanceType\":\"ml.m4.xlarge\",\"VolumeSizeInGB\":10},\"StoppingCondition\":{\"MaxRuntimeInSeconds\":3600}}}}}" ] ] - }, - "RoleArn": { - "Fn::GetAtt": [ - "StateMachineRoleB840431D", - "Arn" - ] } }, "DependsOn": [ @@ -398,5 +398,12 @@ "StateMachineRoleB840431D" ] } + }, + "Outputs": { + "stateMachineArn": { + "Value": { + "Ref": "StateMachine2E01A3A5" + } + } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.create-training-job.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.create-training-job.ts new file mode 100644 index 0000000000000..28e4e65ff0e1e --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.create-training-job.ts @@ -0,0 +1,53 @@ +import { Key } from '@aws-cdk/aws-kms'; +import { Bucket, BucketEncryption } from '@aws-cdk/aws-s3'; +import { StateMachine } from '@aws-cdk/aws-stepfunctions'; +import { App, CfnOutput, RemovalPolicy, Stack } from '@aws-cdk/core'; +import { S3Location } from '../../lib'; +import { SageMakerCreateTrainingJob } from '../../lib/sagemaker/create-training-job'; + +/* + * Creates a state machine with a task state to create a training job in AWS SageMaker + * SageMaker jobs need training algorithms. These can be found in the AWS marketplace + * or created. + * + * Subscribe to demo Algorithm vended by Amazon (free): + * https://aws.amazon.com/marketplace/ai/procurement?productId=cc5186a0-b8d6-4750-a9bb-1dcdf10e787a + * FIXME - create Input data pertinent for the training model and insert into S3 location specified in inputDataConfig. + * + * Stack verification steps: + * The generated State Machine can be executed from the CLI (or Step Functions console) + * and runs with an execution status of `Succeeded`. + * + * -- aws stepfunctions start-execution --state-machine-arn provides execution arn + * -- aws stepfunctions describe-execution --execution-arn returns a status of `Succeeded` + */ +const app = new App(); +const stack = new Stack(app, 'integ-stepfunctions-sagemaker'); + +const encryptionKey = new Key(stack, 'EncryptionKey', { + removalPolicy: RemovalPolicy.DESTROY, +}); +const trainingData = new Bucket(stack, 'TrainingData', { + encryption: BucketEncryption.KMS, + encryptionKey, + removalPolicy: RemovalPolicy.DESTROY, +}); + +const sm = new StateMachine(stack, 'StateMachine', { + definition: new SageMakerCreateTrainingJob(stack, 'TrainTask', { + algorithmSpecification: { + algorithmName: 'arn:aws:sagemaker:us-east-1:865070037744:algorithm/scikit-decision-trees-15423055-57b73412d2e93e9239e4e16f83298b8f', + }, + inputDataConfig: [{ channelName: 'InputData', dataSource: { + s3DataSource: { + s3Location: S3Location.fromBucket(trainingData, 'data/'), + }, + } }], + outputDataConfig: { s3OutputLocation: S3Location.fromBucket(trainingData, 'result/') }, + trainingJobName: 'mytrainingjob', + }), +}); + +new CfnOutput(stack, 'stateMachineArn', { + value: sm.stateMachineArn, +}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.sagemaker.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.sagemaker.ts deleted file mode 100644 index 661f1f1bbd006..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.sagemaker.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Key } from '@aws-cdk/aws-kms'; -import { Bucket, BucketEncryption } from '@aws-cdk/aws-s3'; -import { StateMachine, Task } from '@aws-cdk/aws-stepfunctions'; -import { App, RemovalPolicy, Stack } from '@aws-cdk/core'; -import { S3Location, SagemakerTrainTask } from '../../lib'; - -const app = new App(); -const stack = new Stack(app, 'integ-stepfunctions-sagemaker'); - -const encryptionKey = new Key(stack, 'EncryptionKey', { - removalPolicy: RemovalPolicy.DESTROY, -}); -const trainingData = new Bucket(stack, 'TrainingData', { - encryption: BucketEncryption.KMS, - encryptionKey, - removalPolicy: RemovalPolicy.DESTROY, -}); - -new StateMachine(stack, 'StateMachine', { - definition: new Task(stack, 'TrainTask', { - task: new SagemakerTrainTask({ - algorithmSpecification: { - algorithmName: 'GRADIENT_ASCENT', - }, - inputDataConfig: [{ channelName: 'InputData', dataSource: { - s3DataSource: { - s3Location: S3Location.fromBucket(trainingData, 'data/'), - }, - } }], - outputDataConfig: { s3OutputLocation: S3Location.fromBucket(trainingData, 'result/') }, - trainingJobName: 'MyTrainingJob', - }), - }), -}); From d6a126508e4bb03f6f9d874c2c6648c3e3661a41 Mon Sep 17 00:00:00 2001 From: Alban Esc Date: Tue, 9 Jun 2020 03:38:16 -0700 Subject: [PATCH 27/32] fix(elbv2): missing permission to write NLB access logs to S3 bucket (#8114) fixes #8113 Currently, it's not possible to enable access logs for a network load balancer using the logAccessLogs method. Cloudformation will fail at deploy time because the S3 Bucket doesn't have the right permissions. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/nlb/network-load-balancer.ts | 37 +++++++++++++++++++ .../test/nlb/test.load-balancer.ts | 36 ++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts index 02c7855534d5b..cf7ccbf04b1ed 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts @@ -1,5 +1,7 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; +import { PolicyStatement, ServicePrincipal } from '@aws-cdk/aws-iam'; +import { IBucket } from '@aws-cdk/aws-s3'; import { Construct, Resource } from '@aws-cdk/core'; import { BaseLoadBalancer, BaseLoadBalancerProps, ILoadBalancerV2 } from '../shared/base-load-balancer'; import { BaseNetworkListenerProps, NetworkListener } from './network-listener'; @@ -101,6 +103,41 @@ export class NetworkLoadBalancer extends BaseLoadBalancer implements INetworkLoa }); } + /** + * Enable access logging for this load balancer. + * + * A region must be specified on the stack containing the load balancer; you cannot enable logging on + * environment-agnostic stacks. See https://docs.aws.amazon.com/cdk/latest/guide/environments.html + * + * This is extending the BaseLoadBalancer.logAccessLogs method to match the bucket permissions described + * at https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-access-logs.html#access-logging-bucket-requirements + */ + public logAccessLogs(bucket: IBucket, prefix?: string) { + super.logAccessLogs(bucket, prefix); + + const logsDeliveryServicePrincipal = new ServicePrincipal('delivery.logs.amazonaws.com'); + + bucket.addToResourcePolicy( + new PolicyStatement({ + actions: ['s3:PutObject'], + principals: [logsDeliveryServicePrincipal], + resources: [ + bucket.arnForObjects(`${prefix ? prefix + '/' : ''}AWSLogs/${this.stack.account}/*`), + ], + conditions: { + StringEquals: { 's3:x-amz-acl': 'bucket-owner-full-control' }, + }, + }), + ); + bucket.addToResourcePolicy( + new PolicyStatement({ + actions: ['s3:GetBucketAcl'], + principals: [logsDeliveryServicePrincipal], + resources: [bucket.bucketArn], + }), + ); + } + /** * Return the given named metric for this Network Load Balancer * diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.load-balancer.ts index 3fdaf593d0be4..4ee545b0ccfdc 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.load-balancer.ts @@ -115,6 +115,24 @@ export = { { Ref: 'AWS::AccountId' }, '/*']], }, }, + { + Action: 's3:PutObject', + Condition: { StringEquals: { 's3:x-amz-acl': 'bucket-owner-full-control' }}, + Effect: 'Allow', + Principal: { Service: 'delivery.logs.amazonaws.com' }, + Resource: { + 'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/AWSLogs/', + { Ref: 'AWS::AccountId' }, '/*']], + }, + }, + { + Action: 's3:GetBucketAcl', + Effect: 'Allow', + Principal: { Service: 'delivery.logs.amazonaws.com' }, + Resource: { + 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'], + }, + }, ], }, })); @@ -170,6 +188,24 @@ export = { { Ref: 'AWS::AccountId' }, '/*']], }, }, + { + Action: 's3:PutObject', + Condition: { StringEquals: { 's3:x-amz-acl': 'bucket-owner-full-control' }}, + Effect: 'Allow', + Principal: { Service: 'delivery.logs.amazonaws.com' }, + Resource: { + 'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/prefix-of-access-logs/AWSLogs/', + { Ref: 'AWS::AccountId' }, '/*']], + }, + }, + { + Action: 's3:GetBucketAcl', + Effect: 'Allow', + Principal: { Service: 'delivery.logs.amazonaws.com' }, + Resource: { + 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'], + }, + }, ], }, })); From 888b412797b2bcd7b8f1b8c5cbc0c25d94f91a5f Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 9 Jun 2020 13:26:56 +0200 Subject: [PATCH 28/32] feat(core,s3-assets,lambda): custom asset bundling (#7898) Adds support for asset bundling by running a command inside a Docker container. The asset path is mounted in the container at `/asset-input` and is set as the working directory. The container is responsible for putting content at `/asset-output`. The content at `/asset-output` will be zipped and used as the final asset. This allows to use Docker for Lambda code bundling. It will also be possible to refactor `aws-lambda-nodejs` and create other language specific modules. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/assets/lib/api.ts | 2 + packages/@aws-cdk/assets/lib/index.ts | 2 +- packages/@aws-cdk/aws-lambda/README.md | 52 ++++- packages/@aws-cdk/aws-lambda/lib/runtime.ts | 11 + .../test/integ.bundling.expected.json | 113 ++++++++++ .../aws-lambda/test/integ.bundling.ts | 42 ++++ .../test/python-lambda-handler/index.py | 8 + .../python-lambda-handler/requirements.txt | 1 + packages/@aws-cdk/aws-s3-assets/README.md | 3 + packages/@aws-cdk/aws-s3-assets/lib/asset.ts | 24 ++- packages/@aws-cdk/aws-s3-assets/lib/compat.ts | 17 ++ .../test/alpine-markdown/Dockerfile | 3 + .../aws-s3-assets/test/compat.test.ts | 11 + .../integ.assets.bundling.lit.expected.json | 78 +++++++ .../test/integ.assets.bundling.lit.ts | 31 +++ .../test/markdown-asset/index.md | 3 + packages/@aws-cdk/core/lib/asset-staging.ts | 124 ++++++++++- packages/@aws-cdk/core/lib/assets.ts | 78 +++++++ packages/@aws-cdk/core/lib/bundling.ts | 193 ++++++++++++++++++ packages/@aws-cdk/core/lib/fs/index.ts | 12 +- packages/@aws-cdk/core/lib/index.ts | 1 + packages/@aws-cdk/core/package.json | 2 + packages/@aws-cdk/core/test/test.bundling.ts | 120 +++++++++++ packages/@aws-cdk/core/test/test.staging.ts | 152 +++++++++++++- 24 files changed, 1062 insertions(+), 21 deletions(-) create mode 100644 packages/@aws-cdk/aws-lambda/test/integ.bundling.expected.json create mode 100644 packages/@aws-cdk/aws-lambda/test/integ.bundling.ts create mode 100644 packages/@aws-cdk/aws-lambda/test/python-lambda-handler/index.py create mode 100644 packages/@aws-cdk/aws-lambda/test/python-lambda-handler/requirements.txt create mode 100644 packages/@aws-cdk/aws-s3-assets/lib/compat.ts create mode 100644 packages/@aws-cdk/aws-s3-assets/test/alpine-markdown/Dockerfile create mode 100644 packages/@aws-cdk/aws-s3-assets/test/compat.test.ts create mode 100644 packages/@aws-cdk/aws-s3-assets/test/integ.assets.bundling.lit.expected.json create mode 100644 packages/@aws-cdk/aws-s3-assets/test/integ.assets.bundling.lit.ts create mode 100644 packages/@aws-cdk/aws-s3-assets/test/markdown-asset/index.md create mode 100644 packages/@aws-cdk/core/lib/bundling.ts create mode 100644 packages/@aws-cdk/core/test/test.bundling.ts diff --git a/packages/@aws-cdk/assets/lib/api.ts b/packages/@aws-cdk/assets/lib/api.ts index 75966e57d5af8..a575c92c293a9 100644 --- a/packages/@aws-cdk/assets/lib/api.ts +++ b/packages/@aws-cdk/assets/lib/api.ts @@ -1,5 +1,7 @@ /** * Common interface for all assets. + * + * @deprecated use `core.IAsset` */ export interface IAsset { /** diff --git a/packages/@aws-cdk/assets/lib/index.ts b/packages/@aws-cdk/assets/lib/index.ts index c651e06cc2ac1..e2a67003867bd 100644 --- a/packages/@aws-cdk/assets/lib/index.ts +++ b/packages/@aws-cdk/assets/lib/index.ts @@ -1,4 +1,4 @@ export * from './api'; export * from './fs/follow-mode'; export * from './fs/options'; -export * from './staging'; \ No newline at end of file +export * from './staging'; diff --git a/packages/@aws-cdk/aws-lambda/README.md b/packages/@aws-cdk/aws-lambda/README.md index 92ed4dc61392b..01b211d16e142 100644 --- a/packages/@aws-cdk/aws-lambda/README.md +++ b/packages/@aws-cdk/aws-lambda/README.md @@ -32,7 +32,8 @@ runtime code. * `lambda.Code.fromInline(code)` - inline the handle code as a string. This is limited to supported runtimes and the code cannot exceed 4KiB. * `lambda.Code.fromAsset(path)` - specify a directory or a .zip file in the local - filesystem which will be zipped and uploaded to S3 before deployment. + filesystem which will be zipped and uploaded to S3 before deployment. See also + [bundling asset code](#Bundling-Asset-Code). The following example shows how to define a Python function and deploy the code from the local directory `my-lambda-handler` to it: @@ -62,7 +63,7 @@ const fn = new lambda.Function(this, 'MyFunction', { runtime: lambda.Runtime.NODEJS_10_X, handler: 'index.handler', code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')), - + fn.role // the Role ``` @@ -287,6 +288,53 @@ number of times and with different properties. Using `SingletonFunction` here wi For example, the `LogRetention` construct requires only one single lambda function for all different log groups whose retention it seeks to manage. +### Bundling Asset Code +When using `lambda.Code.fromAsset(path)` it is possible to bundle the code by running a +command in a Docker container. The asset path will be mounted at `/asset-input`. The +Docker container is responsible for putting content at `/asset-output`. The content at +`/asset-output` will be zipped and used as Lambda code. + +Example with Python: +```ts +new lambda.Function(this, 'Function', { + code: lambda.Code.fromAsset(path.join(__dirname, 'my-python-handler'), { + bundling: { + image: lambda.Runtime.PYTHON_3_6.bundlingDockerImage, + command: [ + 'bash', '-c', ` + pip install -r requirements.txt -t /asset-output && + rsync -r . /asset-output + `, + ], + }, + }), + runtime: lambda.Runtime.PYTHON_3_6, + handler: 'index.handler', +}); +``` +Runtimes expose a `bundlingDockerImage` property that points to the [lambci/lambda](https://hub.docker.com/r/lambci/lambda/) build image. + +Use `cdk.BundlingDockerImage.fromRegistry(image)` to use an existing image or +`cdk.BundlingDockerImage.fromAsset(path)` to build a specific image: + +```ts +import * as cdk from '@aws-cdk/core'; + +new lambda.Function(this, 'Function', { + code: lambda.Code.fromAsset('/path/to/handler', { + bundling: { + image: cdk.BundlingDockerImage.fromAsset('/path/to/dir/with/DockerFile', { + buildArgs: { + ARG1: 'value1', + }, + }), + command: ['my', 'cool', 'command'], + }, + }), + // ... +}); +``` + ### Language-specific APIs Language-specific higher level constructs are provided in separate modules: diff --git a/packages/@aws-cdk/aws-lambda/lib/runtime.ts b/packages/@aws-cdk/aws-lambda/lib/runtime.ts index ffa111ca4509b..25f36b6a4f53f 100644 --- a/packages/@aws-cdk/aws-lambda/lib/runtime.ts +++ b/packages/@aws-cdk/aws-lambda/lib/runtime.ts @@ -1,3 +1,5 @@ +import { BundlingDockerImage } from '@aws-cdk/core'; + export interface LambdaRuntimeProps { /** * Whether the ``ZipFile`` (aka inline code) property can be used with this runtime. @@ -154,10 +156,19 @@ export class Runtime { */ public readonly family?: RuntimeFamily; + /** + * The bundling Docker image for this runtime. + * Points to the lambci/lambda build image for this runtime. + * + * @see https://hub.docker.com/r/lambci/lambda/ + */ + public readonly bundlingDockerImage: BundlingDockerImage; + constructor(name: string, family?: RuntimeFamily, props: LambdaRuntimeProps = { }) { this.name = name; this.supportsInlineCode = !!props.supportsInlineCode; this.family = family; + this.bundlingDockerImage = BundlingDockerImage.fromRegistry(`lambci/lambda:build-${name}`); Runtime.ALL.push(this); } diff --git a/packages/@aws-cdk/aws-lambda/test/integ.bundling.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.bundling.expected.json new file mode 100644 index 0000000000000..aa5a63c7a3c3d --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.bundling.expected.json @@ -0,0 +1,113 @@ +{ + "Resources": { + "FunctionServiceRole675BB04A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "Function76856677": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cdS3Bucket6365D8AA" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cdS3VersionKey14A1DBA7" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cdS3VersionKey14A1DBA7" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionServiceRole675BB04A", + "Arn" + ] + }, + "Runtime": "python3.6" + }, + "DependsOn": [ + "FunctionServiceRole675BB04A" + ] + } + }, + "Parameters": { + "AssetParameters0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cdS3Bucket6365D8AA": { + "Type": "String", + "Description": "S3 bucket for asset \"0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cd\"" + }, + "AssetParameters0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cdS3VersionKey14A1DBA7": { + "Type": "String", + "Description": "S3 key for asset version \"0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cd\"" + }, + "AssetParameters0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cdArtifactHashEEC2ED67": { + "Type": "String", + "Description": "Artifact hash for asset \"0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cd\"" + } + }, + "Outputs": { + "FunctionArn": { + "Value": { + "Fn::GetAtt": [ + "Function76856677", + "Arn" + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/integ.bundling.ts b/packages/@aws-cdk/aws-lambda/test/integ.bundling.ts new file mode 100644 index 0000000000000..6c1715bd05747 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.bundling.ts @@ -0,0 +1,42 @@ +import { App, CfnOutput, Construct, Stack, StackProps } from '@aws-cdk/core'; +import * as path from 'path'; +import * as lambda from '../lib'; + +/** + * Stack verification steps: + * * aws cloudformation describe-stacks --stack-name cdk-integ-lambda-bundling --query Stacks[0].Outputs[0].OutputValue + * * aws lambda invoke --function-name response.json + * * cat response.json + * The last command should show '200' + */ +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const assetPath = path.join(__dirname, 'python-lambda-handler'); + const fn = new lambda.Function(this, 'Function', { + code: lambda.Code.fromAsset(assetPath, { + bundling: { + image: lambda.Runtime.PYTHON_3_6.bundlingDockerImage, + command: [ + 'bash', '-c', [ + 'rsync -r . /asset-output', + 'cd /asset-output', + 'pip install -r requirements.txt -t .', + ].join(' && '), + ], + }, + }), + runtime: lambda.Runtime.PYTHON_3_6, + handler: 'index.handler', + }); + + new CfnOutput(this, 'FunctionArn', { + value: fn.functionArn, + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-integ-lambda-bundling'); +app.synth(); diff --git a/packages/@aws-cdk/aws-lambda/test/python-lambda-handler/index.py b/packages/@aws-cdk/aws-lambda/test/python-lambda-handler/index.py new file mode 100644 index 0000000000000..175a36616590a --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/python-lambda-handler/index.py @@ -0,0 +1,8 @@ +import requests + +def handler(event, context): + r = requests.get('https://aws.amazon.com') + + print(r.status_code) + + return r.status_code diff --git a/packages/@aws-cdk/aws-lambda/test/python-lambda-handler/requirements.txt b/packages/@aws-cdk/aws-lambda/test/python-lambda-handler/requirements.txt new file mode 100644 index 0000000000000..b4500579db515 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/python-lambda-handler/requirements.txt @@ -0,0 +1 @@ +requests==2.23.0 diff --git a/packages/@aws-cdk/aws-s3-assets/README.md b/packages/@aws-cdk/aws-s3-assets/README.md index 86490d0421025..07d3a88bb0208 100644 --- a/packages/@aws-cdk/aws-s3-assets/README.md +++ b/packages/@aws-cdk/aws-s3-assets/README.md @@ -50,6 +50,9 @@ The following examples grants an IAM group read permissions on an asset: [Example of granting read access to an asset](./test/integ.assets.permissions.lit.ts) +The following example uses custom asset bundling to convert a markdown file to html: +[Example of using asset bundling](./test/integ.assets.bundling.lit.ts) + ## How does it work? When an asset is defined in a construct, a construct metadata entry diff --git a/packages/@aws-cdk/aws-s3-assets/lib/asset.ts b/packages/@aws-cdk/aws-s3-assets/lib/asset.ts index 5c4b6e6cb3eb9..5c3f0a514f07e 100644 --- a/packages/@aws-cdk/aws-s3-assets/lib/asset.ts +++ b/packages/@aws-cdk/aws-s3-assets/lib/asset.ts @@ -5,11 +5,11 @@ import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs'; import * as path from 'path'; +import { toSymlinkFollow } from './compat'; const ARCHIVE_EXTENSIONS = [ '.zip', '.jar' ]; -export interface AssetOptions extends assets.CopyOptions { - +export interface AssetOptions extends assets.CopyOptions, cdk.AssetOptions { /** * A list of principals that should be able to read this asset from S3. * You can use `asset.grantRead(principal)` to grant read permissions later. @@ -30,7 +30,7 @@ export interface AssetOptions extends assets.CopyOptions { * @default - automatically calculate source hash based on the contents * of the source file or directory. * - * @experimental + * @deprecated see `assetHash` and `assetHashType` */ readonly sourceHash?: string; } @@ -50,7 +50,7 @@ export interface AssetProps extends AssetOptions { * An asset represents a local file or directory, which is automatically uploaded to S3 * and then can be referenced within a CDK application. */ -export class Asset extends cdk.Construct implements assets.IAsset { +export class Asset extends cdk.Construct implements cdk.IAsset { /** * Attribute that represents the name of the bucket this asset exists in. */ @@ -98,18 +98,28 @@ export class Asset extends cdk.Construct implements assets.IAsset { */ public readonly isZipArchive: boolean; + /** + * A cryptographic hash of the asset. + * + * @deprecated see `assetHash` + */ public readonly sourceHash: string; + public readonly assetHash: string; + constructor(scope: cdk.Construct, id: string, props: AssetProps) { super(scope, id); // stage the asset source (conditionally). - const staging = new assets.Staging(this, 'Stage', { + const staging = new cdk.AssetStaging(this, 'Stage', { ...props, sourcePath: path.resolve(props.path), + follow: toSymlinkFollow(props.follow), + assetHash: props.assetHash ?? props.sourceHash, }); - this.sourceHash = props.sourceHash || staging.sourceHash; + this.assetHash = staging.assetHash; + this.sourceHash = this.assetHash; this.assetPath = staging.stagedPath; @@ -136,7 +146,7 @@ export class Asset extends cdk.Construct implements assets.IAsset { this.bucket = s3.Bucket.fromBucketName(this, 'AssetBucket', this.s3BucketName); - for (const reader of (props.readers || [])) { + for (const reader of (props.readers ?? [])) { this.grantRead(reader); } } diff --git a/packages/@aws-cdk/aws-s3-assets/lib/compat.ts b/packages/@aws-cdk/aws-s3-assets/lib/compat.ts new file mode 100644 index 0000000000000..af080a15615a2 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-assets/lib/compat.ts @@ -0,0 +1,17 @@ +import { FollowMode } from '@aws-cdk/assets'; +import { SymlinkFollowMode } from '@aws-cdk/core'; + +export function toSymlinkFollow(follow?: FollowMode): SymlinkFollowMode | undefined { + if (!follow) { + return undefined; + } + + switch (follow) { + case FollowMode.NEVER: return SymlinkFollowMode.NEVER; + case FollowMode.ALWAYS: return SymlinkFollowMode.ALWAYS; + case FollowMode.BLOCK_EXTERNAL: return SymlinkFollowMode.BLOCK_EXTERNAL; + case FollowMode.EXTERNAL: return SymlinkFollowMode.EXTERNAL; + default: + throw new Error(`unknown follow mode: ${follow}`); + } +} diff --git a/packages/@aws-cdk/aws-s3-assets/test/alpine-markdown/Dockerfile b/packages/@aws-cdk/aws-s3-assets/test/alpine-markdown/Dockerfile new file mode 100644 index 0000000000000..fa7a67678bae9 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-assets/test/alpine-markdown/Dockerfile @@ -0,0 +1,3 @@ +FROM alpine + +RUN apk add markdown diff --git a/packages/@aws-cdk/aws-s3-assets/test/compat.test.ts b/packages/@aws-cdk/aws-s3-assets/test/compat.test.ts new file mode 100644 index 0000000000000..41fbf0b57ac53 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-assets/test/compat.test.ts @@ -0,0 +1,11 @@ +import { FollowMode } from '@aws-cdk/assets'; +import { SymlinkFollowMode } from '@aws-cdk/core'; +import { toSymlinkFollow } from '../lib/compat'; + +test('FollowMode compatibility', () => { + expect(toSymlinkFollow(undefined)).toBeUndefined(); + expect(toSymlinkFollow(FollowMode.ALWAYS)).toBe(SymlinkFollowMode.ALWAYS); + expect(toSymlinkFollow(FollowMode.BLOCK_EXTERNAL)).toBe(SymlinkFollowMode.BLOCK_EXTERNAL); + expect(toSymlinkFollow(FollowMode.EXTERNAL)).toBe(SymlinkFollowMode.EXTERNAL); + expect(toSymlinkFollow(FollowMode.NEVER)).toBe(SymlinkFollowMode.NEVER); +}); diff --git a/packages/@aws-cdk/aws-s3-assets/test/integ.assets.bundling.lit.expected.json b/packages/@aws-cdk/aws-s3-assets/test/integ.assets.bundling.lit.expected.json new file mode 100644 index 0000000000000..21d2d76dbd488 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-assets/test/integ.assets.bundling.lit.expected.json @@ -0,0 +1,78 @@ +{ + "Parameters": { + "AssetParameters10af79ff4bd6432db05b51810586c19ca95abd08759fca785e44f594bc9633b8S3Bucket8CD0F73B": { + "Type": "String", + "Description": "S3 bucket for asset \"10af79ff4bd6432db05b51810586c19ca95abd08759fca785e44f594bc9633b8\"" + }, + "AssetParameters10af79ff4bd6432db05b51810586c19ca95abd08759fca785e44f594bc9633b8S3VersionKeyA9EAF743": { + "Type": "String", + "Description": "S3 key for asset version \"10af79ff4bd6432db05b51810586c19ca95abd08759fca785e44f594bc9633b8\"" + }, + "AssetParameters10af79ff4bd6432db05b51810586c19ca95abd08759fca785e44f594bc9633b8ArtifactHashBAE492DD": { + "Type": "String", + "Description": "Artifact hash for asset \"10af79ff4bd6432db05b51810586c19ca95abd08759fca785e44f594bc9633b8\"" + } + }, + "Resources": { + "MyUserDC45028B": { + "Type": "AWS::IAM::User" + }, + "MyUserDefaultPolicy7B897426": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "AssetParameters10af79ff4bd6432db05b51810586c19ca95abd08759fca785e44f594bc9633b8S3Bucket8CD0F73B" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "AssetParameters10af79ff4bd6432db05b51810586c19ca95abd08759fca785e44f594bc9633b8S3Bucket8CD0F73B" + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyUserDefaultPolicy7B897426", + "Users": [ + { + "Ref": "MyUserDC45028B" + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-assets/test/integ.assets.bundling.lit.ts b/packages/@aws-cdk/aws-s3-assets/test/integ.assets.bundling.lit.ts new file mode 100644 index 0000000000000..b1b144f2de275 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-assets/test/integ.assets.bundling.lit.ts @@ -0,0 +1,31 @@ +import * as iam from '@aws-cdk/aws-iam'; +import { App, BundlingDockerImage, Construct, Stack, StackProps } from '@aws-cdk/core'; +import * as path from 'path'; +import * as assets from '../lib'; + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + /// !show + const asset = new assets.Asset(this, 'BundledAsset', { + path: path.join(__dirname, 'markdown-asset'), // /asset-input and working directory in the container + bundling: { + image: BundlingDockerImage.fromAsset(path.join(__dirname, 'alpine-markdown')), // Build an image + command: [ + 'sh', '-c', ` + markdown index.md > /asset-output/index.html + `, + ], + }, + }); + /// !hide + + const user = new iam.User(this, 'MyUser'); + asset.grantRead(user); + } +} + +const app = new App(); +new TestStack(app, 'cdk-integ-assets-bundling'); +app.synth(); diff --git a/packages/@aws-cdk/aws-s3-assets/test/markdown-asset/index.md b/packages/@aws-cdk/aws-s3-assets/test/markdown-asset/index.md new file mode 100644 index 0000000000000..64fdacbb595cb --- /dev/null +++ b/packages/@aws-cdk/aws-s3-assets/test/markdown-asset/index.md @@ -0,0 +1,3 @@ +### This is a sample file + +With **markdown** diff --git a/packages/@aws-cdk/core/lib/asset-staging.ts b/packages/@aws-cdk/core/lib/asset-staging.ts index 0fb9dc3da8265..c37d8d441d7c0 100644 --- a/packages/@aws-cdk/core/lib/asset-staging.ts +++ b/packages/@aws-cdk/core/lib/asset-staging.ts @@ -1,13 +1,16 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs'; +import * as os from 'os'; import * as path from 'path'; +import { AssetHashType, AssetOptions } from './assets'; +import { BUNDLING_INPUT_DIR, BUNDLING_OUTPUT_DIR, BundlingOptions } from './bundling'; import { Construct, ISynthesisSession } from './construct-compat'; import { FileSystem, FingerprintOptions } from './fs'; /** * Initialization properties for `AssetStaging`. */ -export interface AssetStagingProps extends FingerprintOptions { +export interface AssetStagingProps extends FingerprintOptions, AssetOptions { /** * The source file or directory to copy from. */ @@ -33,7 +36,6 @@ export interface AssetStagingProps extends FingerprintOptions { * means that only if content was changed, copy will happen. */ export class AssetStaging extends Construct { - /** * The path to the asset (stringinfied token). * @@ -48,43 +50,80 @@ export class AssetStaging extends Construct { public readonly sourcePath: string; /** - * A cryptographic hash of the source document(s). + * A cryptographic hash of the asset. + * + * @deprecated see `assetHash`. */ public readonly sourceHash: string; + /** + * A cryptographic hash of the asset. + */ + public readonly assetHash: string; + private readonly fingerprintOptions: FingerprintOptions; private readonly relativePath?: string; + private readonly bundleDir?: string; + constructor(scope: Construct, id: string, props: AssetStagingProps) { super(scope, id); this.sourcePath = props.sourcePath; this.fingerprintOptions = props; - this.sourceHash = FileSystem.fingerprint(this.sourcePath, props); + + if (props.bundling) { + this.bundleDir = this.bundle(props.bundling); + } + + this.assetHash = this.calculateHash(props); const stagingDisabled = this.node.tryGetContext(cxapi.DISABLE_ASSET_STAGING_CONTEXT); if (stagingDisabled) { - this.stagedPath = this.sourcePath; + this.stagedPath = this.bundleDir ?? this.sourcePath; } else { - this.relativePath = 'asset.' + this.sourceHash + path.extname(this.sourcePath); - this.stagedPath = this.relativePath; // always relative to outdir + this.relativePath = `asset.${this.assetHash}${path.extname(this.bundleDir ?? this.sourcePath)}`; + this.stagedPath = this.relativePath; } + + this.sourceHash = this.assetHash; } protected synthesize(session: ISynthesisSession) { + // Staging is disabled if (!this.relativePath) { return; } const targetPath = path.join(session.assembly.outdir, this.relativePath); - // asset already staged + // Already staged if (fs.existsSync(targetPath)) { return; } - // copy file/directory to staging directory + // Asset has been bundled + if (this.bundleDir) { + // Try to rename bundling directory to staging directory + try { + fs.renameSync(this.bundleDir, targetPath); + return; + } catch (err) { + // /tmp and cdk.out could be mounted across different mount points + // in this case we will fallback to copying. This can happen in Windows + // Subsystem for Linux (WSL). + if (err.code === 'EXDEV') { + fs.mkdirSync(targetPath); + FileSystem.copyDirectory(this.bundleDir, targetPath, this.fingerprintOptions); + return; + } + + throw err; + } + } + + // Copy file/directory to staging directory const stat = fs.statSync(this.sourcePath); if (stat.isFile()) { fs.copyFileSync(this.sourcePath, targetPath); @@ -95,4 +134,71 @@ export class AssetStaging extends Construct { throw new Error(`Unknown file type: ${this.sourcePath}`); } } + + private bundle(options: BundlingOptions): string { + // Create temporary directory for bundling + const bundleDir = fs.mkdtempSync(path.resolve(path.join(os.tmpdir(), 'cdk-asset-bundle-'))); + + // Always mount input and output dir + const volumes = [ + { + hostPath: this.sourcePath, + containerPath: BUNDLING_INPUT_DIR, + }, + { + hostPath: bundleDir, + containerPath: BUNDLING_OUTPUT_DIR, + }, + ...options.volumes ?? [], + ]; + + try { + options.image._run({ + command: options.command, + volumes, + environment: options.environment, + workingDirectory: options.workingDirectory ?? BUNDLING_INPUT_DIR, + }); + } catch (err) { + throw new Error(`Failed to run bundling Docker image for asset ${this.node.path}: ${err}`); + } + + if (FileSystem.isEmpty(bundleDir)) { + throw new Error(`Bundling did not produce any output. Check that your container writes content to ${BUNDLING_OUTPUT_DIR}.`); + } + + return bundleDir; + } + + private calculateHash(props: AssetStagingProps): string { + let hashType: AssetHashType; + + if (props.assetHash) { + if (props.assetHashType && props.assetHashType !== AssetHashType.CUSTOM) { + throw new Error(`Cannot specify \`${props.assetHashType}\` for \`assetHashType\` when \`assetHash\` is specified. Use \`CUSTOM\` or leave \`undefined\`.`); + } + hashType = AssetHashType.CUSTOM; + } else if (props.assetHashType) { + hashType = props.assetHashType; + } else { + hashType = AssetHashType.SOURCE; + } + + switch (hashType) { + case AssetHashType.SOURCE: + return FileSystem.fingerprint(this.sourcePath, this.fingerprintOptions); + case AssetHashType.BUNDLE: + if (!this.bundleDir) { + throw new Error('Cannot use `AssetHashType.BUNDLE` when `bundling` is not specified.'); + } + return FileSystem.fingerprint(this.bundleDir, this.fingerprintOptions); + case AssetHashType.CUSTOM: + if (!props.assetHash) { + throw new Error('`assetHash` must be specified when `assetHashType` is set to `AssetHashType.CUSTOM`.'); + } + return props.assetHash; + default: + throw new Error('Unknown asset hash type.'); + } + } } diff --git a/packages/@aws-cdk/core/lib/assets.ts b/packages/@aws-cdk/core/lib/assets.ts index 8c59e576b588c..bad303dbd8c31 100644 --- a/packages/@aws-cdk/core/lib/assets.ts +++ b/packages/@aws-cdk/core/lib/assets.ts @@ -1,3 +1,81 @@ +import { BundlingOptions } from './bundling'; + +/** + * Common interface for all assets. + */ +export interface IAsset { + /** + * A hash of this asset, which is available at construction time. As this is a plain string, it + * can be used in construct IDs in order to enforce creation of a new resource when the content + * hash has changed. + */ + readonly assetHash: string; +} + +/** + * Asset hash options + */ +export interface AssetOptions { + /** + * Specify a custom hash for this asset. If `assetHashType` is set it must + * be set to `AssetHashType.CUSTOM`. + * + * NOTE: the hash is used in order to identify a specific revision of the asset, and + * used for optimizing and caching deployment activities related to this asset such as + * packaging, uploading to Amazon S3, etc. If you chose to customize the hash, you will + * need to make sure it is updated every time the asset changes, or otherwise it is + * possible that some deployments will not be invalidated. + * + * @default - based on `assetHashType` + */ + readonly assetHash?: string; + + /** + * Specifies the type of hash to calculate for this asset. + * + * If `assetHash` is configured, this option must be `undefined` or + * `AssetHashType.CUSTOM`. + * + * @default - the default is `AssetHashType.SOURCE`, but if `assetHash` is + * explicitly specified this value defaults to `AssetHashType.CUSTOM`. + */ + readonly assetHashType?: AssetHashType; + + /** + * Bundle the asset by executing a command in a Docker container. + * The asset path will be mounted at `/asset-input`. The Docker + * container is responsible for putting content at `/asset-output`. + * The content at `/asset-output` will be zipped and used as the + * final asset. + * + * @default - uploaded as-is to S3 if the asset is a regular file or a .zip file, + * archived into a .zip file and uploaded to S3 otherwise + * + * @experimental + */ + readonly bundling?: BundlingOptions; +} + +/** + * The type of asset hash + */ +export enum AssetHashType { + /** + * Based on the content of the source path + */ + SOURCE = 'source', + + /** + * Based on the content of the bundled path + */ + BUNDLE = 'bundle', + + /** + * Use a custom hash + */ + CUSTOM = 'custom', +} + /** * Represents the source for a file asset. */ diff --git a/packages/@aws-cdk/core/lib/bundling.ts b/packages/@aws-cdk/core/lib/bundling.ts new file mode 100644 index 0000000000000..bfff68b40f5cd --- /dev/null +++ b/packages/@aws-cdk/core/lib/bundling.ts @@ -0,0 +1,193 @@ +import { spawnSync } from 'child_process'; + +export const BUNDLING_INPUT_DIR = '/asset-input'; +export const BUNDLING_OUTPUT_DIR = '/asset-output'; + +/** + * Bundling options + * + * @experimental + */ +export interface BundlingOptions { + /** + * The Docker image where the command will run. + */ + readonly image: BundlingDockerImage; + + /** + * The command to run in the container. + * + * @example ['npm', 'install'] + * + * @see https://docs.docker.com/engine/reference/run/ + * + * @default - run the command defined in the image + */ + readonly command?: string[]; + + /** + * Additional Docker volumes to mount. + * + * @default - no additional volumes are mounted + */ + readonly volumes?: DockerVolume[]; + + /** + * The environment variables to pass to the container. + * + * @default - no environment variables. + */ + readonly environment?: { [key: string]: string; }; + + /** + * Working directory inside the container. + * + * @default /asset-input + */ + readonly workingDirectory?: string; +} + +/** + * A Docker image used for asset bundling + */ +export class BundlingDockerImage { + /** + * Reference an image on DockerHub or another online registry. + * + * @param image the image name + */ + public static fromRegistry(image: string) { + return new BundlingDockerImage(image); + } + + /** + * Reference an image that's built directly from sources on disk. + * + * @param path The path to the directory containing the Docker file + * @param options Docker build options + */ + public static fromAsset(path: string, options: DockerBuildOptions = {}) { + const buildArgs = options.buildArgs || {}; + + const dockerArgs: string[] = [ + 'build', + ...flatten(Object.entries(buildArgs).map(([k, v]) => ['--build-arg', `${k}=${v}`])), + path, + ]; + + const docker = exec('docker', dockerArgs); + + const match = docker.stdout.toString().match(/Successfully built ([a-z0-9]+)/); + + if (!match) { + throw new Error('Failed to extract image ID from Docker build output'); + } + + return new BundlingDockerImage(match[1]); + } + + /** @param image The Docker image */ + private constructor(public readonly image: string) {} + + /** + * Runs a Docker image + * + * @internal + */ + public _run(options: DockerRunOptions = {}) { + const volumes = options.volumes || []; + const environment = options.environment || {}; + const command = options.command || []; + + const dockerArgs: string[] = [ + 'run', '--rm', + ...flatten(volumes.map(v => ['-v', `${v.hostPath}:${v.containerPath}`])), + ...flatten(Object.entries(environment).map(([k, v]) => ['--env', `${k}=${v}`])), + ...options.workingDirectory + ? ['-w', options.workingDirectory] + : [], + this.image, + ...command, + ]; + + exec('docker', dockerArgs); + } +} + +/** + * A Docker volume + */ +export interface DockerVolume { + /** + * The path to the file or directory on the host machine + */ + readonly hostPath: string; + + /** + * The path where the file or directory is mounted in the container + */ + readonly containerPath: string; +} + +/** + * Docker run options + */ +interface DockerRunOptions { + /** + * The command to run in the container. + * + * @default - run the command defined in the image + */ + readonly command?: string[]; + + /** + * Docker volumes to mount. + * + * @default - no volumes are mounted + */ + readonly volumes?: DockerVolume[]; + + /** + * The environment variables to pass to the container. + * + * @default - no environment variables. + */ + readonly environment?: { [key: string]: string; }; + + /** + * Working directory inside the container. + * + * @default - image default + */ + readonly workingDirectory?: string; +} + +/** + * Docker build options + */ +export interface DockerBuildOptions { + /** + * Build args + * + * @default - no build args + */ + readonly buildArgs?: { [key: string]: string }; +} + +function flatten(x: string[][]) { + return Array.prototype.concat([], ...x); +} + +function exec(cmd: string, args: string[]) { + const proc = spawnSync(cmd, args); + + if (proc.error) { + throw proc.error; + } + + if (proc.status !== 0) { + throw new Error(`[Status ${proc.status}] stdout: ${proc.stdout?.toString().trim()}\n\n\nstderr: ${proc.stderr?.toString().trim()}`); + } + + return proc; +} diff --git a/packages/@aws-cdk/core/lib/fs/index.ts b/packages/@aws-cdk/core/lib/fs/index.ts index ac7f3c9d0f8da..01c6d132956e2 100644 --- a/packages/@aws-cdk/core/lib/fs/index.ts +++ b/packages/@aws-cdk/core/lib/fs/index.ts @@ -1,3 +1,4 @@ +import * as fs from 'fs'; import { copyDirectory } from './copy'; import { fingerprint } from './fingerprint'; import { CopyOptions, FingerprintOptions } from './options'; @@ -33,4 +34,13 @@ export class FileSystem { public static fingerprint(fileOrDirectory: string, options: FingerprintOptions = { }) { return fingerprint(fileOrDirectory, options); } -} \ No newline at end of file + + /** + * Checks whether a directory is empty + * + * @param dir The directory to check + */ + public static isEmpty(dir: string): boolean { + return fs.readdirSync(dir).length === 0; + } +} diff --git a/packages/@aws-cdk/core/lib/index.ts b/packages/@aws-cdk/core/lib/index.ts index 8b238e0c721fd..6c54a222901d6 100644 --- a/packages/@aws-cdk/core/lib/index.ts +++ b/packages/@aws-cdk/core/lib/index.ts @@ -48,6 +48,7 @@ export * from './assets'; export * from './tree'; export * from './asset-staging'; +export * from './bundling'; export * from './fs'; export * from './custom-resource'; diff --git a/packages/@aws-cdk/core/package.json b/packages/@aws-cdk/core/package.json index a654253f2d938..3e5b22b6b8a74 100644 --- a/packages/@aws-cdk/core/package.json +++ b/packages/@aws-cdk/core/package.json @@ -155,12 +155,14 @@ "@types/node": "^10.17.21", "@types/nodeunit": "^0.0.31", "@types/minimatch": "^3.0.3", + "@types/sinon": "^9.0.4", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "fast-check": "^1.24.2", "lodash": "^4.17.15", "nodeunit": "^0.11.3", "pkglint": "0.0.0", + "sinon": "^9.0.2", "ts-mock-imports": "^1.3.0" }, "dependencies": { diff --git a/packages/@aws-cdk/core/test/test.bundling.ts b/packages/@aws-cdk/core/test/test.bundling.ts new file mode 100644 index 0000000000000..658aa99901bb6 --- /dev/null +++ b/packages/@aws-cdk/core/test/test.bundling.ts @@ -0,0 +1,120 @@ +import * as child_process from 'child_process'; +import { Test } from 'nodeunit'; +import * as sinon from 'sinon'; +import { BundlingDockerImage } from '../lib'; + +export = { + 'tearDown'(callback: any) { + sinon.restore(); + callback(); + }, + + 'bundling with image from registry'(test: Test) { + const spawnSyncStub = sinon.stub(child_process, 'spawnSync').returns({ + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from('stdout'), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }); + + const image = BundlingDockerImage.fromRegistry('alpine'); + image._run({ + command: ['cool', 'command'], + environment: { + VAR1: 'value1', + VAR2: 'value2', + }, + volumes: [{ hostPath: '/host-path', containerPath: '/container-path' }], + workingDirectory: '/working-directory', + }); + + test.ok(spawnSyncStub.calledWith('docker', [ + 'run', '--rm', + '-v', '/host-path:/container-path', + '--env', 'VAR1=value1', + '--env', 'VAR2=value2', + '-w', '/working-directory', + 'alpine', + 'cool', 'command', + ])); + test.done(); + }, + + 'bundling with image from asset'(test: Test) { + const imageId = 'abcdef123456'; + const spawnSyncStub = sinon.stub(child_process, 'spawnSync').returns({ + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from(`Successfully built ${imageId}`), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }); + + const image = BundlingDockerImage.fromAsset('docker-path', { + buildArgs: { + TEST_ARG: 'cdk-test', + }, + }); + image._run(); + + test.ok(spawnSyncStub.firstCall.calledWith('docker', [ + 'build', + '--build-arg', 'TEST_ARG=cdk-test', + 'docker-path', + ])); + + test.ok(spawnSyncStub.secondCall.calledWith('docker', [ + 'run', '--rm', + imageId, + ])); + test.done(); + }, + + 'throws if image id cannot be extracted from build output'(test: Test) { + sinon.stub(child_process, 'spawnSync').returns({ + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from('stdout'), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }); + + test.throws(() => BundlingDockerImage.fromAsset('docker-path'), /Failed to extract image ID from Docker build output/); + test.done(); + }, + + 'throws in case of spawnSync error'(test: Test) { + sinon.stub(child_process, 'spawnSync').returns({ + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from('stdout'), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + error: new Error('UnknownError'), + }); + + const image = BundlingDockerImage.fromRegistry('alpine'); + test.throws(() => image._run(), /UnknownError/); + test.done(); + }, + + 'throws if status is not 0'(test: Test) { + sinon.stub(child_process, 'spawnSync').returns({ + status: -1, + stderr: Buffer.from('stderr'), + stdout: Buffer.from('stdout'), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }); + + const image = BundlingDockerImage.fromRegistry('alpine'); + test.throws(() => image._run(), /\[Status -1\]/); + test.done(); + }, +}; diff --git a/packages/@aws-cdk/core/test/test.staging.ts b/packages/@aws-cdk/core/test/test.staging.ts index 3faeea3e95396..5d5ab521eba59 100644 --- a/packages/@aws-cdk/core/test/test.staging.ts +++ b/packages/@aws-cdk/core/test/test.staging.ts @@ -2,7 +2,7 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs'; import { Test } from 'nodeunit'; import * as path from 'path'; -import { App, AssetStaging, Stack } from '../lib'; +import { App, AssetHashType, AssetStaging, BundlingDockerImage, Stack } from '../lib'; export = { 'base case'(test: Test) { @@ -74,4 +74,154 @@ export = { test.deepEqual(withExtra.sourceHash, 'c95c915a5722bb9019e2c725d11868e5a619b55f36172f76bcbcaa8bb2d10c5f'); test.done(); }, + + 'with bundling'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN + new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: ['touch', '/asset-output/test.txt'], + }, + }); + + // THEN + const assembly = app.synth(); + test.deepEqual(fs.readdirSync(assembly.directory), [ + 'asset.2f37f937c51e2c191af66acf9b09f548926008ec68c575bd2ee54b6e997c0e00', + 'cdk.out', + 'manifest.json', + 'stack.template.json', + 'tree.json', + ]); + + test.done(); + }, + + 'bundling throws when /asset-ouput is empty'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // THEN + test.throws(() => new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + }, + }), /Bundling did not produce any output/); + + test.done(); + }, + + 'bundling with BUNDLE asset hash type'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN + const asset = new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: ['touch', '/asset-output/test.txt'], + }, + assetHashType: AssetHashType.BUNDLE, + }); + + test.equal(asset.assetHash, '33cbf2cae5432438e0f046bc45ba8c3cef7b6afcf47b59d1c183775c1918fb1f'); + + test.done(); + }, + + 'custom hash'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN + const asset = new AssetStaging(stack, 'Asset', { + sourcePath: directory, + assetHash: 'my-custom-hash', + }); + + test.equal(asset.assetHash, 'my-custom-hash'); + + test.done(); + }, + + 'throws with assetHash and not CUSTOM hash type'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // THEN + test.throws(() => new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: ['touch', '/asset-output/test.txt'], + }, + assetHash: 'my-custom-hash', + assetHashType: AssetHashType.BUNDLE, + }), /Cannot specify `bundle` for `assetHashType`/); + + test.done(); + }, + + 'throws with BUNDLE hash type and no bundling'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // THEN + test.throws(() => new AssetStaging(stack, 'Asset', { + sourcePath: directory, + assetHashType: AssetHashType.BUNDLE, + }), /Cannot use `AssetHashType.BUNDLE` when `bundling` is not specified/); + + test.done(); + }, + + 'throws with CUSTOM and no hash'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // THEN + test.throws(() => new AssetStaging(stack, 'Asset', { + sourcePath: directory, + assetHashType: AssetHashType.CUSTOM, + }), /`assetHash` must be specified when `assetHashType` is set to `AssetHashType.CUSTOM`/); + + test.done(); + }, + + 'throws when bundling fails'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // THEN + test.throws(() => new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('this-is-an-invalid-docker-image'), + }, + }), /Failed to run bundling Docker image for asset stack\/Asset/); + + test.done(); + }, }; From 706150e36678c48ff7cb79795b37482c3f1dfad2 Mon Sep 17 00:00:00 2001 From: Romain Marcadier Date: Tue, 9 Jun 2020 14:15:08 +0200 Subject: [PATCH 29/32] chore: remove awkward cfn2ts script entries (#8231) Packages that are not containers of L1 libraries (`Cfn~` classes) have no point in having a `cfn2ts` script registered. This causes problems when trying to generate L1s across the whole repository using `lerna run cfn2ts`. This adds a `pkglint` rule that mandates the `cfn2ts` script is only present when the related other metadata is also required to be present. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-autoscaling-hooktargets/package.json | 1 - .../@aws-cdk/aws-cloudwatch-actions/package.json | 1 - packages/@aws-cdk/aws-docdb/package.json | 2 +- packages/@aws-cdk/aws-dynamodb-global/package.json | 2 -- packages/@aws-cdk/aws-ecs-patterns/package.json | 1 - .../aws-elasticloadbalancingv2-actions/package.json | 1 - .../aws-elasticloadbalancingv2-targets/package.json | 1 - packages/@aws-cdk/aws-events-targets/package.json | 1 - .../@aws-cdk/aws-lambda-destinations/package.json | 1 - packages/@aws-cdk/aws-lambda-nodejs/package.json | 1 - packages/@aws-cdk/aws-logs-destinations/package.json | 1 - packages/@aws-cdk/aws-route53-patterns/package.json | 1 - packages/@aws-cdk/aws-route53-targets/package.json | 1 - packages/@aws-cdk/aws-ses-actions/package.json | 1 - packages/@aws-cdk/aws-sns-subscriptions/package.json | 1 - .../@aws-cdk/aws-stepfunctions-tasks/package.json | 1 - packages/@aws-cdk/custom-resources/package.json | 1 - packages/aws-cdk/package.json | 2 +- packages/cdk-assets/package.json | 1 - tools/pkglint/lib/rules.ts | 10 +++------- tools/pkglint/package.json | 8 ++++---- tools/yarn-cling/.eslintrc.js | 3 +++ tools/yarn-cling/.gitignore | 5 +++++ tools/yarn-cling/.npmignore | 2 ++ tools/yarn-cling/lib/index.ts | 6 +++--- tools/yarn-cling/package.json | 12 ++++++++++-- .../test/test-fixture/.no-packagejson-validator | 1 + 27 files changed, 33 insertions(+), 36 deletions(-) create mode 100644 tools/yarn-cling/.eslintrc.js create mode 100644 tools/yarn-cling/test/test-fixture/.no-packagejson-validator diff --git a/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json b/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json index 6a8d4669fc8fc..fc353a027cd84 100644 --- a/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json +++ b/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/package.json b/packages/@aws-cdk/aws-cloudwatch-actions/package.json index 8f8d1edd98766..b0fda2aa4968d 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/package.json +++ b/packages/@aws-cdk/aws-cloudwatch-actions/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" diff --git a/packages/@aws-cdk/aws-docdb/package.json b/packages/@aws-cdk/aws-docdb/package.json index cde479290182a..3fa00723a3c94 100644 --- a/packages/@aws-cdk/aws-docdb/package.json +++ b/packages/@aws-cdk/aws-docdb/package.json @@ -51,7 +51,7 @@ "cloudformation": "AWS::DocDB", "jest": true }, -"keywords": [ + "keywords": [ "aws", "cdk", "constructs", diff --git a/packages/@aws-cdk/aws-dynamodb-global/package.json b/packages/@aws-cdk/aws-dynamodb-global/package.json index c280e52d7ffc4..e214cbbbb210c 100644 --- a/packages/@aws-cdk/aws-dynamodb-global/package.json +++ b/packages/@aws-cdk/aws-dynamodb-global/package.json @@ -57,7 +57,6 @@ "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "cfn2ts": "0.0.0", "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, @@ -77,7 +76,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" diff --git a/packages/@aws-cdk/aws-ecs-patterns/package.json b/packages/@aws-cdk/aws-ecs-patterns/package.json index 451153745909f..16bcc51f7e25e 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/package.json +++ b/packages/@aws-cdk/aws-ecs-patterns/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/package.json b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/package.json index c1a5a92fb1053..ba866cf3a4dee 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/package.json b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/package.json index 768f9e7eebc7f..fdef0856ac238 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" diff --git a/packages/@aws-cdk/aws-events-targets/package.json b/packages/@aws-cdk/aws-events-targets/package.json index b9f9efad57c95..64b7060517815 100644 --- a/packages/@aws-cdk/aws-events-targets/package.json +++ b/packages/@aws-cdk/aws-events-targets/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" diff --git a/packages/@aws-cdk/aws-lambda-destinations/package.json b/packages/@aws-cdk/aws-lambda-destinations/package.json index 92805e6004bd9..d02ddde7aa635 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/package.json +++ b/packages/@aws-cdk/aws-lambda-destinations/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" diff --git a/packages/@aws-cdk/aws-lambda-nodejs/package.json b/packages/@aws-cdk/aws-lambda-nodejs/package.json index b415c57d92d9e..31995f757c849 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/package.json +++ b/packages/@aws-cdk/aws-lambda-nodejs/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" diff --git a/packages/@aws-cdk/aws-logs-destinations/package.json b/packages/@aws-cdk/aws-logs-destinations/package.json index c7e151e165b6c..bfa6f4a73f371 100644 --- a/packages/@aws-cdk/aws-logs-destinations/package.json +++ b/packages/@aws-cdk/aws-logs-destinations/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" diff --git a/packages/@aws-cdk/aws-route53-patterns/package.json b/packages/@aws-cdk/aws-route53-patterns/package.json index 09de36a416910..56855cc2c70b0 100644 --- a/packages/@aws-cdk/aws-route53-patterns/package.json +++ b/packages/@aws-cdk/aws-route53-patterns/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" diff --git a/packages/@aws-cdk/aws-route53-targets/package.json b/packages/@aws-cdk/aws-route53-targets/package.json index 80d2e61663189..f7ab4f96b29b9 100644 --- a/packages/@aws-cdk/aws-route53-targets/package.json +++ b/packages/@aws-cdk/aws-route53-targets/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" diff --git a/packages/@aws-cdk/aws-ses-actions/package.json b/packages/@aws-cdk/aws-ses-actions/package.json index 92472cec0d6bf..98ceb9b2cd0b6 100644 --- a/packages/@aws-cdk/aws-ses-actions/package.json +++ b/packages/@aws-cdk/aws-ses-actions/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" diff --git a/packages/@aws-cdk/aws-sns-subscriptions/package.json b/packages/@aws-cdk/aws-sns-subscriptions/package.json index 07ea20698e7ed..13535b66faf0a 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/package.json +++ b/packages/@aws-cdk/aws-sns-subscriptions/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json index 8f62cc2143eb5..7a8d6299c4072 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" diff --git a/packages/@aws-cdk/custom-resources/package.json b/packages/@aws-cdk/custom-resources/package.json index 7cd16b421cb1b..561ac6ef58a6f 100644 --- a/packages/@aws-cdk/custom-resources/package.json +++ b/packages/@aws-cdk/custom-resources/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 488b57c0d0a13..1ac487d577742 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -100,7 +100,7 @@ ], "homepage": "https://github.com/aws/aws-cdk", "engines": { - "node": ">= 10.13.0 <13 || >=13.7.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "stable", "maturity": "stable" diff --git a/packages/cdk-assets/package.json b/packages/cdk-assets/package.json index eb67aa7d3b6f8..4c8f00e772f30 100644 --- a/packages/cdk-assets/package.json +++ b/packages/cdk-assets/package.json @@ -16,7 +16,6 @@ "pkglint": "pkglint -f", "test": "cdk-test", "watch": "cdk-watch", - "cfn2ts": "cfn2ts", "build+test": "npm run build && npm test", "build+test+package": "npm run build+test && npm run package", "compat": "cdk-compat" diff --git a/tools/pkglint/lib/rules.ts b/tools/pkglint/lib/rules.ts index 48c613758bfee..53bb5e5a894b3 100644 --- a/tools/pkglint/lib/rules.ts +++ b/tools/pkglint/lib/rules.ts @@ -1005,12 +1005,8 @@ export class Cfn2Ts extends ValidationRule { public readonly name = 'cfn2ts'; public validate(pkg: PackageJson) { - if (!isJSII(pkg)) { - return; - } - - if (!isAWS(pkg)) { - return; + if (!isJSII(pkg) || !isAWS(pkg)) { + return expectJSON(this.name, pkg, 'scripts.cfn2ts', undefined); } expectJSON(this.name, pkg, 'scripts.cfn2ts', 'cfn2ts'); @@ -1253,7 +1249,7 @@ function isJSII(pkg: PackageJson): boolean { * @param pkg */ function isAWS(pkg: PackageJson): boolean { - return pkg.json['cdk-build'] && pkg.json['cdk-build'].cloudformation; + return pkg.json['cdk-build']?.cloudformation != null; } /** diff --git a/tools/pkglint/package.json b/tools/pkglint/package.json index 5ce02cb64d1df..2118a2c677bb6 100644 --- a/tools/pkglint/package.json +++ b/tools/pkglint/package.json @@ -17,9 +17,8 @@ }, "scripts": { "build": "tsc -b && tslint -p . && chmod +x bin/pkglint", - "test": "echo success", - "build+test": "npm run build && npm test", - "build+test+package": "npm run build+test", + "build+test": "npm run build", + "build+test+package": "npm run build", "watch": "tsc -b -w", "lint": "tsc -b && tslint -p . --force" }, @@ -37,7 +36,8 @@ "devDependencies": { "@types/fs-extra": "^8.1.0", "@types/semver": "^7.2.0", - "@types/yargs": "^15.0.5" + "@types/yargs": "^15.0.5", + "typescript": "~3.8.3" }, "dependencies": { "case": "^1.6.3", diff --git a/tools/yarn-cling/.eslintrc.js b/tools/yarn-cling/.eslintrc.js new file mode 100644 index 0000000000000..0c60e21090199 --- /dev/null +++ b/tools/yarn-cling/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/tools/yarn-cling/.gitignore b/tools/yarn-cling/.gitignore index d05ddbf403f73..2d8e8a2d36377 100644 --- a/tools/yarn-cling/.gitignore +++ b/tools/yarn-cling/.gitignore @@ -6,3 +6,8 @@ dist .LAST_BUILD *.snk !jest.config.js + +.nyc_output +coverage +nyc.config.js +!.eslintrc.js \ No newline at end of file diff --git a/tools/yarn-cling/.npmignore b/tools/yarn-cling/.npmignore index e049d31151c8f..af12b026f1401 100644 --- a/tools/yarn-cling/.npmignore +++ b/tools/yarn-cling/.npmignore @@ -8,3 +8,5 @@ coverage .LAST_BUILD *.snk jest.config.js + +.eslintrc.js \ No newline at end of file diff --git a/tools/yarn-cling/lib/index.ts b/tools/yarn-cling/lib/index.ts index 816f55e88e97d..eabbf390c5207 100644 --- a/tools/yarn-cling/lib/index.ts +++ b/tools/yarn-cling/lib/index.ts @@ -31,7 +31,7 @@ export async function generateShrinkwrap(options: ShrinkwrapOptions): Promise { - return JSON.parse(await fs.readFile(fileName, { encoding: 'utf-8' })); + return JSON.parse(await fs.readFile(fileName, { encoding: 'utf8' })); } async function fileExists(fullPath: string): Promise { diff --git a/tools/yarn-cling/package.json b/tools/yarn-cling/package.json index 08403bca30b31..ca172a1fbbead 100644 --- a/tools/yarn-cling/package.json +++ b/tools/yarn-cling/package.json @@ -29,11 +29,19 @@ "organization": true }, "license": "Apache-2.0", + "pkglint": { + "exclude": [ + "dependencies/build-tools", + "package-info/scripts/build", + "package-info/scripts/watch", + "package-info/scripts/test" + ] + }, "devDependencies": { "@types/yarnpkg__lockfile": "^1.1.3", "@types/jest": "^25.2.3", "jest": "^25.5.4", - "@types/node": "^13.9.1", + "@types/node": "^10.17.5", "typescript": "~3.8.3", "pkglint": "0.0.0" }, @@ -46,6 +54,6 @@ ], "homepage": "https://github.com/aws/aws-cdk", "engines": { - "node": ">= 10.3.0" + "node": ">= 10.13.0 <13 || >=13.7.0" } } diff --git a/tools/yarn-cling/test/test-fixture/.no-packagejson-validator b/tools/yarn-cling/test/test-fixture/.no-packagejson-validator new file mode 100644 index 0000000000000..6824459f6c5e0 --- /dev/null +++ b/tools/yarn-cling/test/test-fixture/.no-packagejson-validator @@ -0,0 +1 @@ +Test fixtures should not be affected. From b50bb83fad051652f19a989d9424faa384f081b5 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2020 13:08:14 +0000 Subject: [PATCH 30/32] chore(deps-dev): bump @types/node from 10.17.21 to 10.17.25 (#8440) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 10.17.21 to 10.17.25. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- packages/@aws-cdk/core/package.json | 2 +- .../@monocdk-experiment/assert/package.json | 2 +- .../rewrite-imports/package.json | 2 +- packages/aws-cdk/package.json | 2 +- packages/cdk-assets/package.json | 2 +- packages/monocdk-experiment/package.json | 2 +- tools/yarn-cling/package.json | 2 +- yarn.lock | 18 ++++-------------- 8 files changed, 11 insertions(+), 21 deletions(-) diff --git a/packages/@aws-cdk/core/package.json b/packages/@aws-cdk/core/package.json index 3e5b22b6b8a74..c1066fd4799a6 100644 --- a/packages/@aws-cdk/core/package.json +++ b/packages/@aws-cdk/core/package.json @@ -152,7 +152,7 @@ "license": "Apache-2.0", "devDependencies": { "@types/lodash": "^4.14.155", - "@types/node": "^10.17.21", + "@types/node": "^10.17.25", "@types/nodeunit": "^0.0.31", "@types/minimatch": "^3.0.3", "@types/sinon": "^9.0.4", diff --git a/packages/@monocdk-experiment/assert/package.json b/packages/@monocdk-experiment/assert/package.json index 5da40f1a0097f..762a4b67eb193 100644 --- a/packages/@monocdk-experiment/assert/package.json +++ b/packages/@monocdk-experiment/assert/package.json @@ -37,7 +37,7 @@ "license": "Apache-2.0", "devDependencies": { "@types/jest": "^25.2.3", - "@types/node": "^10.17.24", + "@types/node": "^10.17.25", "cdk-build-tools": "0.0.0", "jest": "^25.5.4", "pkglint": "0.0.0", diff --git a/packages/@monocdk-experiment/rewrite-imports/package.json b/packages/@monocdk-experiment/rewrite-imports/package.json index ac8731f60ff8d..1c1a44d1758a0 100644 --- a/packages/@monocdk-experiment/rewrite-imports/package.json +++ b/packages/@monocdk-experiment/rewrite-imports/package.json @@ -36,7 +36,7 @@ "devDependencies": { "@types/glob": "^7.1.1", "@types/jest": "^25.2.3", - "@types/node": "^10.17.21", + "@types/node": "^10.17.25", "cdk-build-tools": "0.0.0", "pkglint": "0.0.0" }, diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 1ac487d577742..7a056f1b1ba02 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -47,7 +47,7 @@ "@types/jest": "^25.2.3", "@types/minimatch": "^3.0.3", "@types/mockery": "^1.4.29", - "@types/node": "^10.17.21", + "@types/node": "^10.17.25", "@types/promptly": "^3.0.0", "@types/semver": "^7.2.0", "@types/sinon": "^9.0.4", diff --git a/packages/cdk-assets/package.json b/packages/cdk-assets/package.json index 4c8f00e772f30..91c272bb380c8 100644 --- a/packages/cdk-assets/package.json +++ b/packages/cdk-assets/package.json @@ -34,7 +34,7 @@ "@types/glob": "^7.1.1", "@types/jest": "^25.2.3", "@types/mock-fs": "^4.10.0", - "@types/node": "^10.17.21", + "@types/node": "^10.17.25", "@types/yargs": "^15.0.5", "@types/jszip": "^3.4.1", "jszip": "^3.4.0", diff --git a/packages/monocdk-experiment/package.json b/packages/monocdk-experiment/package.json index 6979ba08618d3..24d6079e29cee 100644 --- a/packages/monocdk-experiment/package.json +++ b/packages/monocdk-experiment/package.json @@ -246,7 +246,7 @@ "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/region-info": "0.0.0", "@types/fs-extra": "^8.1.1", - "@types/node": "^10.17.24", + "@types/node": "^10.17.25", "cdk-build-tools": "0.0.0", "fs-extra": "^9.0.1", "pkglint": "0.0.0", diff --git a/tools/yarn-cling/package.json b/tools/yarn-cling/package.json index ca172a1fbbead..2c87c0e9467ec 100644 --- a/tools/yarn-cling/package.json +++ b/tools/yarn-cling/package.json @@ -41,7 +41,7 @@ "@types/yarnpkg__lockfile": "^1.1.3", "@types/jest": "^25.2.3", "jest": "^25.5.4", - "@types/node": "^10.17.5", + "@types/node": "^10.17.25", "typescript": "~3.8.3", "pkglint": "0.0.0" }, diff --git a/yarn.lock b/yarn.lock index 92848de826535..3434b584d847c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1546,20 +1546,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.0.tgz#30d2d09f623fe32cde9cb582c7a6eda2788ce4a8" integrity sha512-WE4IOAC6r/yBZss1oQGM5zs2D7RuKR6Q+w+X2SouPofnWn+LbCqClRyhO3ZE7Ix8nmFgo/oVuuE01cJT2XB13A== -"@types/node@^10.17.21": - version "10.17.21" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.21.tgz#c00e9603399126925806bed2d9a1e37da506965e" - integrity sha512-PQKsydPxYxF1DsAFWmunaxd3sOi3iMt6Zmx/tgaagHYmwJ/9cRH91hQkeJZaUGWbvn0K5HlSVEXkn5U/llWPpQ== - -"@types/node@^10.17.24": - version "10.17.24" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.24.tgz#c57511e3a19c4b5e9692bb2995c40a3a52167944" - integrity sha512-5SCfvCxV74kzR3uWgTYiGxrd69TbT1I6+cMx1A5kEly/IVveJBimtAMlXiEyVFn5DvUFewQWxOOiJhlxeQwxgA== - -"@types/node@^13.9.1": - version "13.13.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.9.tgz#79df4ae965fb76d31943b54a6419599307a21394" - integrity sha512-EPZBIGed5gNnfWCiwEIwTE2Jdg4813odnG8iNPMQGrqVxrI+wL68SPtPeCX+ZxGBaA6pKAVc6jaKgP/Q0QzfdQ== +"@types/node@^10.17.25": + version "10.17.25" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.25.tgz#64f64cd3e8641e8163c81045e545d2825d300e37" + integrity sha512-EWPw3jDB0jip4HafDkoezNOwG00TtVZ1TOe74MaxIBWgpyM60UF/LXzFVx9+8AdSYNNOPgx7TuJoRmgnhHZ/7g== "@types/nodeunit@^0.0.31": version "0.0.31" From ed6f763bddbb2090bbf07e5bbd6c7710a54dd33d Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 9 Jun 2020 15:56:21 +0200 Subject: [PATCH 31/32] feat(assert): more powerful matchers (#8444) In order to write better assertions on complex resource structs that only test what we're interested in (and not properties that may accidentally change as part of unrelated refactors), add more powerful matchers that can express things like: - `objectLike()` - `arrayWith()` - `stringContaining()` (not implemented by default but easy to add now) We can now write: ```ts expect(stack).toHaveResourceLike('AWS::S3::BucketPolicy', { PolicyDocument: { Statement: arrayWith(objectLike({ Action: arrayWith('s3:GetObject*', 's3:GetBucket*', 's3:List*'), Principal: { AWS: { 'Fn::Sub': stringContaining('-deploy-role-') } } })) } }); ``` And be invariant to things like the order of elements in the arrays, and default role name qualifiers. Refactor the old assertions to be epxressed in terms of the new matchers. NOTE: Matchers are now functions, which won't translate into jsii in the future. It will be easy enough to make them single-method objects in the future when we move this library (or a similar one to jsii). For now, I did not want to let that impact the design. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/assert/README.md | 31 +- .../assert/lib/assertions/have-resource.ts | 322 ++++++++++++++---- .../assert/test/have-resource.test.ts | 102 +++++- 3 files changed, 383 insertions(+), 72 deletions(-) diff --git a/packages/@aws-cdk/assert/README.md b/packages/@aws-cdk/assert/README.md index 71c19f3652a51..c81fac74562e9 100644 --- a/packages/@aws-cdk/assert/README.md +++ b/packages/@aws-cdk/assert/README.md @@ -63,6 +63,7 @@ If you only care that a resource of a particular type exists (regardless of its ```ts haveResource(type, subsetOfProperties) +haveResourceLike(type, subsetOfProperties) ``` Example: @@ -76,7 +77,35 @@ expect(stack).to(haveResource('AWS::CertificateManager::Certificate', { })); ``` -`ABSENT` is a magic value to assert that a particular key in an object is *not* set (or set to `undefined`). +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`). +- `arrayWith(...)`/`objectLike(...)`/`deepObjectLike(...)`/`exactValue()`: special matchers + for inexact matching. You can use these to match arrays where not all elements have to match, + just a single one, or objects where not all keys have to match. + +The difference between `haveResource` and `haveResourceLike` is the same as +between `objectLike` and `deepObjectLike`: the first allows +additional (unspecified) object keys only at the *first* level, while the +second one allows them in nested objects as well. + +If you want to escape from the "deep lenient matching" behavior, you can use +`exactValue()`. + +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'], + }}) + } +})); +``` ### Check number of resources diff --git a/packages/@aws-cdk/assert/lib/assertions/have-resource.ts b/packages/@aws-cdk/assert/lib/assertions/have-resource.ts index cf7b9c6d15da1..3676f06352068 100644 --- a/packages/@aws-cdk/assert/lib/assertions/have-resource.ts +++ b/packages/@aws-cdk/assert/lib/assertions/have-resource.ts @@ -40,21 +40,24 @@ export function haveResourceLike( return haveResource(resourceType, properties, comparison, true); } -type PropertyPredicate = (props: any, inspection: InspectionFailure) => boolean; +export type PropertyMatcher = (props: any, inspection: InspectionFailure) => boolean; export class HaveResourceAssertion extends JestFriendlyAssertion { private readonly inspected: InspectionFailure[] = []; private readonly part: ResourcePart; - private readonly predicate: PropertyPredicate; + private readonly matcher: any; constructor( private readonly resourceType: string, - private readonly properties?: any, + properties?: any, part?: ResourcePart, allowValueExtension: boolean = false) { super(); - this.predicate = typeof properties === 'function' ? properties : makeSuperObjectPredicate(properties, allowValueExtension); + this.matcher = isCallable(properties) ? properties : + properties === undefined ? anything() : + allowValueExtension ? deepObjectLike(properties) : + objectLike(properties); this.part = part !== undefined ? part : ResourcePart.Properties; } @@ -68,7 +71,7 @@ export class HaveResourceAssertion extends JestFriendlyAssertion // to maintain backwards compatibility with old predicate API. const inspection = { resource, failureReason: 'Object did not match predicate' }; - if (this.predicate(propsToCheck, inspection)) { + if (match(propsToCheck, this.matcher, inspection)) { return true; } @@ -99,7 +102,7 @@ export class HaveResourceAssertion extends JestFriendlyAssertion public get description(): string { // tslint:disable-next-line:max-line-length - return `resource '${this.resourceType}' with properties ${JSON.stringify(this.properties, undefined, 2)}`; + return `resource '${this.resourceType}' with ${JSON.stringify(this.matcher, undefined, 2)}`; } } @@ -108,111 +111,275 @@ function indent(n: number, s: string) { return prefix + s.replace(/\n/g, '\n' + prefix); } -/** - * Make a predicate that checks property superset - */ -function makeSuperObjectPredicate(obj: any, allowValueExtension: boolean) { - return (resourceProps: any, inspection: InspectionFailure) => { - const errors: string[] = []; - const ret = isSuperObject(resourceProps, obj, errors, allowValueExtension); - inspection.failureReason = errors.join(','); - return ret; - }; -} - export interface InspectionFailure { resource: any; failureReason: string; } /** - * Return whether `superObj` is a super-object of `obj`. + * Match a given literal value against a matcher * - * A super-object has the same or more property values, recursing into sub properties if ``allowValueExtension`` is true. + * If the matcher is a callable, use that to evaluate the value. Otherwise, the values + * must be literally the same. */ -export function isSuperObject(superObj: any, pattern: any, errors: string[] = [], allowValueExtension: boolean = false): boolean { +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; } - if (Array.isArray(superObj) !== Array.isArray(pattern)) { - errors.push('Array type mismatch'); - return false; + + const errors = new Array(); + + if (Array.isArray(value) !== Array.isArray(pattern)) { + return failMatcher(inspection, 'Array type mismatch'); } - if (Array.isArray(superObj)) { - if (pattern.length !== superObj.length) { - errors.push('Array length mismatch'); - return false; + if (Array.isArray(value)) { + if (pattern.length !== value.length) { + return failMatcher(inspection, 'Array length mismatch'); } - // Do isSuperObject comparison for individual objects + // Recurse comparison for individual objects for (let i = 0; i < pattern.length; i++) { - if (!isSuperObject(superObj[i], pattern[i], [], allowValueExtension)) { + if (!match(value[i], pattern[i], { ...inspection })) { errors.push(`Array element ${i} mismatch`); } } - return errors.length === 0; + + if (errors.length > 0) { + return failMatcher(inspection, errors.join(', ')); + } + return true; } - if ((typeof superObj === 'object') !== (typeof pattern === 'object')) { - errors.push('Object type mismatch'); - return false; + 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; +} + +/** + * 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. + */ +function failMatcher(inspection: InspectionFailure, error: string): boolean { + inspection.failureReason = error; + return false; +} + +/** + * 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); +} + +export function _objectContaining(pattern: A, deep: boolean): PropertyMatcher { + const ret = (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 (superObj[patternKey] !== undefined) { errors.push(`Field ${patternKey} present, but shouldn't be`); } + if (value[patternKey] !== undefined) { errors.push(`Field ${patternKey} present, but shouldn't be`); } continue; } - if (!(patternKey in superObj)) { + if (!(patternKey in value)) { errors.push(`Field ${patternKey} missing`); continue; } - const innerErrors = new Array(); - const valueMatches = allowValueExtension - ? isSuperObject(superObj[patternKey], patternValue, innerErrors, allowValueExtension) - : isStrictlyEqual(superObj[patternKey], patternValue, innerErrors); + // 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: ${innerErrors.join(', ')}`); + errors.push(`Field ${patternKey} mismatch: ${innerInspection.failureReason}`); } } - return errors.length === 0; - } - if (superObj !== pattern) { - errors.push('Different values'); - } - return errors.length === 0; + /** + * 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; + }; + + // Override toJSON so that our error messages print an readable version of this matcher + // (which we produce by doing JSON.stringify() at some point in the future). + ret.toJSON = () => ({ [deep ? '$deepObjectLike' : '$objectLike']: pattern }); + return ret; } -function isStrictlyEqual(left: any, pattern: any, errors: string[]): boolean { - if (left === pattern) { return true; } - if (typeof left !== typeof pattern) { - errors.push(`${typeof left} !== ${typeof pattern}`); - return false; - } +/** + * 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 ret = (value: any, inspection: InspectionFailure): boolean => { + return matchLiteral(value, expected, inspection); + }; - if (typeof left === 'object' && typeof pattern === 'object') { - if (Array.isArray(left) !== Array.isArray(pattern)) { return false; } - const allKeys = new Set([...Object.keys(left), ...Object.keys(pattern)]); - for (const key of allKeys) { - if (pattern[key] === ABSENT) { - if (left[key] !== undefined) { - errors.push(`Field ${key} present, but shouldn't be`); - return false; - } - return true; + // Override toJSON so that our error messages print an readable version of this matcher + // (which we produce by doing JSON.stringify() at some point in the future). + ret.toJSON = () => ({ $exactValue: expected }); + return ret; +} + +/** + * 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 ret = (value: any, inspection: InspectionFailure): boolean => { + if (!Array.isArray(value)) { + return failMatcher(inspection, `Expect an object 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; + } - const innerErrors = new Array(); - if (!isStrictlyEqual(left[key], pattern[key], innerErrors)) { - errors.push(`${Array.isArray(left) ? 'element ' : ''}${key}: ${innerErrors.join(', ')}`); - return false; + if (fail === null || innerInspection.failureReason.length > fail[1].length) { + fail = [i, innerInspection.failureReason]; + } } + return fail; } + }; + + // Override toJSON so that our error messages print an readable version of this matcher + // (which we produce by doing JSON.stringify() at some point in the future). + ret.toJSON = () => ({ $arrayContaining: elements.length === 1 ? elements[0] : elements }); + return ret; +} + +/** + * Matches anything + */ +function anything() { + const ret = () => { return true; - } + }; + ret.toJSON = () => ({ $anything: true }); + return ret; +} - errors.push(`${left} !== ${pattern}`); - return false; +/** + * 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; } /** @@ -231,3 +398,18 @@ export enum ResourcePart { */ CompleteDefinition } + +/** + * Whether a value is a callable + */ +function isCallable(x: any): x is ((...args: any[]) => any) { + return x && {}.toString.call(x) === '[object Function]'; +} + +/** + * Whether a value is an object + */ +function isObject(x: any): x is object { + // Because `typeof null === 'object'`. + return x && typeof x === 'object'; +} \ No newline at end of file diff --git a/packages/@aws-cdk/assert/test/have-resource.test.ts b/packages/@aws-cdk/assert/test/have-resource.test.ts index b523fd2a8bcfc..69ab649433350 100644 --- a/packages/@aws-cdk/assert/test/have-resource.test.ts +++ b/packages/@aws-cdk/assert/test/have-resource.test.ts @@ -2,7 +2,7 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; import { writeFileSync } from 'fs'; import { join } from 'path'; -import { ABSENT, expect as cdkExpect, haveResource } from '../lib/index'; +import { ABSENT, arrayWith, exactValue, expect as cdkExpect, haveResource, haveResourceLike } from '../lib/index'; test('support resource with no properties', () => { const synthStack = mkStack({ @@ -138,6 +138,106 @@ describe('property absence', () => { }).toThrowError(/Prop/); }); + test('can use matcher to test for list element', () => { + const synthStack = mkSomeResource({ + List: [ + { Prop: 'distraction' }, + { Prop: 'goal' }, + ], + }); + + expect(() => { + cdkExpect(synthStack).to(haveResource('Some::Resource', { + List: arrayWith({ Prop: 'goal' }), + })); + }).not.toThrowError(); + + expect(() => { + cdkExpect(synthStack).to(haveResource('Some::Resource', { + List: arrayWith({ Prop: 'missme' }), + })); + }).toThrowError(/Array did not contain expected element/); + }); + + test('arrayContaining must match all elements in any order', () => { + const synthStack = mkSomeResource({ + List: ['a', 'b'], + }); + + expect(() => { + cdkExpect(synthStack).to(haveResource('Some::Resource', { + List: arrayWith('b', 'a'), + })); + }).not.toThrowError(); + + expect(() => { + cdkExpect(synthStack).to(haveResource('Some::Resource', { + List: arrayWith('a', 'c'), + })); + }).toThrowError(/Array did not contain expected element/); + }); + + test('exactValue escapes from deep fuzzy matching', () => { + const synthStack = mkSomeResource({ + Deep: { + PropA: 'A', + PropB: 'B', + }, + }); + + expect(() => { + cdkExpect(synthStack).to(haveResourceLike('Some::Resource', { + Deep: { + PropA: 'A', + }, + })); + }).not.toThrowError(); + + expect(() => { + cdkExpect(synthStack).to(haveResourceLike('Some::Resource', { + Deep: exactValue({ + PropA: 'A', + }), + })); + }).toThrowError(/Unexpected keys present in object/); + }); + + /** + * Backwards compatibility test + * + * If we had designed this with a matcher library from the start, we probably wouldn't + * have had this behavior, but here we are. + * + * Historically, when we do `haveResourceLike` (which maps to `objectContainingDeep`) with + * a pattern containing lists of objects, the objects inside the list are also matched + * as 'containing' keys (instead of having to completely 'match' the pattern objects). + * + * People will have written assertions depending on this behavior, so we have to maintain + * it. + */ + test('objectContainingDeep has deep effect through lists', () => { + const synthStack = mkSomeResource({ + List: [ + { + PropA: 'A', + PropB: 'B', + }, + { + PropA: 'A', + PropB: 'B', + }, + ], + }); + + expect(() => { + cdkExpect(synthStack).to(haveResourceLike('Some::Resource', { + List: [ + { PropA: 'A' }, + { PropB: 'B' }, + ], + })); + }).not.toThrowError(); + }); }); function mkStack(template: any): cxapi.CloudFormationStackArtifact { From c07141e1ddd41c7a4e992313bf90b92e56c8955a Mon Sep 17 00:00:00 2001 From: AWS CDK Team Date: Tue, 9 Jun 2020 14:11:26 +0000 Subject: [PATCH 32/32] chore(release): 1.45.0 --- CHANGELOG.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ lerna.json | 2 +- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 797f21440a6fe..dbb4c1ce8f5ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,50 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.45.0](https://github.com/aws/aws-cdk/compare/v1.44.0...v1.45.0) (2020-06-09) + + +### ⚠ BREAKING CHANGES + +* **stepfunctions-tasks:** constructs for `SageMakerCreateTrainingJob` and +`SageMakerCreateTransformJob` replace previous implementation that +implemented `IStepFunctionsTask`. +* **stepfunctions-tasks:** `volumeSizeInGB` property in `ResourceConfig` for +SageMaker tasks are now type `core.Size` +* **stepfunctions-tasks:** `maxPayload` property in `SagemakerTransformProps` +is now type `core.Size` +* **stepfunctions-tasks:** `volumeKmsKeyId` property in `SageMakerCreateTrainingJob` is now `volumeEncryptionKey` +* **cognito:** `requiredAttributes` on `UserPool` construct is now replaced with `standardAttributes` with a slightly modified signature. +* **rds:** DatabaseClusterProps.kmsKey has been renamed to storageEncryptionKey +* **rds**: DatabaseInstanceNewProps.performanceInsightKmsKey has been renamed to performanceInsightEncryptionKey +* **rds**: DatabaseInstanceSourceProps.secretKmsKey has been renamed to masterUserPasswordEncryptionKey +* **rds**: DatabaseInstanceProps.kmsKey has been renamed to storageEncryptionKey +* **rds**: DatabaseInstanceReadReplicaProps.kmsKey has been renamed to storageEncryptionKey +* **rds**: Login.kmsKey has been renamed to encryptionKey + +### Features + +* **assert:** more powerful matchers ([#8444](https://github.com/aws/aws-cdk/issues/8444)) ([ed6f763](https://github.com/aws/aws-cdk/commit/ed6f763bddbb2090bbf07e5bbd6c7710a54dd33d)) +* **cloud9:** support AWS CodeCommit repository clone on launch ([#8205](https://github.com/aws/aws-cdk/issues/8205)) ([4781f94](https://github.com/aws/aws-cdk/commit/4781f94ee530ef66488fbf7b3728a753fa5718cd)), closes [#8204](https://github.com/aws/aws-cdk/issues/8204) +* **codestar:** support the GitHubRepository resource ([#8209](https://github.com/aws/aws-cdk/issues/8209)) ([02ddab8](https://github.com/aws/aws-cdk/commit/02ddab8c1e76c59ccaff4f45986de68d538d54eb)), closes [#8210](https://github.com/aws/aws-cdk/issues/8210) +* **cognito:** allow mutable attributes for requiredAttributes ([#7754](https://github.com/aws/aws-cdk/issues/7754)) ([1fabd98](https://github.com/aws/aws-cdk/commit/1fabd9819d4dbe64d175e73400078e435235d1d2)) +* **core,s3-assets,lambda:** custom asset bundling ([#7898](https://github.com/aws/aws-cdk/issues/7898)) ([888b412](https://github.com/aws/aws-cdk/commit/888b412797b2bcd7b8f1b8c5cbc0c25d94f91a5f)) +* **rds:** rename 'kmsKey' properties to 'encryptionKey' ([#8324](https://github.com/aws/aws-cdk/issues/8324)) ([4eefbbe](https://github.com/aws/aws-cdk/commit/4eefbbe612d4bd643bffd4dee525d88a921439cb)) +* **secretsmanager:** deletionPolicy for secretsmanager ([#8188](https://github.com/aws/aws-cdk/issues/8188)) ([f6fe36a](https://github.com/aws/aws-cdk/commit/f6fe36a0281a60ad65474b6ce0e22d0182ed2bea)), closes [#6527](https://github.com/aws/aws-cdk/issues/6527) +* **secretsmanager:** Secret.grantRead() also gives DescribeSecret permissions ([#8409](https://github.com/aws/aws-cdk/issues/8409)) ([f44ae60](https://github.com/aws/aws-cdk/commit/f44ae607670bccee21dfd390effa7d0e8701efd4)), closes [#6444](https://github.com/aws/aws-cdk/issues/6444) [#7953](https://github.com/aws/aws-cdk/issues/7953) +* **stepfunctions-tasks:** task constructs for creating and transforming SageMaker jobs ([#8391](https://github.com/aws/aws-cdk/issues/8391)) ([480d4c0](https://github.com/aws/aws-cdk/commit/480d4c004122f37533c22a14c6ecb89b5da07011)) + + +### Bug Fixes + +* **apigateway:** authorizerUri does not resolve to the correct partition ([#8152](https://github.com/aws/aws-cdk/issues/8152)) ([f455273](https://github.com/aws/aws-cdk/commit/f4552733909cd0734a7d829a35d0c1277b2ca4fc)), closes [#8098](https://github.com/aws/aws-cdk/issues/8098) +* **apigateway:** methodArn not replacing path parameters with asterisks ([#8206](https://github.com/aws/aws-cdk/issues/8206)) ([8fc3751](https://github.com/aws/aws-cdk/commit/8fc37513477f4d9a8a37e4b6979a79e8ba6a1efd)), closes [#8036](https://github.com/aws/aws-cdk/issues/8036) +* **aws-s3-deployment:** Set proper s-maxage Cache Control header ([#8434](https://github.com/aws/aws-cdk/issues/8434)) ([8d5b801](https://github.com/aws/aws-cdk/commit/8d5b801971ddaba82e0767c74fe7640d3e802c2f)), closes [#6292](https://github.com/aws/aws-cdk/issues/6292) +* **cognito:** error when using parameter for `domainPrefix` ([#8399](https://github.com/aws/aws-cdk/issues/8399)) ([681b3bb](https://github.com/aws/aws-cdk/commit/681b3bbc7de517c06ac0bd848b73cc6d7267dfa1)), closes [#8314](https://github.com/aws/aws-cdk/issues/8314) +* **dynamodb:** old global table replicas cannot be deleted ([#8224](https://github.com/aws/aws-cdk/issues/8224)) ([00884c7](https://github.com/aws/aws-cdk/commit/00884c752d6746864f2a71d093502d4fb2422037)), closes [#7189](https://github.com/aws/aws-cdk/issues/7189) +* **elbv2:** addAction ignores conditions ([#8385](https://github.com/aws/aws-cdk/issues/8385)) ([729cc0b](https://github.com/aws/aws-cdk/commit/729cc0b1705cab64696682f21985d97ce6c41607)), closes [#8328](https://github.com/aws/aws-cdk/issues/8328) +* **elbv2:** missing permission to write NLB access logs to S3 bucket ([#8114](https://github.com/aws/aws-cdk/issues/8114)) ([d6a1265](https://github.com/aws/aws-cdk/commit/d6a126508e4bb03f6f9d874c2c6648c3e3661a41)), closes [#8113](https://github.com/aws/aws-cdk/issues/8113) + ## [1.44.0](https://github.com/aws/aws-cdk/compare/v1.43.0...v1.44.0) (2020-06-04) diff --git a/lerna.json b/lerna.json index 9e6a0c4ba7b18..95b92c6940487 100644 --- a/lerna.json +++ b/lerna.json @@ -10,5 +10,5 @@ "tools/*" ], "rejectCycles": "true", - "version": "1.44.0" + "version": "1.45.0" }