From c429bb7df2406346426dce22d716cabc484ec7e6 Mon Sep 17 00:00:00 2001 From: Robert Djurasaj Date: Wed, 27 Jan 2021 04:19:39 -0700 Subject: [PATCH 01/33] fix(cloudfront): use node addr for edgeStackId name (#12702) Closes #12323 BREAKING CHANGE: experimental EdgeFunction stack names have changed from 'edge-lambda-stack-${region}' to 'edge-lambda-stack-${stackid}' to support multiple independent CloudFront distributions with EdgeFunctions. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-cloudfront/lib/experimental/edge-function.ts | 2 +- .../aws-cloudfront/test/experimental/edge-function.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts b/packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts index e1d7a5dd9c960..f97b7e176d874 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts @@ -214,7 +214,7 @@ export class EdgeFunction extends Resource implements lambda.IVersion { throw new Error('stacks which use EdgeFunctions must have an explicitly set region'); } - const edgeStackId = stackId ?? `edge-lambda-stack-${region}`; + const edgeStackId = stackId ?? `edge-lambda-stack-${this.stack.node.addr}`; let edgeStack = stage.node.tryFindChild(edgeStackId) as Stack; if (!edgeStack) { edgeStack = new Stack(stage, edgeStackId, { diff --git a/packages/@aws-cdk/aws-cloudfront/test/experimental/edge-function.test.ts b/packages/@aws-cdk/aws-cloudfront/test/experimental/edge-function.test.ts index 372caea5c3fb6..4154f79cabff2 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/experimental/edge-function.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/experimental/edge-function.test.ts @@ -250,10 +250,10 @@ function defaultEdgeFunctionProps(stackId?: string) { code: lambda.Code.fromInline('foo'), handler: 'index.handler', runtime: lambda.Runtime.NODEJS_12_X, - stackId: stackId ?? 'edge-lambda-stack-testregion', + stackId: stackId, }; } -function getFnStack(region: string = 'testregion'): cdk.Stack { - return app.node.findChild(`edge-lambda-stack-${region}`) as cdk.Stack; +function getFnStack(): cdk.Stack { + return app.node.findChild(`edge-lambda-stack-${stack.node.addr}`) as cdk.Stack; } From 59db763e7d05d68fd85b6fd37246d69d4670d7d5 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Wed, 27 Jan 2021 11:56:38 +0000 Subject: [PATCH 02/33] fix(lambda): codeguru profiler not set up for Node runtime (#12712) The CodeGuru profiler is only supported for Java and Python runtimes as part of its lambda integration. Codify this into the CDK and add a validation to fail if this is used with unsupported runtimes. fixes #12624 ---- *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/README.md | 11 +++++- packages/@aws-cdk/aws-lambda/lib/function.ts | 9 +++-- packages/@aws-cdk/aws-lambda/lib/runtime.ts | 38 ++++++++++++++++--- .../@aws-cdk/aws-lambda/test/function.test.ts | 24 +++++++++--- 4 files changed, 66 insertions(+), 16 deletions(-) diff --git a/packages/@aws-cdk/aws-lambda/README.md b/packages/@aws-cdk/aws-lambda/README.md index 5b7ee7cd3240e..e2b317b8faba3 100644 --- a/packages/@aws-cdk/aws-lambda/README.md +++ b/packages/@aws-cdk/aws-lambda/README.md @@ -270,17 +270,24 @@ to learn more about AWS Lambda's X-Ray support. ## Lambda with Profiling +The following code configures the lambda function with CodeGuru profiling. By default, this creates a new CodeGuru +profiling group - + ```ts import * as lambda from '@aws-cdk/aws-lambda'; const fn = new lambda.Function(this, 'MyFunction', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.PYTHON_3_6, handler: 'index.handler', - code: lambda.Code.fromInline('exports.handler = function(event, ctx, cb) { return cb(null, "hi"); }'), + code: lambda.Code.fromAsset('lambda-handler'), profiling: true }); ``` +The `profilingGroup` property can be used to configure an existing CodeGuru profiler group. + +CodeGuru profiling is supported for all Java runtimes and Python3.6+ runtimes. + See [the AWS documentation](https://docs.aws.amazon.com/codeguru/latest/profiler-ug/setting-up-lambda.html) to learn more about AWS Lambda's Profiling support. diff --git a/packages/@aws-cdk/aws-lambda/lib/function.ts b/packages/@aws-cdk/aws-lambda/lib/function.ts index 0e56ee695d4e5..fdcf4b4e0ec24 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function.ts @@ -574,7 +574,7 @@ export class Function extends FunctionBase { let profilingGroupEnvironmentVariables: { [key: string]: string } = {}; if (props.profilingGroup && props.profiling !== false) { - this.validateProfilingEnvironmentVariables(props); + this.validateProfiling(props); props.profilingGroup.grantPublish(this.role); profilingGroupEnvironmentVariables = { AWS_CODEGURU_PROFILER_GROUP_ARN: Stack.of(scope).formatArn({ @@ -585,7 +585,7 @@ export class Function extends FunctionBase { AWS_CODEGURU_PROFILER_ENABLED: 'TRUE', }; } else if (props.profiling) { - this.validateProfilingEnvironmentVariables(props); + this.validateProfiling(props); const profilingGroup = new ProfilingGroup(this, 'ProfilingGroup', { computePlatform: ComputePlatform.AWS_LAMBDA, }); @@ -941,7 +941,10 @@ Environment variables can be marked for removal when used in Lambda@Edge by sett }; } - private validateProfilingEnvironmentVariables(props: FunctionProps) { + private validateProfiling(props: FunctionProps) { + if (!props.runtime.supportsCodeGuruProfiling) { + throw new Error(`CodeGuru profiling is not supported by runtime ${props.runtime.name}`); + } if (props.environment && (props.environment.AWS_CODEGURU_PROFILER_GROUP_ARN || props.environment.AWS_CODEGURU_PROFILER_ENABLED)) { throw new Error('AWS_CODEGURU_PROFILER_GROUP_ARN and AWS_CODEGURU_PROFILER_ENABLED must not be set when profiling options enabled'); } diff --git a/packages/@aws-cdk/aws-lambda/lib/runtime.ts b/packages/@aws-cdk/aws-lambda/lib/runtime.ts index 1930641f45f04..1d98212628588 100644 --- a/packages/@aws-cdk/aws-lambda/lib/runtime.ts +++ b/packages/@aws-cdk/aws-lambda/lib/runtime.ts @@ -12,6 +12,12 @@ export interface LambdaRuntimeProps { * @default - the latest docker image "amazon/aws-sam-cli-build-image-" from https://hub.docker.com/u/amazon */ readonly bundlingDockerImage?: string; + + /** + * Whether this runtime is integrated with and supported for profiling using Amazon CodeGuru Profiler. + * @default false + */ + readonly supportsCodeGuruProfiling?: boolean; } export enum RuntimeFamily { @@ -80,32 +86,46 @@ export class Runtime { /** * The Python 3.6 runtime (python3.6) */ - public static readonly PYTHON_3_6 = new Runtime('python3.6', RuntimeFamily.PYTHON, { supportsInlineCode: true }); + public static readonly PYTHON_3_6 = new Runtime('python3.6', RuntimeFamily.PYTHON, { + supportsInlineCode: true, + supportsCodeGuruProfiling: true, + }); /** * The Python 3.7 runtime (python3.7) */ - public static readonly PYTHON_3_7 = new Runtime('python3.7', RuntimeFamily.PYTHON, { supportsInlineCode: true }); + public static readonly PYTHON_3_7 = new Runtime('python3.7', RuntimeFamily.PYTHON, { + supportsInlineCode: true, + supportsCodeGuruProfiling: true, + }); /** * The Python 3.8 runtime (python3.8) */ - public static readonly PYTHON_3_8 = new Runtime('python3.8', RuntimeFamily.PYTHON); + public static readonly PYTHON_3_8 = new Runtime('python3.8', RuntimeFamily.PYTHON, { + supportsCodeGuruProfiling: true, + }); /** * The Java 8 runtime (java8) */ - public static readonly JAVA_8 = new Runtime('java8', RuntimeFamily.JAVA); + public static readonly JAVA_8 = new Runtime('java8', RuntimeFamily.JAVA, { + supportsCodeGuruProfiling: true, + }); /** * The Java 8 Corretto runtime (java8.al2) */ - public static readonly JAVA_8_CORRETTO = new Runtime('java8.al2', RuntimeFamily.JAVA); + public static readonly JAVA_8_CORRETTO = new Runtime('java8.al2', RuntimeFamily.JAVA, { + supportsCodeGuruProfiling: true, + }); /** * The Java 11 runtime (java11) */ - public static readonly JAVA_11 = new Runtime('java11', RuntimeFamily.JAVA); + public static readonly JAVA_11 = new Runtime('java11', RuntimeFamily.JAVA, { + supportsCodeGuruProfiling: true, + }); /** * The .NET Core 1.0 runtime (dotnetcore1.0) @@ -178,6 +198,11 @@ export class Runtime { */ public readonly supportsInlineCode: boolean; + /** + * Whether this runtime is integrated with and supported for profiling using Amazon CodeGuru Profiler. + */ + public readonly supportsCodeGuruProfiling: boolean; + /** * The runtime family. */ @@ -194,6 +219,7 @@ export class Runtime { this.family = family; const imageName = props.bundlingDockerImage ?? `amazon/aws-sam-cli-build-image-${name}`; this.bundlingDockerImage = BundlingDockerImage.fromRegistry(imageName); + this.supportsCodeGuruProfiling = props.supportsCodeGuruProfiling ?? false; Runtime.ALL.push(this); } diff --git a/packages/@aws-cdk/aws-lambda/test/function.test.ts b/packages/@aws-cdk/aws-lambda/test/function.test.ts index 707b32428912b..51cfe70fd878c 100644 --- a/packages/@aws-cdk/aws-lambda/test/function.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/function.test.ts @@ -1611,7 +1611,7 @@ describe('function', () => { new lambda.Function(stack, 'MyLambda', { code: new lambda.InlineCode('foo'), handler: 'index.handler', - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.PYTHON_3_6, profiling: true, }); @@ -1660,7 +1660,7 @@ describe('function', () => { new lambda.Function(stack, 'MyLambda', { code: new lambda.InlineCode('foo'), handler: 'index.handler', - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.PYTHON_3_6, profilingGroup: new ProfilingGroup(stack, 'ProfilingGroup'), }); @@ -1712,7 +1712,7 @@ describe('function', () => { new lambda.Function(stack, 'MyLambda', { code: new lambda.InlineCode('foo'), handler: 'index.handler', - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.PYTHON_3_6, profiling: false, profilingGroup: new ProfilingGroup(stack, 'ProfilingGroup'), }); @@ -1743,7 +1743,7 @@ describe('function', () => { expect(() => new lambda.Function(stack, 'MyLambda', { code: new lambda.InlineCode('foo'), handler: 'index.handler', - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.PYTHON_3_6, profiling: true, environment: { AWS_CODEGURU_PROFILER_GROUP_ARN: 'profiler_group_arn', @@ -1758,7 +1758,7 @@ describe('function', () => { expect(() => new lambda.Function(stack, 'MyLambda', { code: new lambda.InlineCode('foo'), handler: 'index.handler', - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.PYTHON_3_6, profilingGroup: new ProfilingGroup(stack, 'ProfilingGroup'), environment: { AWS_CODEGURU_PROFILER_GROUP_ARN: 'profiler_group_arn', @@ -1766,6 +1766,20 @@ describe('function', () => { }, })).toThrow(/AWS_CODEGURU_PROFILER_GROUP_ARN and AWS_CODEGURU_PROFILER_ENABLED must not be set when profiling options enabled/); }); + + test('throws an error when used with an unsupported runtime', () => { + const stack = new cdk.Stack(); + expect(() => new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_10_X, + profilingGroup: new ProfilingGroup(stack, 'ProfilingGroup'), + environment: { + AWS_CODEGURU_PROFILER_GROUP_ARN: 'profiler_group_arn', + AWS_CODEGURU_PROFILER_ENABLED: 'yes', + }, + })).toThrow(/not supported by runtime/); + }); }); describe('currentVersion', () => { From 5448388bd0574322a1f5943065d79328fc84b570 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Wed, 27 Jan 2021 12:34:02 +0000 Subject: [PATCH 03/33] chore(s3): bucket.test.ts is using jest (#12709) Move `stack.test.ts` to use native jest APIs instead of `nodeunitshim`. Motivation As part of the work for feature flags in v2, it's easier to inject conditional testing using jest's native APIs than to work with nodeunitshim. Nevertheless, we should be using native jest APIs everywhere. ---- *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/test/bucket.test.ts | 880 +++++++++---------- 1 file changed, 440 insertions(+), 440 deletions(-) diff --git a/packages/@aws-cdk/aws-s3/test/bucket.test.ts b/packages/@aws-cdk/aws-s3/test/bucket.test.ts index 0d4e5b03f8258..adf4858bae8c8 100644 --- a/packages/@aws-cdk/aws-s3/test/bucket.test.ts +++ b/packages/@aws-cdk/aws-s3/test/bucket.test.ts @@ -1,22 +1,22 @@ +import '@aws-cdk/assert/jest'; import { EOL } from 'os'; -import { countResources, expect, haveResource, haveResourceLike, ResourcePart, SynthUtils, arrayWith, objectLike } from '@aws-cdk/assert'; +import { ResourcePart, SynthUtils, arrayWith, objectLike } from '@aws-cdk/assert'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as s3 from '../lib'; // to make it easy to copy & paste from output: /* eslint-disable quote-props */ -nodeunitShim({ - 'default bucket'(test: Test) { +describe('bucket', () => { + test('default bucket', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket'); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -26,29 +26,29 @@ nodeunitShim({ }, }); - test.done(); - }, - 'CFN properties are type-validated during resolution'(test: Test) { + }); + + test('CFN properties are type-validated during resolution', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { bucketName: cdk.Token.asString(5), // Oh no }); - test.throws(() => { + expect(() => { SynthUtils.synthesize(stack); - }, /bucketName: 5 should be a string/); + }).toThrow(/bucketName: 5 should be a string/); + - test.done(); - }, + }); - 'bucket without encryption'(test: Test) { + test('bucket without encryption', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.UNENCRYPTED, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -58,16 +58,16 @@ nodeunitShim({ }, }); - test.done(); - }, - 'bucket with managed encryption'(test: Test) { + }); + + test('bucket with managed encryption', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.KMS_MANAGED, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -87,34 +87,34 @@ nodeunitShim({ }, }, }); - test.done(); - }, - 'valid bucket names'(test: Test) { + }); + + test('valid bucket names', () => { const stack = new cdk.Stack(); - test.doesNotThrow(() => new s3.Bucket(stack, 'MyBucket1', { + expect(() => new s3.Bucket(stack, 'MyBucket1', { bucketName: 'abc.xyz-34ab', - })); + })).not.toThrow(); - test.doesNotThrow(() => new s3.Bucket(stack, 'MyBucket2', { + expect(() => new s3.Bucket(stack, 'MyBucket2', { bucketName: '124.pp--33', - })); + })).not.toThrow(); + - test.done(); - }, + }); - 'bucket validation skips tokenized values'(test: Test) { + test('bucket validation skips tokenized values', () => { const stack = new cdk.Stack(); - test.doesNotThrow(() => new s3.Bucket(stack, 'MyBucket', { + expect(() => new s3.Bucket(stack, 'MyBucket', { bucketName: cdk.Lazy.string({ produce: () => '_BUCKET' }), - })); + })).not.toThrow(); + - test.done(); - }, + }); - 'fails with message on invalid bucket names'(test: Test) { + test('fails with message on invalid bucket names', () => { const stack = new cdk.Stack(); const bucket = `-buckEt.-${new Array(65).join('$')}`; const expectedErrors = [ @@ -126,135 +126,135 @@ nodeunitShim({ 'Bucket name must not have dash next to period, or period next to dash, or consecutive periods (offset: 7)', ].join(EOL); - test.throws(() => new s3.Bucket(stack, 'MyBucket', { + expect(() => new s3.Bucket(stack, 'MyBucket', { bucketName: bucket, - }), expectedErrors); + })).toThrow(expectedErrors); - test.done(); - }, - 'fails if bucket name has less than 3 or more than 63 characters'(test: Test) { + }); + + test('fails if bucket name has less than 3 or more than 63 characters', () => { const stack = new cdk.Stack(); - test.throws(() => new s3.Bucket(stack, 'MyBucket1', { + expect(() => new s3.Bucket(stack, 'MyBucket1', { bucketName: 'a', - }), /at least 3/); + })).toThrow(/at least 3/); - test.throws(() => new s3.Bucket(stack, 'MyBucket2', { + expect(() => new s3.Bucket(stack, 'MyBucket2', { bucketName: new Array(65).join('x'), - }), /no more than 63/); + })).toThrow(/no more than 63/); + - test.done(); - }, + }); - 'fails if bucket name has invalid characters'(test: Test) { + test('fails if bucket name has invalid characters', () => { const stack = new cdk.Stack(); - test.throws(() => new s3.Bucket(stack, 'MyBucket1', { + expect(() => new s3.Bucket(stack, 'MyBucket1', { bucketName: 'b@cket', - }), /offset: 1/); + })).toThrow(/offset: 1/); - test.throws(() => new s3.Bucket(stack, 'MyBucket2', { + expect(() => new s3.Bucket(stack, 'MyBucket2', { bucketName: 'bucKet', - }), /offset: 3/); + })).toThrow(/offset: 3/); - test.throws(() => new s3.Bucket(stack, 'MyBucket3', { + expect(() => new s3.Bucket(stack, 'MyBucket3', { bucketName: 'bučket', - }), /offset: 2/); + })).toThrow(/offset: 2/); + - test.done(); - }, + }); - 'fails if bucket name does not start or end with lowercase character or number'(test: Test) { + test('fails if bucket name does not start or end with lowercase character or number', () => { const stack = new cdk.Stack(); - test.throws(() => new s3.Bucket(stack, 'MyBucket1', { + expect(() => new s3.Bucket(stack, 'MyBucket1', { bucketName: '-ucket', - }), /offset: 0/); + })).toThrow(/offset: 0/); - test.throws(() => new s3.Bucket(stack, 'MyBucket2', { + expect(() => new s3.Bucket(stack, 'MyBucket2', { bucketName: 'bucke.', - }), /offset: 5/); + })).toThrow(/offset: 5/); - test.done(); - }, - 'fails only if bucket name has the consecutive symbols (..), (.-), (-.)'(test: Test) { + }); + + test('fails only if bucket name has the consecutive symbols (..), (.-), (-.)', () => { const stack = new cdk.Stack(); - test.throws(() => new s3.Bucket(stack, 'MyBucket1', { + expect(() => new s3.Bucket(stack, 'MyBucket1', { bucketName: 'buc..ket', - }), /offset: 3/); + })).toThrow(/offset: 3/); - test.throws(() => new s3.Bucket(stack, 'MyBucket2', { + expect(() => new s3.Bucket(stack, 'MyBucket2', { bucketName: 'buck.-et', - }), /offset: 4/); + })).toThrow(/offset: 4/); - test.throws(() => new s3.Bucket(stack, 'MyBucket3', { + expect(() => new s3.Bucket(stack, 'MyBucket3', { bucketName: 'b-.ucket', - }), /offset: 1/); + })).toThrow(/offset: 1/); - test.doesNotThrow(() => new s3.Bucket(stack, 'MyBucket4', { + expect(() => new s3.Bucket(stack, 'MyBucket4', { bucketName: 'bu--cket', - })); + })).not.toThrow(); + - test.done(); - }, + }); - 'fails only if bucket name resembles IP address'(test: Test) { + test('fails only if bucket name resembles IP address', () => { const stack = new cdk.Stack(); - test.throws(() => new s3.Bucket(stack, 'MyBucket1', { + expect(() => new s3.Bucket(stack, 'MyBucket1', { bucketName: '1.2.3.4', - }), /must not resemble an IP address/); + })).toThrow(/must not resemble an IP address/); - test.doesNotThrow(() => new s3.Bucket(stack, 'MyBucket2', { + expect(() => new s3.Bucket(stack, 'MyBucket2', { bucketName: '1.2.3', - })); + })).not.toThrow(); - test.doesNotThrow(() => new s3.Bucket(stack, 'MyBucket3', { + expect(() => new s3.Bucket(stack, 'MyBucket3', { bucketName: '1.2.3.a', - })); + })).not.toThrow(); - test.doesNotThrow(() => new s3.Bucket(stack, 'MyBucket4', { + expect(() => new s3.Bucket(stack, 'MyBucket4', { bucketName: '1000.2.3.4', - })); + })).not.toThrow(); - test.done(); - }, - 'fails if encryption key is used with managed encryption'(test: Test) { + }); + + test('fails if encryption key is used with managed encryption', () => { const stack = new cdk.Stack(); const myKey = new kms.Key(stack, 'MyKey'); - test.throws(() => new s3.Bucket(stack, 'MyBucket', { + expect(() => new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.KMS_MANAGED, encryptionKey: myKey, - }), /encryptionKey is specified, so 'encryption' must be set to KMS/); + })).toThrow(/encryptionKey is specified, so 'encryption' must be set to KMS/); + - test.done(); - }, + }); - 'fails if encryption key is used with encryption set to unencrypted'(test: Test) { + test('fails if encryption key is used with encryption set to unencrypted', () => { const stack = new cdk.Stack(); const myKey = new kms.Key(stack, 'MyKey'); - test.throws(() => new s3.Bucket(stack, 'MyBucket', { + expect(() => new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.UNENCRYPTED, encryptionKey: myKey, - }), /encryptionKey is specified, so 'encryption' must be set to KMS/); + })).toThrow(/encryptionKey is specified, so 'encryption' must be set to KMS/); + - test.done(); - }, + }); - 'encryptionKey can specify kms key'(test: Test) { + test('encryptionKey can specify kms key', () => { const stack = new cdk.Stack(); const encryptionKey = new kms.Key(stack, 'MyKey', { description: 'hello, world' }); new s3.Bucket(stack, 'MyBucket', { encryptionKey, encryption: s3.BucketEncryption.KMS }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyKey6AB29FA6': { 'Type': 'AWS::KMS::Key', @@ -332,15 +332,15 @@ nodeunitShim({ }, }, }); - test.done(); - }, - 'bucketKeyEnabled can be enabled'(test: Test) { + }); + + test('bucketKeyEnabled can be enabled', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { bucketKeyEnabled: true, encryption: s3.BucketEncryption.KMS }); - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { 'BucketEncryption': { 'ServerSideEncryptionConfiguration': [ { @@ -357,30 +357,30 @@ nodeunitShim({ }, ], }, - }), - ); - test.done(); - }, + }); + - 'throws error if bucketKeyEnabled is set, but encryption is not KMS'(test: Test) { + }); + + test('throws error if bucketKeyEnabled is set, but encryption is not KMS', () => { const stack = new cdk.Stack(); - test.throws(() => { + expect(() => { new s3.Bucket(stack, 'MyBucket', { bucketKeyEnabled: true, encryption: s3.BucketEncryption.S3_MANAGED }); - }, "bucketKeyEnabled is specified, so 'encryption' must be set to KMS (value: S3MANAGED)"); - test.throws(() => { + }).toThrow("bucketKeyEnabled is specified, so 'encryption' must be set to KMS (value: S3MANAGED)"); + expect(() => { new s3.Bucket(stack, 'MyBucket3', { bucketKeyEnabled: true }); - }, "bucketKeyEnabled is specified, so 'encryption' must be set to KMS (value: NONE)"); - test.done(); - }, + }).toThrow("bucketKeyEnabled is specified, so 'encryption' must be set to KMS (value: NONE)"); + + }); - 'bucket with versioning turned on'(test: Test) { + test('bucket with versioning turned on', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { versioned: true, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -394,16 +394,16 @@ nodeunitShim({ }, }, }); - test.done(); - }, - 'bucket with block public access set to BlockAll'(test: Test) { + }); + + test('bucket with block public access set to BlockAll', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -420,16 +420,16 @@ nodeunitShim({ }, }, }); - test.done(); - }, - 'bucket with block public access set to BlockAcls'(test: Test) { + }); + + test('bucket with block public access set to BlockAcls', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { blockPublicAccess: s3.BlockPublicAccess.BLOCK_ACLS, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -444,16 +444,16 @@ nodeunitShim({ }, }, }); - test.done(); - }, - 'bucket with custom block public access setting'(test: Test) { + }); + + test('bucket with custom block public access setting', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { blockPublicAccess: new s3.BlockPublicAccess({ restrictPublicBuckets: true }), }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -467,16 +467,16 @@ nodeunitShim({ }, }, }); - test.done(); - }, - 'bucket with custom canned access control'(test: Test) { + }); + + test('bucket with custom canned access control', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { accessControl: s3.BucketAccessControl.LOG_DELIVERY_WRITE, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -488,12 +488,12 @@ nodeunitShim({ }, }, }); - test.done(); - }, - 'permissions': { + }); + + describe('permissions', () => { - 'addPermission creates a bucket policy'(test: Test) { + test('addPermission creates a bucket policy', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.UNENCRYPTED }); @@ -503,7 +503,7 @@ nodeunitShim({ principals: [new iam.AnyPrincipal()], })); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -532,10 +532,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 'forBucket returns a permission statement associated with the bucket\'s ARN'(test: Test) { + }); + + test('forBucket returns a permission statement associated with the bucket\'s ARN', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.UNENCRYPTED }); @@ -546,17 +546,17 @@ nodeunitShim({ principals: [new iam.AnyPrincipal()], }); - test.deepEqual(stack.resolve(x.toStatementJson()), { + expect(stack.resolve(x.toStatementJson())).toEqual({ Action: 's3:ListBucket', Effect: 'Allow', Principal: '*', Resource: { 'Fn::GetAtt': ['MyBucketF68F3FF0', 'Arn'] }, }); - test.done(); - }, - 'arnForObjects returns a permission statement associated with objects in the bucket'(test: Test) { + }); + + test('arnForObjects returns a permission statement associated with objects in the bucket', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.UNENCRYPTED }); @@ -567,7 +567,7 @@ nodeunitShim({ principals: [new iam.AnyPrincipal()], }); - test.deepEqual(stack.resolve(p.toStatementJson()), { + expect(stack.resolve(p.toStatementJson())).toEqual({ Action: 's3:GetObject', Effect: 'Allow', Principal: '*', @@ -579,10 +579,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 'arnForObjects accepts multiple arguments and FnConcats them'(test: Test) { + }); + + test('arnForObjects accepts multiple arguments and FnConcats them', () => { const stack = new cdk.Stack(); @@ -598,7 +598,7 @@ nodeunitShim({ principals: [new iam.AnyPrincipal()], }); - test.deepEqual(stack.resolve(p.toStatementJson()), { + expect(stack.resolve(p.toStatementJson())).toEqual({ Action: 's3:GetObject', Effect: 'Allow', Principal: '*', @@ -617,18 +617,18 @@ nodeunitShim({ }, }); - test.done(); - }, - }, - 'removal policy can be used to specify behavior upon delete'(test: Test) { + }); + }); + + test('removal policy can be used to specify behavior upon delete', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { removalPolicy: cdk.RemovalPolicy.RETAIN, encryption: s3.BucketEncryption.UNENCRYPTED, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ Resources: { MyBucketF68F3FF0: { Type: 'AWS::S3::Bucket', @@ -638,12 +638,12 @@ nodeunitShim({ }, }); - test.done(); - }, - 'import/export': { + }); + + describe('import/export', () => { - 'static import(ref) allows importing an external/existing bucket'(test: Test) { + test('static import(ref) allows importing an external/existing bucket', () => { const stack = new cdk.Stack(); const bucketArn = 'arn:aws:s3:::my-bucket'; @@ -663,21 +663,21 @@ nodeunitShim({ }); // it is possible to obtain a permission statement for a ref - test.deepEqual(p.toStatementJson(), { + expect(p.toStatementJson()).toEqual({ Action: 's3:ListBucket', Effect: 'Allow', Principal: '*', Resource: 'arn:aws:s3:::my-bucket', }); - test.deepEqual(bucket.bucketArn, bucketArn); - test.deepEqual(stack.resolve(bucket.bucketName), 'my-bucket'); + expect(bucket.bucketArn).toEqual(bucketArn); + expect(stack.resolve(bucket.bucketName)).toEqual('my-bucket'); - test.deepEqual(SynthUtils.synthesize(stack).template, {}, 'the ref is not a real resource'); - test.done(); - }, + expect(SynthUtils.synthesize(stack).template).toEqual({}); - 'import does not create any resources'(test: Test) { + }); + + test('import does not create any resources', () => { const stack = new cdk.Stack(); const bucket = s3.Bucket.fromBucketAttributes(stack, 'ImportedBucket', { bucketArn: 'arn:aws:s3:::my-bucket' }); bucket.addToResourcePolicy(new iam.PolicyStatement({ @@ -687,11 +687,11 @@ nodeunitShim({ })); // at this point we technically didn't create any resources in the consuming stack. - expect(stack).toMatch({}); - test.done(); - }, + expect(stack).toMatchTemplate({}); + + }); - 'import can also be used to import arbitrary ARNs'(test: Test) { + test('import can also be used to import arbitrary ARNs', () => { const stack = new cdk.Stack(); const bucket = s3.Bucket.fromBucketAttributes(stack, 'ImportedBucket', { bucketArn: 'arn:aws:s3:::my-bucket' }); bucket.addToResourcePolicy(new iam.PolicyStatement({ resources: ['*'], actions: ['*'] })); @@ -704,7 +704,7 @@ nodeunitShim({ actions: ['s3:*'], })); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyUserDC45028B': { 'Type': 'AWS::IAM::User', @@ -733,10 +733,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 'import can explicitly set bucket region'(test: Test) { + }); + + test('import can explicitly set bucket region', () => { const stack = new cdk.Stack(undefined, undefined, { env: { region: 'us-east-1' }, }); @@ -746,19 +746,19 @@ nodeunitShim({ region: 'eu-west-1', }); - test.equals(bucket.bucketRegionalDomainName, `myBucket.s3.eu-west-1.${stack.urlSuffix}`); - test.equals(bucket.bucketWebsiteDomainName, `myBucket.s3-website-eu-west-1.${stack.urlSuffix}`); + expect(bucket.bucketRegionalDomainName).toEqual(`myBucket.s3.eu-west-1.${stack.urlSuffix}`); + expect(bucket.bucketWebsiteDomainName).toEqual(`myBucket.s3-website-eu-west-1.${stack.urlSuffix}`); - test.done(); - }, - }, - 'grantRead'(test: Test) { + }); + }); + + test('grantRead', () => { const stack = new cdk.Stack(); const reader = new iam.User(stack, 'Reader'); const bucket = new s3.Bucket(stack, 'MyBucket'); bucket.grantRead(reader); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'ReaderF7BF189D': { 'Type': 'AWS::IAM::User', @@ -816,17 +816,17 @@ nodeunitShim({ }, }, }); - test.done(); - }, - 'grantReadWrite': { - 'can be used to grant reciprocal permissions to an identity'(test: Test) { + }); + + describe('grantReadWrite', () => { + test('can be used to grant reciprocal permissions to an identity', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket'); const user = new iam.User(stack, 'MyUser'); bucket.grantReadWrite(user); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -888,10 +888,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 'grant permissions to non-identity principal'(test: Test) { + }); + + test('grant permissions to non-identity principal', () => { // GIVEN const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.KMS }); @@ -900,7 +900,7 @@ nodeunitShim({ bucket.grantRead(new iam.OrganizationPrincipal('o-1234')); // THEN - expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + expect(stack).toHaveResource('AWS::S3::BucketPolicy', { PolicyDocument: { 'Version': '2012-10-17', 'Statement': [ @@ -916,9 +916,9 @@ nodeunitShim({ }, ], }, - })); + }); - expect(stack).to(haveResource('AWS::KMS::Key', { + expect(stack).toHaveResource('AWS::KMS::Key', { 'KeyPolicy': { 'Statement': [ { @@ -946,18 +946,18 @@ nodeunitShim({ 'Version': '2012-10-17', }, - })); + }); + - test.done(); - }, + }); - 'if an encryption key is included, encrypt/decrypt permissions are also added both ways'(test: Test) { + test('if an encryption key is included, encrypt/decrypt permissions are also added both ways', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.KMS }); const user = new iam.User(stack, 'MyUser'); bucket.grantReadWrite(user); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketKeyC17130CF': { 'Type': 'AWS::KMS::Key', @@ -1123,10 +1123,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 'does not grant PutObjectAcl when the S3_GRANT_WRITE_WITHOUT_ACL feature is enabled'(test: Test) { + }); + + test('does not grant PutObjectAcl when the S3_GRANT_WRITE_WITHOUT_ACL feature is enabled', () => { const app = new cdk.App({ context: { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true, @@ -1138,7 +1138,7 @@ nodeunitShim({ bucket.grantReadWrite(user); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -1162,20 +1162,20 @@ nodeunitShim({ }, ], }, - })); + }); + - test.done(); - }, - }, + }); + }); - 'grantWrite': { - 'with KMS key has appropriate permissions for multipart uploads'(test: Test) { + describe('grantWrite', () => { + test('with KMS key has appropriate permissions for multipart uploads', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.KMS }); const user = new iam.User(stack, 'MyUser'); bucket.grantWrite(user); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketKeyC17130CF': { 'Type': 'AWS::KMS::Key', @@ -1336,10 +1336,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 'does not grant PutObjectAcl when the S3_GRANT_WRITE_WITHOUT_ACL feature is enabled'(test: Test) { + }); + + test('does not grant PutObjectAcl when the S3_GRANT_WRITE_WITHOUT_ACL feature is enabled', () => { const app = new cdk.App({ context: { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true, @@ -1351,7 +1351,7 @@ nodeunitShim({ bucket.grantWrite(user); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -1372,14 +1372,14 @@ nodeunitShim({ }, ], }, - })); + }); - test.done(); - }, - }, - 'grantPut': { - 'does not grant PutObjectAcl when the S3_GRANT_WRITE_WITHOUT_ACL feature is enabled'(test: Test) { + }); + }); + + describe('grantPut', () => { + test('does not grant PutObjectAcl when the S3_GRANT_WRITE_WITHOUT_ACL feature is enabled', () => { const app = new cdk.App({ context: { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true, @@ -1391,7 +1391,7 @@ nodeunitShim({ bucket.grantPut(user); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -1408,13 +1408,13 @@ nodeunitShim({ }, ], }, - })); + }); - test.done(); - }, - }, - 'more grants'(test: Test) { + }); + }); + + test('more grants', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.KMS }); const putter = new iam.User(stack, 'Putter'); @@ -1428,13 +1428,13 @@ nodeunitShim({ const resources = SynthUtils.synthesize(stack).template.Resources; const actions = (id: string) => resources[id].Properties.PolicyDocument.Statement[0].Action; - test.deepEqual(actions('WriterDefaultPolicyDC585BCE'), ['s3:DeleteObject*', 's3:PutObject*', 's3:Abort*']); - test.deepEqual(actions('PutterDefaultPolicyAB138DD3'), ['s3:PutObject*', 's3:Abort*']); - test.deepEqual(actions('DeleterDefaultPolicyCD33B8A0'), 's3:DeleteObject*'); - test.done(); - }, + expect(actions('WriterDefaultPolicyDC585BCE')).toEqual(['s3:DeleteObject*', 's3:PutObject*', 's3:Abort*']); + expect(actions('PutterDefaultPolicyAB138DD3')).toEqual(['s3:PutObject*', 's3:Abort*']); + expect(actions('DeleterDefaultPolicyCD33B8A0')).toEqual('s3:DeleteObject*'); - 'grantDelete, with a KMS Key'(test: Test) { + }); + + test('grantDelete, with a KMS Key', () => { // given const stack = new cdk.Stack(); const key = new kms.Key(stack, 'MyKey'); @@ -1449,7 +1449,7 @@ nodeunitShim({ bucket.grantDelete(deleter); // then - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -1473,13 +1473,13 @@ nodeunitShim({ ], 'Version': '2012-10-17', }, - })); + }); + - test.done(); - }, + }); - 'cross-stack permissions': { - 'in the same account and region'(test: Test) { + describe('cross-stack permissions', () => { + test('in the same account and region', () => { const app = new cdk.App(); const stackA = new cdk.Stack(app, 'stackA'); const bucketFromStackA = new s3.Bucket(stackA, 'MyBucket'); @@ -1488,7 +1488,7 @@ nodeunitShim({ const user = new iam.User(stackB, 'UserWhoNeedsAccess'); bucketFromStackA.grantRead(user); - expect(stackA).toMatch({ + expect(stackA).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -1511,7 +1511,7 @@ nodeunitShim({ }, }); - expect(stackB).toMatch({ + expect(stackB).toMatchTemplate({ 'Resources': { 'UserWhoNeedsAccessF8959C3D': { 'Type': 'AWS::IAM::User', @@ -1559,10 +1559,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 'in different accounts'(test: Test) { + }); + + test('in different accounts', () => { // given const stackA = new cdk.Stack(undefined, 'StackA', { env: { account: '123456789012' } }); const bucketFromStackA = new s3.Bucket(stackA, 'MyBucket', { @@ -1579,7 +1579,7 @@ nodeunitShim({ bucketFromStackA.grantRead(roleFromStackB); // then - expect(stackA).to(haveResourceLike('AWS::S3::BucketPolicy', { + expect(stackA).toHaveResourceLike('AWS::S3::BucketPolicy', { 'PolicyDocument': { 'Statement': [ { @@ -1606,9 +1606,9 @@ nodeunitShim({ }, ], }, - })); + }); - expect(stackB).to(haveResourceLike('AWS::IAM::Policy', { + expect(stackB).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -1647,12 +1647,12 @@ nodeunitShim({ }, ], }, - })); + }); - test.done(); - }, - 'in different accounts, with a KMS Key'(test: Test) { + }); + + test('in different accounts, with a KMS Key', () => { // given const stackA = new cdk.Stack(undefined, 'StackA', { env: { account: '123456789012' } }); const key = new kms.Key(stackA, 'MyKey'); @@ -1672,7 +1672,7 @@ nodeunitShim({ bucketFromStackA.grantRead(roleFromStackB); // then - expect(stackA).to(haveResourceLike('AWS::KMS::Key', { + expect(stackA).toHaveResourceLike('AWS::KMS::Key', { 'KeyPolicy': { 'Statement': [ { @@ -1701,9 +1701,9 @@ nodeunitShim({ }, ], }, - })); + }); - expect(stackB).to(haveResourceLike('AWS::IAM::Policy', { + expect(stackB).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -1719,13 +1719,13 @@ nodeunitShim({ }, ], }, - })); + }); + - test.done(); - }, - }, + }); + }); - 'urlForObject returns a token with the S3 URL of the token'(test: Test) { + test('urlForObject returns a token with the S3 URL of the token', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket'); @@ -1733,7 +1733,7 @@ nodeunitShim({ new cdk.CfnOutput(stack, 'MyFileURL', { value: bucket.urlForObject('my/file.txt') }); new cdk.CfnOutput(stack, 'YourFileURL', { value: bucket.urlForObject('/your/file.txt') }); // "/" is optional - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -1810,10 +1810,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 's3UrlForObject returns a token with the S3 URL of the token'(test: Test) { + }); + + test('s3UrlForObject returns a token with the S3 URL of the token', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket'); @@ -1821,7 +1821,7 @@ nodeunitShim({ new cdk.CfnOutput(stack, 'MyFileS3URL', { value: bucket.s3UrlForObject('my/file.txt') }); new cdk.CfnOutput(stack, 'YourFileS3URL', { value: bucket.s3UrlForObject('/your/file.txt') }); // "/" is optional - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -1874,11 +1874,11 @@ nodeunitShim({ }, }); - test.done(); - }, - 'grantPublicAccess': { - 'by default, grants s3:GetObject to all objects'(test: Test) { + }); + + describe('grantPublicAccess', () => { + test('by default, grants s3:GetObject to all objects', () => { // GIVEN const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'b'); @@ -1887,7 +1887,7 @@ nodeunitShim({ bucket.grantPublicAccess(); // THEN - expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + expect(stack).toHaveResource('AWS::S3::BucketPolicy', { 'PolicyDocument': { 'Statement': [ { @@ -1899,11 +1899,11 @@ nodeunitShim({ ], 'Version': '2012-10-17', }, - })); - test.done(); - }, + }); - '"keyPrefix" can be used to only grant access to certain objects'(test: Test) { + }); + + test('"keyPrefix" can be used to only grant access to certain objects', () => { // GIVEN const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'b'); @@ -1912,7 +1912,7 @@ nodeunitShim({ bucket.grantPublicAccess('only/access/these/*'); // THEN - expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + expect(stack).toHaveResource('AWS::S3::BucketPolicy', { 'PolicyDocument': { 'Statement': [ { @@ -1924,11 +1924,11 @@ nodeunitShim({ ], 'Version': '2012-10-17', }, - })); - test.done(); - }, + }); + + }); - '"allowedActions" can be used to specify actions explicitly'(test: Test) { + test('"allowedActions" can be used to specify actions explicitly', () => { // GIVEN const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'b'); @@ -1937,7 +1937,7 @@ nodeunitShim({ bucket.grantPublicAccess('*', 's3:GetObject', 's3:PutObject'); // THEN - expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + expect(stack).toHaveResource('AWS::S3::BucketPolicy', { 'PolicyDocument': { 'Statement': [ { @@ -1949,11 +1949,11 @@ nodeunitShim({ ], 'Version': '2012-10-17', }, - })); - test.done(); - }, + }); - 'returns the PolicyStatement which can be then customized'(test: Test) { + }); + + test('returns the PolicyStatement which can be then customized', () => { // GIVEN const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'b'); @@ -1963,7 +1963,7 @@ nodeunitShim({ result.resourceStatement!.addCondition('IpAddress', { 'aws:SourceIp': '54.240.143.0/24' }); // THEN - expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + expect(stack).toHaveResource('AWS::S3::BucketPolicy', { 'PolicyDocument': { 'Statement': [ { @@ -1978,11 +1978,11 @@ nodeunitShim({ ], 'Version': '2012-10-17', }, - })); - test.done(); - }, + }); + + }); - 'throws when blockPublicPolicy is set to true'(test: Test) { + test('throws when blockPublicPolicy is set to true', () => { // GIVEN const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket', { @@ -1990,62 +1990,62 @@ nodeunitShim({ }); // THEN - test.throws(() => bucket.grantPublicAccess(), /blockPublicPolicy/); + expect(() => bucket.grantPublicAccess()).toThrow(/blockPublicPolicy/); - test.done(); - }, - }, - 'website configuration': { - 'only index doc'(test: Test) { + }); + }); + + describe('website configuration', () => { + test('only index doc', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'Website', { websiteIndexDocument: 'index2.html', }); - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { WebsiteConfiguration: { IndexDocument: 'index2.html', }, - })); - test.done(); - }, - 'fails if only error doc is specified'(test: Test) { + }); + + }); + test('fails if only error doc is specified', () => { const stack = new cdk.Stack(); - test.throws(() => { + expect(() => { new s3.Bucket(stack, 'Website', { websiteErrorDocument: 'error.html', }); - }, /"websiteIndexDocument" is required if "websiteErrorDocument" is set/); - test.done(); - }, - 'error and index docs'(test: Test) { + }).toThrow(/"websiteIndexDocument" is required if "websiteErrorDocument" is set/); + + }); + test('error and index docs', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'Website', { websiteIndexDocument: 'index2.html', websiteErrorDocument: 'error.html', }); - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { WebsiteConfiguration: { IndexDocument: 'index2.html', ErrorDocument: 'error.html', }, - })); - test.done(); - }, - 'exports the WebsiteURL'(test: Test) { + }); + + }); + test('exports the WebsiteURL', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'Website', { websiteIndexDocument: 'index.html', }); - test.deepEqual(stack.resolve(bucket.bucketWebsiteUrl), { 'Fn::GetAtt': ['Website32962D0B', 'WebsiteURL'] }); - test.done(); - }, - 'exports the WebsiteDomain'(test: Test) { + expect(stack.resolve(bucket.bucketWebsiteUrl)).toEqual({ 'Fn::GetAtt': ['Website32962D0B', 'WebsiteURL'] }); + + }); + test('exports the WebsiteDomain', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'Website', { websiteIndexDocument: 'index.html', }); - test.deepEqual(stack.resolve(bucket.bucketWebsiteDomainName), { + expect(stack.resolve(bucket.bucketWebsiteDomainName)).toEqual({ 'Fn::Select': [ 2, { @@ -2053,12 +2053,12 @@ nodeunitShim({ }, ], }); - test.done(); - }, - 'exports the WebsiteURL for imported buckets'(test: Test) { + + }); + test('exports the WebsiteURL for imported buckets', () => { const stack = new cdk.Stack(); const bucket = s3.Bucket.fromBucketName(stack, 'Website', 'my-test-bucket'); - test.deepEqual(stack.resolve(bucket.bucketWebsiteUrl), { + expect(stack.resolve(bucket.bucketWebsiteUrl)).toEqual({ 'Fn::Join': [ '', [ @@ -2069,7 +2069,7 @@ nodeunitShim({ ], ], }); - test.deepEqual(stack.resolve(bucket.bucketWebsiteDomainName), { + expect(stack.resolve(bucket.bucketWebsiteDomainName)).toEqual({ 'Fn::Join': [ '', [ @@ -2080,19 +2080,19 @@ nodeunitShim({ ], ], }); - test.done(); - }, - 'exports the WebsiteURL for imported buckets with url'(test: Test) { + + }); + test('exports the WebsiteURL for imported buckets with url', () => { const stack = new cdk.Stack(); const bucket = s3.Bucket.fromBucketAttributes(stack, 'Website', { bucketName: 'my-test-bucket', bucketWebsiteUrl: 'http://my-test-bucket.my-test.suffix', }); - test.deepEqual(stack.resolve(bucket.bucketWebsiteUrl), 'http://my-test-bucket.my-test.suffix'); - test.deepEqual(stack.resolve(bucket.bucketWebsiteDomainName), 'my-test-bucket.my-test.suffix'); - test.done(); - }, - 'adds RedirectAllRequestsTo property'(test: Test) { + expect(stack.resolve(bucket.bucketWebsiteUrl)).toEqual('http://my-test-bucket.my-test.suffix'); + expect(stack.resolve(bucket.bucketWebsiteDomainName)).toEqual('my-test-bucket.my-test.suffix'); + + }); + test('adds RedirectAllRequestsTo property', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'Website', { websiteRedirect: { @@ -2100,19 +2100,19 @@ nodeunitShim({ protocol: s3.RedirectProtocol.HTTPS, }, }); - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { WebsiteConfiguration: { RedirectAllRequestsTo: { HostName: 'www.example.com', Protocol: 'https', }, }, - })); - test.done(); - }, - 'fails if websiteRedirect and websiteIndex and websiteError are specified'(test: Test) { + }); + + }); + test('fails if websiteRedirect and websiteIndex and websiteError are specified', () => { const stack = new cdk.Stack(); - test.throws(() => { + expect(() => { new s3.Bucket(stack, 'Website', { websiteIndexDocument: 'index.html', websiteErrorDocument: 'error.html', @@ -2120,22 +2120,22 @@ nodeunitShim({ hostName: 'www.example.com', }, }); - }, /"websiteIndexDocument", "websiteErrorDocument" and, "websiteRoutingRules" cannot be set if "websiteRedirect" is used/); - test.done(); - }, - 'fails if websiteRedirect and websiteRoutingRules are specified'(test: Test) { + }).toThrow(/"websiteIndexDocument", "websiteErrorDocument" and, "websiteRoutingRules" cannot be set if "websiteRedirect" is used/); + + }); + test('fails if websiteRedirect and websiteRoutingRules are specified', () => { const stack = new cdk.Stack(); - test.throws(() => { + expect(() => { new s3.Bucket(stack, 'Website', { websiteRoutingRules: [], websiteRedirect: { hostName: 'www.example.com', }, }); - }, /"websiteIndexDocument", "websiteErrorDocument" and, "websiteRoutingRules" cannot be set if "websiteRedirect" is used/); - test.done(); - }, - 'adds RedirectRules property'(test: Test) { + }).toThrow(/"websiteIndexDocument", "websiteErrorDocument" and, "websiteRoutingRules" cannot be set if "websiteRedirect" is used/); + + }); + test('adds RedirectRules property', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'Website', { websiteRoutingRules: [{ @@ -2149,7 +2149,7 @@ nodeunitShim({ }, }], }); - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { WebsiteConfiguration: { RoutingRules: [{ RedirectRule: { @@ -2164,40 +2164,40 @@ nodeunitShim({ }, }], }, - })); - test.done(); - }, - 'fails if routingRule condition object is empty'(test: Test) { + }); + + }); + test('fails if routingRule condition object is empty', () => { const stack = new cdk.Stack(); - test.throws(() => { + expect(() => { new s3.Bucket(stack, 'Website', { websiteRoutingRules: [{ httpRedirectCode: '303', condition: {}, }], }); - }, /The condition property cannot be an empty object/); - test.done(); - }, - 'isWebsite set properly with': { - 'only index doc'(test: Test) { + }).toThrow(/The condition property cannot be an empty object/); + + }); + describe('isWebsite set properly with', () => { + test('only index doc', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'Website', { websiteIndexDocument: 'index2.html', }); - test.equal(bucket.isWebsite, true); - test.done(); - }, - 'error and index docs'(test: Test) { + expect(bucket.isWebsite).toEqual(true); + + }); + test('error and index docs', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'Website', { websiteIndexDocument: 'index2.html', websiteErrorDocument: 'error.html', }); - test.equal(bucket.isWebsite, true); - test.done(); - }, - 'redirects'(test: Test) { + expect(bucket.isWebsite).toEqual(true); + + }); + test('redirects', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'Website', { websiteRedirect: { @@ -2205,36 +2205,36 @@ nodeunitShim({ protocol: s3.RedirectProtocol.HTTPS, }, }); - test.equal(bucket.isWebsite, true); - test.done(); - }, - 'no website properties set'(test: Test) { + expect(bucket.isWebsite).toEqual(true); + + }); + test('no website properties set', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'Website'); - test.equal(bucket.isWebsite, false); - test.done(); - }, - 'imported website buckets'(test: Test) { + expect(bucket.isWebsite).toEqual(false); + + }); + test('imported website buckets', () => { const stack = new cdk.Stack(); const bucket = s3.Bucket.fromBucketAttributes(stack, 'Website', { bucketArn: 'arn:aws:s3:::my-bucket', isWebsite: true, }); - test.equal(bucket.isWebsite, true); - test.done(); - }, - 'imported buckets'(test: Test) { + expect(bucket.isWebsite).toEqual(true); + + }); + test('imported buckets', () => { const stack = new cdk.Stack(); const bucket = s3.Bucket.fromBucketAttributes(stack, 'NotWebsite', { bucketArn: 'arn:aws:s3:::my-bucket', }); - test.equal(bucket.isWebsite, false); - test.done(); - }, - }, - }, + expect(bucket.isWebsite).toEqual(false); - 'Bucket.fromBucketArn'(test: Test) { + }); + }); + }); + + test('Bucket.fromBucketArn', () => { // GIVEN const stack = new cdk.Stack(); @@ -2242,12 +2242,12 @@ nodeunitShim({ const bucket = s3.Bucket.fromBucketArn(stack, 'my-bucket', 'arn:aws:s3:::my_corporate_bucket'); // THEN - test.deepEqual(bucket.bucketName, 'my_corporate_bucket'); - test.deepEqual(bucket.bucketArn, 'arn:aws:s3:::my_corporate_bucket'); - test.done(); - }, + expect(bucket.bucketName).toEqual('my_corporate_bucket'); + expect(bucket.bucketArn).toEqual('arn:aws:s3:::my_corporate_bucket'); + + }); - 'Bucket.fromBucketName'(test: Test) { + test('Bucket.fromBucketName', () => { // GIVEN const stack = new cdk.Stack(); @@ -2255,24 +2255,24 @@ nodeunitShim({ const bucket = s3.Bucket.fromBucketName(stack, 'imported-bucket', 'my-bucket-name'); // THEN - test.deepEqual(bucket.bucketName, 'my-bucket-name'); - test.deepEqual(stack.resolve(bucket.bucketArn), { + expect(bucket.bucketName).toEqual('my-bucket-name'); + expect(stack.resolve(bucket.bucketArn)).toEqual({ 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':s3:::my-bucket-name']], }); - test.done(); - }, - 'if a kms key is specified, it implies bucket is encrypted with kms (dah)'(test: Test) { + }); + + test('if a kms key is specified, it implies bucket is encrypted with kms (dah)', () => { // GIVEN const stack = new cdk.Stack(); const key = new kms.Key(stack, 'k'); // THEN new s3.Bucket(stack, 'b', { encryptionKey: key }); - test.done(); - }, - 'Bucket with Server Access Logs'(test: Test) { + }); + + test('Bucket with Server Access Logs', () => { // GIVEN const stack = new cdk.Stack(); @@ -2283,18 +2283,18 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { LoggingConfiguration: { DestinationBucketName: { Ref: 'AccessLogs8B620ECA', }, }, - })); + }); + - test.done(); - }, + }); - 'Bucket with Server Access Logs with Prefix'(test: Test) { + test('Bucket with Server Access Logs with Prefix', () => { // GIVEN const stack = new cdk.Stack(); @@ -2306,19 +2306,19 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { LoggingConfiguration: { DestinationBucketName: { Ref: 'AccessLogs8B620ECA', }, LogFilePrefix: 'hello', }, - })); + }); + - test.done(); - }, + }); - 'Access log prefix given without bucket'(test: Test) { + test('Access log prefix given without bucket', () => { // GIVEN const stack = new cdk.Stack(); @@ -2327,15 +2327,15 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { LoggingConfiguration: { LogFilePrefix: 'hello', }, - })); - test.done(); - }, + }); - 'Bucket Allow Log delivery changes bucket Access Control should fail'(test: Test) { + }); + + test('Bucket Allow Log delivery changes bucket Access Control should fail', () => { // GIVEN const stack = new cdk.Stack(); @@ -2343,18 +2343,18 @@ nodeunitShim({ const accessLogBucket = new s3.Bucket(stack, 'AccessLogs', { accessControl: s3.BucketAccessControl.AUTHENTICATED_READ, }); - test.throws(() => + expect(() => new s3.Bucket(stack, 'MyBucket', { serverAccessLogsBucket: accessLogBucket, serverAccessLogsPrefix: 'hello', accessControl: s3.BucketAccessControl.AUTHENTICATED_READ, - }) - , /Cannot enable log delivery to this bucket because the bucket's ACL has been set and can't be changed/); + }), + ).toThrow(/Cannot enable log delivery to this bucket because the bucket's ACL has been set and can't be changed/); + - test.done(); - }, + }); - 'Defaults for an inventory bucket'(test: Test) { + test('Defaults for an inventory bucket', () => { // Given const stack = new cdk.Stack(); @@ -2369,7 +2369,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::S3::Bucket', { + expect(stack).toHaveResourceLike('AWS::S3::Bucket', { InventoryConfigurations: [ { Enabled: true, @@ -2382,9 +2382,9 @@ nodeunitShim({ Id: 'MyBucketInventory0', }, ], - })); + }); - expect(stack).to(haveResourceLike('AWS::S3::BucketPolicy', { + expect(stack).toHaveResourceLike('AWS::S3::BucketPolicy', { Bucket: { Ref: 'InventoryBucketA869B8CB' }, PolicyDocument: { Statement: arrayWith(objectLike({ @@ -2400,17 +2400,17 @@ nodeunitShim({ ], })), }, - })); + }); + - test.done(); - }, + }); - 'Bucket with objectOwnership set to BUCKET_OWNER_PREFERRED'(test: Test) { + test('Bucket with objectOwnership set to BUCKET_OWNER_PREFERRED', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { objectOwnership: s3.ObjectOwnership.BUCKET_OWNER_PREFERRED, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -2428,15 +2428,15 @@ nodeunitShim({ }, }, }); - test.done(); - }, - 'Bucket with objectOwnership set to OBJECT_WRITER'(test: Test) { + }); + + test('Bucket with objectOwnership set to OBJECT_WRITER', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { objectOwnership: s3.ObjectOwnership.OBJECT_WRITER, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -2454,15 +2454,15 @@ nodeunitShim({ }, }, }); - test.done(); - }, - 'Bucket with objectOwnerships set to undefined'(test: Test) { + }); + + test('Bucket with objectOwnerships set to undefined', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { objectOwnership: undefined, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -2471,10 +2471,10 @@ nodeunitShim({ }, }, }); - test.done(); - }, - 'with autoDeleteObjects'(test: Test) { + }); + + test('with autoDeleteObjects', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { @@ -2482,12 +2482,12 @@ nodeunitShim({ autoDeleteObjects: true, }); - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { UpdateReplacePolicy: 'Delete', DeletionPolicy: 'Delete', - }, ResourcePart.CompleteDefinition)); + }, ResourcePart.CompleteDefinition); - expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + expect(stack).toHaveResource('AWS::S3::BucketPolicy', { Bucket: { Ref: 'MyBucketF68F3FF0', }, @@ -2535,9 +2535,9 @@ nodeunitShim({ ], 'Version': '2012-10-17', }, - })); + }); - expect(stack).to(haveResource('Custom::S3AutoDeleteObjects', { + expect(stack).toHaveResource('Custom::S3AutoDeleteObjects', { 'Properties': { 'ServiceToken': { 'Fn::GetAtt': [ @@ -2552,12 +2552,12 @@ nodeunitShim({ 'DependsOn': [ 'MyBucketPolicyE7FBAC7B', ], - }, ResourcePart.CompleteDefinition)); + }, ResourcePart.CompleteDefinition); + - test.done(); - }, + }); - 'with autoDeleteObjects on multiple buckets'(test: Test) { + test('with autoDeleteObjects on multiple buckets', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'Bucket1', { @@ -2570,18 +2570,18 @@ nodeunitShim({ autoDeleteObjects: true, }); - expect(stack).to(countResources('AWS::Lambda::Function', 1)); + expect(stack).toCountResources('AWS::Lambda::Function', 1); - test.done(); - }, - 'autoDeleteObjects throws if RemovalPolicy is not DESTROY'(test: Test) { + }); + + test('autoDeleteObjects throws if RemovalPolicy is not DESTROY', () => { const stack = new cdk.Stack(); - test.throws(() => new s3.Bucket(stack, 'MyBucket', { + expect(() => new s3.Bucket(stack, 'MyBucket', { autoDeleteObjects: true, - }), /Cannot use \'autoDeleteObjects\' property on a bucket without setting removal policy to \'DESTROY\'/); + })).toThrow(/Cannot use \'autoDeleteObjects\' property on a bucket without setting removal policy to \'DESTROY\'/); + - test.done(); - }, + }); }); From 1a73d761ad2363842567a1b6e0488ceb093e70b2 Mon Sep 17 00:00:00 2001 From: Jacob Doetsch Date: Wed, 27 Jan 2021 03:11:30 -1000 Subject: [PATCH 04/33] fix(ec2): ARM-backed bastion hosts try to run x86-based Amazon Linux AMI (#12280) Fixes #12279 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-ec2/lib/bastion-host.ts | 33 +- .../@aws-cdk/aws-ec2/lib/instance-types.ts | 37 + .../aws-ec2/test/bastion-host.test.ts | 41 +- .../@aws-cdk/aws-ec2/test/instance.test.ts | 51 +- ...teg.bastion-host-arm-support.expected.json | 659 ++++++++++++++++++ .../test/integ.bastion-host-arm-support.ts | 26 + .../test/integ.bastion-host.expected.json | 2 +- 7 files changed, 837 insertions(+), 12 deletions(-) create mode 100644 packages/@aws-cdk/aws-ec2/test/integ.bastion-host-arm-support.expected.json create mode 100644 packages/@aws-cdk/aws-ec2/test/integ.bastion-host-arm-support.ts diff --git a/packages/@aws-cdk/aws-ec2/lib/bastion-host.ts b/packages/@aws-cdk/aws-ec2/lib/bastion-host.ts index dbc6c45fe415a..0656440ea3ff4 100644 --- a/packages/@aws-cdk/aws-ec2/lib/bastion-host.ts +++ b/packages/@aws-cdk/aws-ec2/lib/bastion-host.ts @@ -1,10 +1,10 @@ import { IPrincipal, IRole, PolicyStatement } from '@aws-cdk/aws-iam'; import { CfnOutput, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { AmazonLinuxGeneration, InstanceClass, InstanceSize, InstanceType } from '.'; +import { AmazonLinuxGeneration, InstanceArchitecture, InstanceClass, InstanceSize, InstanceType } from '.'; import { Connections } from './connections'; import { IInstance, Instance } from './instance'; -import { IMachineImage, MachineImage } from './machine-image'; +import { AmazonLinuxCpuType, IMachineImage, MachineImage } from './machine-image'; import { IPeer } from './peer'; import { Port } from './port'; import { ISecurityGroup } from './security-group'; @@ -60,10 +60,10 @@ export interface BastionHostLinuxProps { readonly instanceType?: InstanceType; /** - * The machine image to use + * The machine image to use, assumed to have SSM Agent preinstalled. * * @default - An Amazon Linux 2 image which is kept up-to-date automatically (the instance - * may be replaced on every deployment). + * may be replaced on every deployment) and already has SSM Agent installed. */ readonly machineImage?: IMachineImage; @@ -146,14 +146,17 @@ export class BastionHostLinux extends Resource implements IInstance { constructor(scope: Construct, id: string, props: BastionHostLinuxProps) { super(scope, id); this.stack = Stack.of(scope); - + const instanceType = props.instanceType ?? InstanceType.of(InstanceClass.T3, InstanceSize.NANO); this.instance = new Instance(this, 'Resource', { vpc: props.vpc, availabilityZone: props.availabilityZone, securityGroup: props.securityGroup, instanceName: props.instanceName ?? 'BastionHost', - instanceType: props.instanceType ?? InstanceType.of(InstanceClass.T3, InstanceSize.NANO), - machineImage: props.machineImage ?? MachineImage.latestAmazonLinux({ generation: AmazonLinuxGeneration.AMAZON_LINUX_2 }), + instanceType, + machineImage: props.machineImage ?? MachineImage.latestAmazonLinux({ + generation: AmazonLinuxGeneration.AMAZON_LINUX_2, + cpuType: this.toAmazonLinuxCpuType(instanceType.architecture), + }), vpcSubnets: props.subnetSelection ?? {}, blockDevices: props.blockDevices ?? undefined, }); @@ -165,8 +168,6 @@ export class BastionHostLinux extends Resource implements IInstance { ], resources: ['*'], })); - this.instance.addUserData('yum install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm'); - this.connections = this.instance.connections; this.role = this.instance.role; this.grantPrincipal = this.instance.role; @@ -183,6 +184,20 @@ export class BastionHostLinux extends Resource implements IInstance { }); } + /** + * Returns the AmazonLinuxCpuType corresponding to the given instance architecture + * @param architecture the instance architecture value to convert + */ + private toAmazonLinuxCpuType(architecture: InstanceArchitecture): AmazonLinuxCpuType { + if (architecture === InstanceArchitecture.ARM_64) { + return AmazonLinuxCpuType.ARM_64; + } else if (architecture === InstanceArchitecture.X86_64) { + return AmazonLinuxCpuType.X86_64; + } + + throw new Error(`Unsupported instance architecture '${architecture}'`); + } + /** * Allow SSH access from the given peer or peers * diff --git a/packages/@aws-cdk/aws-ec2/lib/instance-types.ts b/packages/@aws-cdk/aws-ec2/lib/instance-types.ts index be2a58561b181..c9a7f8ac74509 100644 --- a/packages/@aws-cdk/aws-ec2/lib/instance-types.ts +++ b/packages/@aws-cdk/aws-ec2/lib/instance-types.ts @@ -473,6 +473,21 @@ export enum InstanceClass { INF1 = 'inf1' } +/** + * Identifies an instance's CPU architecture + */ +export enum InstanceArchitecture { + /** + * ARM64 architecture + */ + ARM_64 = 'arm64', + + /** + * x86-64 architecture + */ + X86_64 = 'x86_64', +} + /** * What size of instance to use */ @@ -597,4 +612,26 @@ export class InstanceType { public toString(): string { return this.instanceTypeIdentifier; } + + /** + * The instance's CPU architecture + */ + public get architecture(): InstanceArchitecture { + // capture the family, generation, capabilities, and size portions of the instance type id + const instanceTypeComponents = this.instanceTypeIdentifier.match(/^([a-z]+)(\d{1,2})([a-z]*)\.([a-z0-9]+)$/); + if (instanceTypeComponents == null) { + throw new Error('Malformed instance type identifier'); + } + + const family = instanceTypeComponents[1]; + const capabilities = instanceTypeComponents[3]; + + // Instance family `a` are first-gen Graviton instances + // Capability `g` indicates the instance is Graviton2 powered + if (family === 'a' || capabilities.includes('g')) { + return InstanceArchitecture.ARM_64; + } + + return InstanceArchitecture.X86_64; + } } diff --git a/packages/@aws-cdk/aws-ec2/test/bastion-host.test.ts b/packages/@aws-cdk/aws-ec2/test/bastion-host.test.ts index e950a397707b8..6c547d7859c1a 100644 --- a/packages/@aws-cdk/aws-ec2/test/bastion-host.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/bastion-host.test.ts @@ -1,7 +1,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; import { Stack } from '@aws-cdk/core'; import { nodeunitShim, Test } from 'nodeunit-shim'; -import { BastionHostLinux, BlockDeviceVolume, SubnetType, Vpc } from '../lib'; +import { BastionHostLinux, BlockDeviceVolume, InstanceClass, InstanceSize, InstanceType, SubnetType, Vpc } from '../lib'; nodeunitShim({ 'default instance is created in basic'(test: Test) { @@ -83,6 +83,45 @@ nodeunitShim({ ], })); + test.done(); + }, + 'x86-64 instances use x86-64 image by default'(test: Test) { + // GIVEN + const stack = new Stack(); + const vpc = new Vpc(stack, 'VPC'); + + // WHEN + new BastionHostLinux(stack, 'Bastion', { + vpc, + }); + + // THEN + expect(stack).to(haveResource('AWS::EC2::Instance', { + ImageId: { + Ref: 'SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter', + }, + })); + + test.done(); + }, + 'arm instances use arm image by default'(test: Test) { + // GIVEN + const stack = new Stack(); + const vpc = new Vpc(stack, 'VPC'); + + // WHEN + new BastionHostLinux(stack, 'Bastion', { + vpc, + instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.NANO), + }); + + // THEN + expect(stack).to(haveResource('AWS::EC2::Instance', { + ImageId: { + Ref: 'SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmarm64gp2C96584B6F00A464EAD1953AFF4B05118Parameter', + }, + })); + test.done(); }, }); diff --git a/packages/@aws-cdk/aws-ec2/test/instance.test.ts b/packages/@aws-cdk/aws-ec2/test/instance.test.ts index 6d002e38af568..a2049fb31e86b 100644 --- a/packages/@aws-cdk/aws-ec2/test/instance.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/instance.test.ts @@ -8,7 +8,7 @@ import { Stack } from '@aws-cdk/core'; import { nodeunitShim, Test } from 'nodeunit-shim'; import { AmazonLinuxImage, BlockDeviceVolume, CloudFormationInit, - EbsDeviceVolumeType, InitCommand, Instance, InstanceClass, InstanceSize, InstanceType, UserData, Vpc, + EbsDeviceVolumeType, InitCommand, Instance, InstanceArchitecture, InstanceClass, InstanceSize, InstanceType, UserData, Vpc, } from '../lib'; @@ -107,7 +107,56 @@ nodeunitShim({ test.done(); }, + 'instance architecture is correctly discerned for arm instances'(test: Test) { + // GIVEN + const sampleInstanceClasses = [ + 'a1', 't4g', 'c6g', 'c6gd', 'c6gn', 'm6g', 'm6gd', 'r6g', 'r6gd', // current Graviton-based instance classes + 'a13', 't11g', 'y10ng', 'z11ngd', // theoretical future Graviton-based instance classes + ]; + + for (const instanceClass of sampleInstanceClasses) { + // WHEN + const instanceType = InstanceType.of(instanceClass as InstanceClass, InstanceSize.XLARGE18); + + // THEN + expect(instanceType.architecture).toBe(InstanceArchitecture.ARM_64); + } + + test.done(); + }, + 'instance architecture is correctly discerned for x86-64 instance'(test: Test) { + // GIVEN + const sampleInstanceClasses = ['c5', 'm5ad', 'r5n', 'm6', 't3a']; // A sample of x86-64 instance classes + for (const instanceClass of sampleInstanceClasses) { + // WHEN + const instanceType = InstanceType.of(instanceClass as InstanceClass, InstanceSize.XLARGE18); + + // THEN + expect(instanceType.architecture).toBe(InstanceArchitecture.X86_64); + } + + test.done(); + }, + 'instance architecture throws an error when instance type is invalid'(test: Test) { + // GIVEN + const malformedInstanceTypes = ['t4', 't4g.nano.', 't4gnano', '']; + + for (const malformedInstanceType of malformedInstanceTypes) { + // WHEN + const instanceType = new InstanceType(malformedInstanceType); + + // THEN + try { + instanceType.architecture; + expect(true).toBe(false); // The line above should have thrown an error + } catch (err) { + expect(err.message).toBe('Malformed instance type identifier'); + } + } + + test.done(); + }, blockDeviceMappings: { 'can set blockDeviceMappings'(test: Test) { // WHEN diff --git a/packages/@aws-cdk/aws-ec2/test/integ.bastion-host-arm-support.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.bastion-host-arm-support.expected.json new file mode 100644 index 0000000000000..81f4ae3377d40 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.bastion-host-arm-support.expected.json @@ -0,0 +1,659 @@ +{ + "Resources": { + "VPCB9E5F0B4": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/VPC" + } + ] + } + }, + "VPCPublicSubnet1SubnetB4246D30": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/19", + "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": "TestStack/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/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": "TestStack/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet2Subnet74179F39": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.32.0/19", + "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": "TestStack/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/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": "TestStack/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2NATGateway3C070193": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet3Subnet631C5E25": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "TestStack/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPublicSubnet3RouteTable98AE0E14": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPublicSubnet3RouteTableAssociation427FE0C6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet3RouteTable98AE0E14" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + } + } + }, + "VPCPublicSubnet3DefaultRouteA0D29D46": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet3RouteTable98AE0E14" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet3EIPAD4BC883": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPublicSubnet3NATGatewayD3048F5C": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet3EIPAD4BC883", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPrivateSubnet1Subnet8BCA10E0": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.96.0/19", + "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": "TestStack/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/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.128.0/19", + "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": "TestStack/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/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" + } + } + }, + "VPCPrivateSubnet3Subnet3EDCD457": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.160.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "TestStack/VPC/PrivateSubnet3" + } + ] + } + }, + "VPCPrivateSubnet3RouteTable192186F8": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/VPC/PrivateSubnet3" + } + ] + } + }, + "VPCPrivateSubnet3RouteTableAssociationC28D144E": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet3RouteTable192186F8" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet3Subnet3EDCD457" + } + } + }, + "VPCPrivateSubnet3DefaultRoute27F311AE": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet3RouteTable192186F8" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet3NATGatewayD3048F5C" + } + } + }, + "VPCIGWB7E252D3": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/VPC" + } + ] + } + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "BastionHostInstanceSecurityGroupE75D4274": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "TestStack/BastionHost/Resource/InstanceSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "Tags": [ + { + "Key": "Name", + "Value": "BastionHost" + } + ], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "BastionHostInstanceRoleDD3FA5F1": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "ec2.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "Tags": [ + { + "Key": "Name", + "Value": "BastionHost" + } + ] + } + }, + "BastionHostInstanceRoleDefaultPolicy17347525": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ssmmessages:*", + "ssm:UpdateInstanceInformation", + "ec2messages:*" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "BastionHostInstanceRoleDefaultPolicy17347525", + "Roles": [ + { + "Ref": "BastionHostInstanceRoleDD3FA5F1" + } + ] + } + }, + "BastionHostInstanceProfile770FCA07": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "BastionHostInstanceRoleDD3FA5F1" + } + ] + } + }, + "BastionHost30F9ED05": { + "Type": "AWS::EC2::Instance", + "Properties": { + "AvailabilityZone": "test-region-1a", + "IamInstanceProfile": { + "Ref": "BastionHostInstanceProfile770FCA07" + }, + "ImageId": { + "Ref": "SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmarm64gp2C96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "InstanceType": "t4g.nano", + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "BastionHostInstanceSecurityGroupE75D4274", + "GroupId" + ] + } + ], + "SubnetId": { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + "Tags": [ + { + "Key": "Name", + "Value": "BastionHost" + } + ], + "UserData": { + "Fn::Base64": "#!/bin/bash" + } + }, + "DependsOn": [ + "BastionHostInstanceRoleDefaultPolicy17347525", + "BastionHostInstanceRoleDD3FA5F1" + ] + } + }, + "Outputs": { + "BastionHostBastionHostIdC743CBD6": { + "Description": "Instance ID of the bastion host. Use this to connect via SSM Session Manager", + "Value": { + "Ref": "BastionHost30F9ED05" + } + } + }, + "Parameters": { + "SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmarm64gp2C96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-arm64-gp2" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/integ.bastion-host-arm-support.ts b/packages/@aws-cdk/aws-ec2/test/integ.bastion-host-arm-support.ts new file mode 100644 index 0000000000000..06d6d12557ba9 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.bastion-host-arm-support.ts @@ -0,0 +1,26 @@ +/* + * Stack verification steps: + * * aws ssm start-session --target + * * lscpu # Architecture should be aarch64 + */ +import * as cdk from '@aws-cdk/core'; +import * as ec2 from '../lib'; + +const app = new cdk.App(); + +class TestStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const vpc = new ec2.Vpc(this, 'VPC'); + + new ec2.BastionHostLinux(this, 'BastionHost', { + vpc, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.NANO), + }); + } +} + +new TestStack(app, 'TestStack'); + +app.synth(); diff --git a/packages/@aws-cdk/aws-ec2/test/integ.bastion-host.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.bastion-host.expected.json index 73b52ab630a76..4943873897e75 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.bastion-host.expected.json +++ b/packages/@aws-cdk/aws-ec2/test/integ.bastion-host.expected.json @@ -633,7 +633,7 @@ } ], "UserData": { - "Fn::Base64": "#!/bin/bash\nyum install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm" + "Fn::Base64": "#!/bin/bash" } }, "DependsOn": [ From 1dd3d0518dc2a70c725f87dd5d4377338389125c Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Wed, 27 Jan 2021 14:23:54 +0000 Subject: [PATCH 05/33] feat(elbv2): support for 2020 SSL policy (#12710) Adds the new 'ELBSecurityPolicy-FS-1-2-Res-2020-10' SSL policy. closes #12595 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-elasticloadbalancingv2/lib/shared/enums.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/enums.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/enums.ts index 89f2c88d7ad7e..be25c49b96513 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/enums.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/enums.ts @@ -82,6 +82,12 @@ export enum SslPolicy { */ RECOMMENDED = 'ELBSecurityPolicy-2016-08', + /** + * Strong foward secrecy ciphers and TLV1.2 only (2020 edition). + * Same as FORWARD_SECRECY_TLS12_RES, but only supports GCM versions of the TLS ciphers + */ + FORWARD_SECRECY_TLS12_RES_GCM = 'ELBSecurityPolicy-FS-1-2-Res-2020-10', + /** * Strong forward secrecy ciphers and TLS1.2 only */ From 126a6935cacc1f68b1d1155e484912d4ed6978f2 Mon Sep 17 00:00:00 2001 From: Ayush Goyal Date: Wed, 27 Jan 2021 21:44:07 +0530 Subject: [PATCH 06/33] feat(aws-route53): cross account DNS delegations (#12680) feat(aws-route53): cross account DNS delegations closes #8776 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-route53/.gitignore | 3 +- packages/@aws-cdk/aws-route53/.npmignore | 3 +- packages/@aws-cdk/aws-route53/README.md | 23 ++ packages/@aws-cdk/aws-route53/jest.config.js | 2 + .../index.ts | 66 ++++++ .../@aws-cdk/aws-route53/lib/hosted-zone.ts | 29 +++ .../@aws-cdk/aws-route53/lib/record-set.ts | 64 ++++- packages/@aws-cdk/aws-route53/package.json | 7 +- .../index.test.ts | 119 ++++++++++ ...ovider.ts => hosted-zone-provider.test.ts} | 6 +- .../aws-route53/test/hosted-zone.test.ts | 149 ++++++++++++ ...ross-account-zone-delegation.expected.json | 219 ++++++++++++++++++ .../integ.cross-account-zone-delegation.ts | 23 ++ ...{test.record-set.ts => record-set.test.ts} | 55 ++++- .../test/{test.route53.ts => route53.test.ts} | 6 +- .../aws-route53/test/test.hosted-zone.ts | 64 ----- .../test/{test.util.ts => util.test.ts} | 6 +- .../vpc-endpoint-service-domain-name.test.ts | 10 +- 18 files changed, 770 insertions(+), 84 deletions(-) create mode 100644 packages/@aws-cdk/aws-route53/jest.config.js create mode 100644 packages/@aws-cdk/aws-route53/lib/cross-account-zone-delegation-handler/index.ts create mode 100644 packages/@aws-cdk/aws-route53/test/cross-account-zone-delegation-handler/index.test.ts rename packages/@aws-cdk/aws-route53/test/{test.hosted-zone-provider.ts => hosted-zone-provider.test.ts} (97%) create mode 100644 packages/@aws-cdk/aws-route53/test/hosted-zone.test.ts create mode 100644 packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.expected.json create mode 100644 packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.ts rename packages/@aws-cdk/aws-route53/test/{test.record-set.ts => record-set.test.ts} (88%) rename packages/@aws-cdk/aws-route53/test/{test.route53.ts => route53.test.ts} (98%) delete mode 100644 packages/@aws-cdk/aws-route53/test/test.hosted-zone.ts rename packages/@aws-cdk/aws-route53/test/{test.util.ts => util.test.ts} (97%) diff --git a/packages/@aws-cdk/aws-route53/.gitignore b/packages/@aws-cdk/aws-route53/.gitignore index 86fc837df8fca..a82230b5888d0 100644 --- a/packages/@aws-cdk/aws-route53/.gitignore +++ b/packages/@aws-cdk/aws-route53/.gitignore @@ -15,4 +15,5 @@ nyc.config.js *.snk !.eslintrc.js -junit.xml \ No newline at end of file +junit.xml +!jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53/.npmignore b/packages/@aws-cdk/aws-route53/.npmignore index a94c531529866..9e88226921c33 100644 --- a/packages/@aws-cdk/aws-route53/.npmignore +++ b/packages/@aws-cdk/aws-route53/.npmignore @@ -23,4 +23,5 @@ tsconfig.json # exclude cdk artifacts **/cdk.out junit.xml -test/ \ No newline at end of file +test/ +jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53/README.md b/packages/@aws-cdk/aws-route53/README.md index cc517c5ad6937..49947e4791092 100644 --- a/packages/@aws-cdk/aws-route53/README.md +++ b/packages/@aws-cdk/aws-route53/README.md @@ -109,6 +109,29 @@ Constructs are available for A, AAAA, CAA, CNAME, MX, NS, SRV and TXT records. Use the `CaaAmazonRecord` construct to easily restrict certificate authorities allowed to issue certificates for a domain to Amazon only. +To add a NS record to a HostedZone in different account + +```ts +import * as route53 from '@aws-cdk/aws-route53'; + +// In the account containing the HostedZone +const parentZone = new route53.PublicHostedZone(this, 'HostedZone', { + zoneName: 'someexample.com', + crossAccountZoneDelegationPrinciple: new iam.AccountPrincipal('12345678901') +}); + +// In this account +const subZone = new route53.PublicHostedZone(this, 'SubZone', { + zoneName: 'sub.someexample.com' +}); + +new route53.CrossAccountZoneDelegationRecord(this, 'delegate', { + delegatedZone: subZone, + parentHostedZoneId: parentZone.hostedZoneId, + delegationRole: parentZone.crossAccountDelegationRole +}); +``` + ## Imports If you don't know the ID of the Hosted Zone to import, you can use the diff --git a/packages/@aws-cdk/aws-route53/jest.config.js b/packages/@aws-cdk/aws-route53/jest.config.js new file mode 100644 index 0000000000000..54e28beb9798b --- /dev/null +++ b/packages/@aws-cdk/aws-route53/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-route53/lib/cross-account-zone-delegation-handler/index.ts b/packages/@aws-cdk/aws-route53/lib/cross-account-zone-delegation-handler/index.ts new file mode 100644 index 0000000000000..3c711d283d7e5 --- /dev/null +++ b/packages/@aws-cdk/aws-route53/lib/cross-account-zone-delegation-handler/index.ts @@ -0,0 +1,66 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { Credentials, Route53, STS } from 'aws-sdk'; + +interface ResourceProperties { + AssumeRoleArn: string, + ParentZoneId: string, + DelegatedZoneName: string, + DelegatedZoneNameServers: string[], + TTL: number, +} + +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent) { + const resourceProps = event.ResourceProperties as unknown as ResourceProperties; + + switch (event.RequestType) { + case 'Create': + case 'Update': + return cfnEventHandler(resourceProps, false); + case 'Delete': + return cfnEventHandler(resourceProps, true); + } +} + +async function cfnEventHandler(props: ResourceProperties, isDeleteEvent: boolean) { + const { AssumeRoleArn, ParentZoneId, DelegatedZoneName, DelegatedZoneNameServers, TTL } = props; + + const credentials = await getCrossAccountCredentials(AssumeRoleArn); + const route53 = new Route53({ credentials }); + + await route53.changeResourceRecordSets({ + HostedZoneId: ParentZoneId, + ChangeBatch: { + Changes: [{ + Action: isDeleteEvent ? 'DELETE' : 'UPSERT', + ResourceRecordSet: { + Name: DelegatedZoneName, + Type: 'NS', + TTL, + ResourceRecords: DelegatedZoneNameServers.map(ns => ({ Value: ns })), + }, + }], + }, + }).promise(); +} + +async function getCrossAccountCredentials(roleArn: string): Promise { + const sts = new STS(); + const timestamp = (new Date()).getTime(); + + const { Credentials: assumedCredentials } = await sts + .assumeRole({ + RoleArn: roleArn, + RoleSessionName: `cross-account-zone-delegation-${timestamp}`, + }) + .promise(); + + if (!assumedCredentials) { + throw Error('Error getting assume role credentials'); + } + + return new Credentials({ + accessKeyId: assumedCredentials.AccessKeyId, + secretAccessKey: assumedCredentials.SecretAccessKey, + sessionToken: assumedCredentials.SessionToken, + }); +} diff --git a/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts b/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts index f11e9ae180e7f..1ca835cb258d9 100644 --- a/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts +++ b/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts @@ -1,4 +1,5 @@ import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { ContextProvider, Duration, Lazy, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; @@ -190,6 +191,13 @@ export interface PublicHostedZoneProps extends CommonHostedZoneProps { * @default false */ readonly caaAmazon?: boolean; + + /** + * A principal which is trusted to assume a role for zone delegation + * + * @default - No delegation configuration + */ + readonly crossAccountZoneDelegationPrincipal?: iam.IPrincipal; } /** @@ -222,6 +230,11 @@ export class PublicHostedZone extends HostedZone implements IPublicHostedZone { return new Import(scope, id); } + /** + * Role for cross account zone delegation + */ + public readonly crossAccountZoneDelegationRole?: iam.Role; + constructor(scope: Construct, id: string, props: PublicHostedZoneProps) { super(scope, id, props); @@ -230,6 +243,22 @@ export class PublicHostedZone extends HostedZone implements IPublicHostedZone { zone: this, }); } + + if (props.crossAccountZoneDelegationPrincipal) { + this.crossAccountZoneDelegationRole = new iam.Role(this, 'CrossAccountZoneDelegationRole', { + assumedBy: props.crossAccountZoneDelegationPrincipal, + inlinePolicies: { + delegation: new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + actions: ['route53:ChangeResourceRecordSets'], + resources: [this.hostedZoneArn], + }), + ], + }), + }, + }); + } } public addVpc(_vpc: ec2.IVpc) { diff --git a/packages/@aws-cdk/aws-route53/lib/record-set.ts b/packages/@aws-cdk/aws-route53/lib/record-set.ts index 3111375a8682d..577af5c1a3a57 100644 --- a/packages/@aws-cdk/aws-route53/lib/record-set.ts +++ b/packages/@aws-cdk/aws-route53/lib/record-set.ts @@ -1,10 +1,18 @@ -import { Duration, IResource, Resource, Token } from '@aws-cdk/core'; +import * as path from 'path'; +import * as iam from '@aws-cdk/aws-iam'; +import { CustomResource, CustomResourceProvider, CustomResourceProviderRuntime, Duration, IResource, Resource, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { IAliasRecordTarget } from './alias-record-target'; import { IHostedZone } from './hosted-zone-ref'; import { CfnRecordSet } from './route53.generated'; import { determineFullyQualifiedDomainName } from './util'; +const CROSS_ACCOUNT_ZONE_DELEGATION_RESOURCE_TYPE = 'Custom::CrossAccountZoneDelegation'; + +// v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. +// eslint-disable-next-line +import { Construct as CoreConstruct } from '@aws-cdk/core'; + /** * A record set */ @@ -559,3 +567,57 @@ export class ZoneDelegationRecord extends RecordSet { }); } } + +/** + * Construction properties for a CrossAccountZoneDelegationRecord + */ +export interface CrossAccountZoneDelegationRecordProps { + /** + * The zone to be delegated + */ + readonly delegatedZone: IHostedZone; + + /** + * The hosted zone id in the parent account + */ + readonly parentHostedZoneId: string; + + /** + * The delegation role in the parent account + */ + readonly delegationRole: iam.IRole; + + /** + * The resource record cache time to live (TTL). + * + * @default Duration.days(2) + */ + readonly ttl?: Duration; +} + +/** + * A Cross Account Zone Delegation record + */ +export class CrossAccountZoneDelegationRecord extends CoreConstruct { + constructor(scope: Construct, id: string, props: CrossAccountZoneDelegationRecordProps) { + super(scope, id); + + const serviceToken = CustomResourceProvider.getOrCreate(this, CROSS_ACCOUNT_ZONE_DELEGATION_RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'cross-account-zone-delegation-handler'), + runtime: CustomResourceProviderRuntime.NODEJS_12, + policyStatements: [{ Effect: 'Allow', Action: 'sts:AssumeRole', Resource: props.delegationRole.roleArn }], + }); + + new CustomResource(this, 'CrossAccountZoneDelegationCustomResource', { + resourceType: CROSS_ACCOUNT_ZONE_DELEGATION_RESOURCE_TYPE, + serviceToken, + properties: { + AssumeRoleArn: props.delegationRole.roleArn, + ParentZoneId: props.parentHostedZoneId, + DelegatedZoneName: props.delegatedZone.zoneName, + DelegatedZoneNameServers: props.delegatedZone.hostedZoneNameServers!, + TTL: (props.ttl || Duration.days(2)).toSeconds(), + }, + }); + } +} diff --git a/packages/@aws-cdk/aws-route53/package.json b/packages/@aws-cdk/aws-route53/package.json index 89990b82cc61f..70aa8fc17f4a8 100644 --- a/packages/@aws-cdk/aws-route53/package.json +++ b/packages/@aws-cdk/aws-route53/package.json @@ -55,7 +55,8 @@ "cloudformation": "AWS::Route53", "env": { "AWSLINT_BASE_CONSTRUCT": true - } + }, + "jest": true }, "keywords": [ "aws", @@ -77,11 +78,12 @@ "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "jest": "^26.6.0", - "nodeunit": "^0.11.3", + "nodeunit-shim": "0.0.0", "pkglint": "0.0.0" }, "dependencies": { "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", @@ -91,6 +93,7 @@ "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", diff --git a/packages/@aws-cdk/aws-route53/test/cross-account-zone-delegation-handler/index.test.ts b/packages/@aws-cdk/aws-route53/test/cross-account-zone-delegation-handler/index.test.ts new file mode 100644 index 0000000000000..8c8dcafcbd9c7 --- /dev/null +++ b/packages/@aws-cdk/aws-route53/test/cross-account-zone-delegation-handler/index.test.ts @@ -0,0 +1,119 @@ +import { handler } from '../../lib/cross-account-zone-delegation-handler'; + +const mockStsClient = { + assumeRole: jest.fn().mockReturnThis(), + promise: jest.fn(), +}; +const mockRoute53Client = { + changeResourceRecordSets: jest.fn().mockReturnThis(), + promise: jest.fn(), +}; + +jest.mock('aws-sdk', () => { + return { + ...(jest.requireActual('aws-sdk') as any), + STS: jest.fn(() => mockStsClient), + Route53: jest.fn(() => mockRoute53Client), + }; +}); + +beforeEach(() => { + mockStsClient.assumeRole.mockReturnThis(); + mockRoute53Client.changeResourceRecordSets.mockReturnThis(); +}); + +afterEach(() => { + jest.clearAllMocks(); +}); + +test('throws error if getting credentials fails', async () => { + // GIVEN + mockStsClient.promise.mockResolvedValueOnce({ Credentials: undefined }); + + // WHEN + const event= getCfnEvent(); + + // THEN + await expect(invokeHandler(event)).rejects.toThrow(/Error getting assume role credentials/); + + expect(mockStsClient.assumeRole).toHaveBeenCalledTimes(1); + expect(mockStsClient.assumeRole).toHaveBeenCalledWith({ + RoleArn: 'roleArn', + RoleSessionName: expect.any(String), + }); +}); + +test('calls create resouce record set with Upsert for Create event', async () => { + // GIVEN + mockStsClient.promise.mockResolvedValueOnce({ Credentials: { AccessKeyId: 'K', SecretAccessKey: 'S', SessionToken: 'T' } }); + mockRoute53Client.promise.mockResolvedValueOnce({}); + + // WHEN + const event= getCfnEvent(); + await invokeHandler(event); + + // THEN + expect(mockRoute53Client.changeResourceRecordSets).toHaveBeenCalledTimes(1); + expect(mockRoute53Client.changeResourceRecordSets).toHaveBeenCalledWith({ + HostedZoneId: '1', + ChangeBatch: { + Changes: [{ + Action: 'UPSERT', + ResourceRecordSet: { + Name: 'recordName', + Type: 'NS', + TTL: 172800, + ResourceRecords: [{ Value: 'one' }, { Value: 'two' }], + }, + }], + }, + }); +}); + +test('calls create resouce record set with DELETE for Delete event', async () => { + // GIVEN + mockStsClient.promise.mockResolvedValueOnce({ Credentials: { AccessKeyId: 'K', SecretAccessKey: 'S', SessionToken: 'T' } }); + mockRoute53Client.promise.mockResolvedValueOnce({}); + + // WHEN + const event= getCfnEvent({ RequestType: 'Delete' }); + await invokeHandler(event); + + // THEN + expect(mockRoute53Client.changeResourceRecordSets).toHaveBeenCalledTimes(1); + expect(mockRoute53Client.changeResourceRecordSets).toHaveBeenCalledWith({ + HostedZoneId: '1', + ChangeBatch: { + Changes: [{ + Action: 'DELETE', + ResourceRecordSet: { + Name: 'recordName', + Type: 'NS', + TTL: 172800, + ResourceRecords: [{ Value: 'one' }, { Value: 'two' }], + }, + }], + }, + }); +}); + +function getCfnEvent(event?: Partial): Partial { + return { + RequestType: 'Create', + ResourceProperties: { + ServiceToken: 'Foo', + AssumeRoleArn: 'roleArn', + ParentZoneId: '1', + DelegatedZoneName: 'recordName', + DelegatedZoneNameServers: ['one', 'two'], + TTL: 172800, + }, + ...event, + }; +} + +// helper function to get around TypeScript expecting a complete event object, +// even though our tests only need some of the fields +async function invokeHandler(event: Partial) { + return handler(event as AWSLambda.CloudFormationCustomResourceEvent); +} diff --git a/packages/@aws-cdk/aws-route53/test/test.hosted-zone-provider.ts b/packages/@aws-cdk/aws-route53/test/hosted-zone-provider.test.ts similarity index 97% rename from packages/@aws-cdk/aws-route53/test/test.hosted-zone-provider.ts rename to packages/@aws-cdk/aws-route53/test/hosted-zone-provider.test.ts index 98778fe9d5107..07917bb83ba40 100644 --- a/packages/@aws-cdk/aws-route53/test/test.hosted-zone-provider.ts +++ b/packages/@aws-cdk/aws-route53/test/hosted-zone-provider.test.ts @@ -1,9 +1,9 @@ import { SynthUtils } from '@aws-cdk/assert'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; +import { nodeunitShim, Test } from 'nodeunit-shim'; import { HostedZone } from '../lib'; -export = { +nodeunitShim({ 'Hosted Zone Provider': { 'HostedZoneProvider will return context values if available'(test: Test) { // GIVEN @@ -82,4 +82,4 @@ export = { test.done(); }, }, -}; +}); diff --git a/packages/@aws-cdk/aws-route53/test/hosted-zone.test.ts b/packages/@aws-cdk/aws-route53/test/hosted-zone.test.ts new file mode 100644 index 0000000000000..6e7810824c2bf --- /dev/null +++ b/packages/@aws-cdk/aws-route53/test/hosted-zone.test.ts @@ -0,0 +1,149 @@ +import { expect } from '@aws-cdk/assert'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import { nodeunitShim, Test } from 'nodeunit-shim'; +import { HostedZone, PublicHostedZone } from '../lib'; + +nodeunitShim({ + 'Hosted Zone': { + 'Hosted Zone constructs the ARN'(test: Test) { + // GIVEN + const stack = new cdk.Stack(undefined, 'TestStack', { + env: { account: '123456789012', region: 'us-east-1' }, + }); + + const testZone = new HostedZone(stack, 'HostedZone', { + zoneName: 'testZone', + }); + + test.deepEqual(stack.resolve(testZone.hostedZoneArn), { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':route53:::hostedzone/', + { Ref: 'HostedZoneDB99F866' }, + ], + ], + }); + + test.done(); + }, + }, + + 'Supports tags'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const hostedZone = new HostedZone(stack, 'HostedZone', { + zoneName: 'test.zone', + }); + cdk.Tags.of(hostedZone).add('zoneTag', 'inMyZone'); + + // THEN + expect(stack).toMatch({ + Resources: { + HostedZoneDB99F866: { + Type: 'AWS::Route53::HostedZone', + Properties: { + Name: 'test.zone.', + HostedZoneTags: [ + { + Key: 'zoneTag', + Value: 'inMyZone', + }, + ], + }, + }, + }, + }); + + test.done(); + }, + + 'with crossAccountZoneDelegationPrinciple'(test: Test) { + // GIVEN + const stack = new cdk.Stack(undefined, 'TestStack', { + env: { account: '123456789012', region: 'us-east-1' }, + }); + + // WHEN + new PublicHostedZone(stack, 'HostedZone', { + zoneName: 'testZone', + crossAccountZoneDelegationPrincipal: new iam.AccountPrincipal('223456789012'), + }); + + // THEN + expect(stack).toMatch({ + Resources: { + HostedZoneDB99F866: { + Type: 'AWS::Route53::HostedZone', + Properties: { + Name: 'testZone.', + }, + }, + HostedZoneCrossAccountZoneDelegationRole685DF755: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::223456789012:root', + ], + ], + }, + }, + }, + ], + Version: '2012-10-17', + }, + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: 'route53:ChangeResourceRecordSets', + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':route53:::hostedzone/', + { + Ref: 'HostedZoneDB99F866', + }, + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'delegation', + }, + ], + }, + }, + }, + }); + + test.done(); + }, +}); diff --git a/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.expected.json b/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.expected.json new file mode 100644 index 0000000000000..919a54f8b5051 --- /dev/null +++ b/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.expected.json @@ -0,0 +1,219 @@ +{ + "Resources": { + "ParentHostedZoneC2BD86E1": { + "Type": "AWS::Route53::HostedZone", + "Properties": { + "Name": "myzone.com." + } + }, + "ParentHostedZoneCrossAccountZoneDelegationRole95B1C36E": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "route53:ChangeResourceRecordSets", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":route53:::hostedzone/", + { + "Ref": "ParentHostedZoneC2BD86E1" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "delegation" + } + ] + } + }, + "ChildHostedZone4B14AC71": { + "Type": "AWS::Route53::HostedZone", + "Properties": { + "Name": "sub.myzone.com." + } + }, + "DelegationCrossAccountZoneDelegationCustomResourceFADC27F0": { + "Type": "Custom::CrossAccountZoneDelegation", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomCrossAccountZoneDelegationCustomResourceProviderHandler44A84265", + "Arn" + ] + }, + "AssumeRoleArn": { + "Fn::GetAtt": [ + "ParentHostedZoneCrossAccountZoneDelegationRole95B1C36E", + "Arn" + ] + }, + "ParentZoneId": { + "Ref": "ParentHostedZoneC2BD86E1" + }, + "DelegatedZoneName": "sub.myzone.com", + "DelegatedZoneNameServers": { + "Fn::GetAtt": [ + "ChildHostedZone4B14AC71", + "NameServers" + ] + }, + "TTL": 172800 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CustomCrossAccountZoneDelegationCustomResourceProviderRoleED64687B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ], + "Policies": [ + { + "PolicyName": "Inline", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Resource": { + "Fn::GetAtt": [ + "ParentHostedZoneCrossAccountZoneDelegationRole95B1C36E", + "Arn" + ] + } + } + ] + } + } + ] + } + }, + "CustomCrossAccountZoneDelegationCustomResourceProviderHandler44A84265": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113aS3Bucket8B462894" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113aS3VersionKeyFDEC5E1D" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113aS3VersionKeyFDEC5E1D" + } + ] + } + ] + } + ] + ] + } + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "__entrypoint__.handler", + "Role": { + "Fn::GetAtt": [ + "CustomCrossAccountZoneDelegationCustomResourceProviderRoleED64687B", + "Arn" + ] + }, + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "CustomCrossAccountZoneDelegationCustomResourceProviderRoleED64687B" + ] + } + }, + "Parameters": { + "AssetParameters3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113aS3Bucket8B462894": { + "Type": "String", + "Description": "S3 bucket for asset \"3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113a\"" + }, + "AssetParameters3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113aS3VersionKeyFDEC5E1D": { + "Type": "String", + "Description": "S3 key for asset version \"3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113a\"" + }, + "AssetParameters3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113aArtifactHash4F367D8C": { + "Type": "String", + "Description": "Artifact hash for asset \"3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113a\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.ts b/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.ts new file mode 100644 index 0000000000000..75f9e86152eb0 --- /dev/null +++ b/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.ts @@ -0,0 +1,23 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import { PublicHostedZone, CrossAccountZoneDelegationRecord } from '../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-route53-cross-account-integ'); + +const parentZone = new PublicHostedZone(stack, 'ParentHostedZone', { + zoneName: 'myzone.com', + crossAccountZoneDelegationPrincipal: new iam.AccountPrincipal(cdk.Aws.ACCOUNT_ID), +}); + +const childZone = new PublicHostedZone(stack, 'ChildHostedZone', { + zoneName: 'sub.myzone.com', +}); +new CrossAccountZoneDelegationRecord(stack, 'Delegation', { + delegatedZone: childZone, + parentHostedZoneId: parentZone.hostedZoneId, + delegationRole: parentZone.crossAccountZoneDelegationRole!, +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-route53/test/test.record-set.ts b/packages/@aws-cdk/aws-route53/test/record-set.test.ts similarity index 88% rename from packages/@aws-cdk/aws-route53/test/test.record-set.ts rename to packages/@aws-cdk/aws-route53/test/record-set.test.ts index 8da38f60d9720..373464f455992 100644 --- a/packages/@aws-cdk/aws-route53/test/test.record-set.ts +++ b/packages/@aws-cdk/aws-route53/test/record-set.test.ts @@ -1,9 +1,10 @@ import { expect, haveResource } from '@aws-cdk/assert'; +import * as iam from '@aws-cdk/aws-iam'; import { Duration, Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; +import { nodeunitShim, Test } from 'nodeunit-shim'; import * as route53 from '../lib'; -export = { +nodeunitShim({ 'with default ttl'(test: Test) { // GIVEN const stack = new Stack(); @@ -513,4 +514,52 @@ export = { })); test.done(); }, -}; + + 'Cross account zone delegation record'(test: Test) { + // GIVEN + const stack = new Stack(); + const parentZone = new route53.PublicHostedZone(stack, 'ParentHostedZone', { + zoneName: 'myzone.com', + crossAccountZoneDelegationPrincipal: new iam.AccountPrincipal('123456789012'), + }); + + // WHEN + const childZone = new route53.PublicHostedZone(stack, 'ChildHostedZone', { + zoneName: 'sub.myzone.com', + }); + new route53.CrossAccountZoneDelegationRecord(stack, 'Delegation', { + delegatedZone: childZone, + parentHostedZoneId: parentZone.hostedZoneId, + delegationRole: parentZone.crossAccountZoneDelegationRole!, + ttl: Duration.seconds(60), + }); + + // THEN + expect(stack).to(haveResource('Custom::CrossAccountZoneDelegation', { + ServiceToken: { + 'Fn::GetAtt': [ + 'CustomCrossAccountZoneDelegationCustomResourceProviderHandler44A84265', + 'Arn', + ], + }, + AssumeRoleArn: { + 'Fn::GetAtt': [ + 'ParentHostedZoneCrossAccountZoneDelegationRole95B1C36E', + 'Arn', + ], + }, + ParentZoneId: { + Ref: 'ParentHostedZoneC2BD86E1', + }, + DelegatedZoneName: 'sub.myzone.com', + DelegatedZoneNameServers: { + 'Fn::GetAtt': [ + 'ChildHostedZone4B14AC71', + 'NameServers', + ], + }, + TTL: 60, + })); + test.done(); + }, +}); diff --git a/packages/@aws-cdk/aws-route53/test/test.route53.ts b/packages/@aws-cdk/aws-route53/test/route53.test.ts similarity index 98% rename from packages/@aws-cdk/aws-route53/test/test.route53.ts rename to packages/@aws-cdk/aws-route53/test/route53.test.ts index 4655e1c10fda8..8f58486bebcbc 100644 --- a/packages/@aws-cdk/aws-route53/test/test.route53.ts +++ b/packages/@aws-cdk/aws-route53/test/route53.test.ts @@ -1,10 +1,10 @@ import { beASupersetOfTemplate, exactlyMatchTemplate, expect, haveResource } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; +import { nodeunitShim, Test } from 'nodeunit-shim'; import { HostedZone, PrivateHostedZone, PublicHostedZone, TxtRecord } from '../lib'; -export = { +nodeunitShim({ 'default properties': { 'public hosted zone'(test: Test) { const app = new TestApp(); @@ -215,7 +215,7 @@ export = { })); test.done(); }, -}; +}); class TestApp { public readonly stack: cdk.Stack; diff --git a/packages/@aws-cdk/aws-route53/test/test.hosted-zone.ts b/packages/@aws-cdk/aws-route53/test/test.hosted-zone.ts deleted file mode 100644 index 37d80a5908a9b..0000000000000 --- a/packages/@aws-cdk/aws-route53/test/test.hosted-zone.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { expect } from '@aws-cdk/assert'; -import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import { HostedZone } from '../lib'; - -export = { - 'Hosted Zone': { - 'Hosted Zone constructs the ARN'(test: Test) { - // GIVEN - const stack = new cdk.Stack(undefined, 'TestStack', { - env: { account: '123456789012', region: 'us-east-1' }, - }); - - const testZone = new HostedZone(stack, 'HostedZone', { - zoneName: 'testZone', - }); - - test.deepEqual(stack.resolve(testZone.hostedZoneArn), { - 'Fn::Join': [ - '', - [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':route53:::hostedzone/', - { Ref: 'HostedZoneDB99F866' }, - ], - ], - }); - - test.done(); - }, - }, - - 'Supports tags'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const hostedZone = new HostedZone(stack, 'HostedZone', { - zoneName: 'test.zone', - }); - cdk.Tags.of(hostedZone).add('zoneTag', 'inMyZone'); - - // THEN - expect(stack).toMatch({ - Resources: { - HostedZoneDB99F866: { - Type: 'AWS::Route53::HostedZone', - Properties: { - Name: 'test.zone.', - HostedZoneTags: [ - { - Key: 'zoneTag', - Value: 'inMyZone', - }, - ], - }, - }, - }, - }); - - test.done(); - }, -}; diff --git a/packages/@aws-cdk/aws-route53/test/test.util.ts b/packages/@aws-cdk/aws-route53/test/util.test.ts similarity index 97% rename from packages/@aws-cdk/aws-route53/test/test.util.ts rename to packages/@aws-cdk/aws-route53/test/util.test.ts index d589b058e40cc..c6ded4e74f7b4 100644 --- a/packages/@aws-cdk/aws-route53/test/test.util.ts +++ b/packages/@aws-cdk/aws-route53/test/util.test.ts @@ -1,9 +1,9 @@ import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; +import { nodeunitShim, Test } from 'nodeunit-shim'; import { HostedZone } from '../lib'; import * as util from '../lib/util'; -export = { +nodeunitShim({ 'throws when zone name ending with a \'.\''(test: Test) { test.throws(() => util.validateZoneName('zone.name.'), /trailing dot/); test.done(); @@ -78,4 +78,4 @@ export = { test.equal(qualified, 'test.domain.com.'); test.done(); }, -}; +}); diff --git a/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts b/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts index 70ba07c201d5f..86edd9992776d 100644 --- a/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts +++ b/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable jest/no-disabled-tests */ import { expect as cdkExpect, haveResource, haveResourceLike, ResourcePart } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import { IVpcEndpointServiceLoadBalancer, VpcEndpointService } from '@aws-cdk/aws-ec2'; @@ -56,7 +57,7 @@ test('create domain name resource', () => { }, }, physicalResourceId: { - id: 'EndpointDomain', + id: 'VPCES', }, }, Update: { @@ -69,7 +70,7 @@ test('create domain name resource', () => { }, }, physicalResourceId: { - id: 'EndpointDomain', + id: 'VPCES', }, }, Delete: { @@ -236,6 +237,9 @@ test('create domain name resource', () => { test('throws if creating multiple domains for a single service', () => { // GIVEN + vpces = new VpcEndpointService(stack, 'VPCES-2', { + vpcEndpointServiceLoadBalancers: [nlb], + }); new VpcEndpointServiceDomainName(stack, 'EndpointDomain', { endpointService: vpces, @@ -250,5 +254,5 @@ test('throws if creating multiple domains for a single service', () => { domainName: 'my-stuff-2.aws-cdk.dev', publicHostedZone: zone, }); - }).toThrow(); + }).toThrow(/Cannot create a VpcEndpointServiceDomainName for service/); }); \ No newline at end of file From 2f6521a1d8670b2653f7dee281309351181cf918 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 27 Jan 2021 19:34:54 +0100 Subject: [PATCH 07/33] fix(iam): cannot use the same Role for multiple Config Rules (#12724) This, or any other construct that adds Managed Policies to a Role by default; Managed Policy ARNs need to be deduplicated, otherwise CloudFormation will throw an error upon creation. Slightly more complicated than you'd expect in order to deal with Tokens. Fixes #12714. ---- *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-iam/lib/role.ts | 6 +-- packages/@aws-cdk/aws-iam/lib/util.ts | 44 ++++++++++++++++++++- packages/@aws-cdk/aws-iam/test/role.test.ts | 28 +++++++++++++ 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-iam/lib/role.ts b/packages/@aws-cdk/aws-iam/lib/role.ts index 74e251bd7bc07..9b81e4f174152 100644 --- a/packages/@aws-cdk/aws-iam/lib/role.ts +++ b/packages/@aws-cdk/aws-iam/lib/role.ts @@ -1,4 +1,4 @@ -import { Duration, Lazy, Resource, Stack, Token, TokenComparison } from '@aws-cdk/core'; +import { Duration, Resource, Stack, Token, TokenComparison } from '@aws-cdk/core'; import { Construct, Node } from 'constructs'; import { Grant } from './grant'; import { CfnRole } from './iam.generated'; @@ -9,7 +9,7 @@ import { PolicyDocument } from './policy-document'; import { PolicyStatement } from './policy-statement'; import { AddToPrincipalPolicyResult, ArnPrincipal, IPrincipal, PrincipalPolicyFragment } from './principals'; import { ImmutableRole } from './private/immutable-role'; -import { AttachedPolicies } from './util'; +import { AttachedPolicies, UniqueStringSet } from './util'; /** * Properties for defining an IAM Role @@ -326,7 +326,7 @@ export class Role extends Resource implements IRole { const role = new CfnRole(this, 'Resource', { assumeRolePolicyDocument: this.assumeRolePolicy as any, - managedPolicyArns: Lazy.list({ produce: () => this.managedPolicies.map(p => p.managedPolicyArn) }, { omitEmpty: true }), + managedPolicyArns: UniqueStringSet.from(() => this.managedPolicies.map(p => p.managedPolicyArn)), policies: _flatten(this.inlinePolicies), path: props.path, permissionsBoundary: this.permissionsBoundary ? this.permissionsBoundary.managedPolicyArn : undefined, diff --git a/packages/@aws-cdk/aws-iam/lib/util.ts b/packages/@aws-cdk/aws-iam/lib/util.ts index b5f1700baefe7..19fcbffe09639 100644 --- a/packages/@aws-cdk/aws-iam/lib/util.ts +++ b/packages/@aws-cdk/aws-iam/lib/util.ts @@ -1,4 +1,4 @@ -import { DefaultTokenResolver, Lazy, StringConcat, Tokenization } from '@aws-cdk/core'; +import { captureStackTrace, DefaultTokenResolver, IPostProcessor, IResolvable, IResolveContext, Lazy, StringConcat, Token, Tokenization } from '@aws-cdk/core'; import { IConstruct } from 'constructs'; import { IPolicy } from './policy'; @@ -82,3 +82,45 @@ export function mergePrincipal(target: { [key: string]: string[] }, source: { [k return target; } + +/** + * Lazy string set token that dedupes entries + * + * Needs to operate post-resolve, because the inputs could be + * `[ '${Token[TOKEN.9]}', '${Token[TOKEN.10]}', '${Token[TOKEN.20]}' ]`, which + * still all resolve to the same string value. + * + * Needs to JSON.stringify() results because strings could resolve to literal + * strings but could also resolve to `{ Fn::Join: [...] }`. + */ +export class UniqueStringSet implements IResolvable, IPostProcessor { + public static from(fn: () => string[]) { + return Token.asList(new UniqueStringSet(fn)); + } + + public readonly creationStack: string[]; + + private constructor(private readonly fn: () => string[]) { + this.creationStack = captureStackTrace(); + } + + public resolve(context: IResolveContext) { + context.registerPostProcessor(this); + return this.fn(); + } + + public postProcess(input: any, _context: IResolveContext) { + if (!Array.isArray(input)) { return input; } + if (input.length === 0) { return undefined; } + + const uniq: Record = {}; + for (const el of input) { + uniq[JSON.stringify(el)] = el; + } + return Object.values(uniq); + } + + public toString(): string { + return Token.asString(this); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/test/role.test.ts b/packages/@aws-cdk/aws-iam/test/role.test.ts index 510416f580f39..8f0415c45598d 100644 --- a/packages/@aws-cdk/aws-iam/test/role.test.ts +++ b/packages/@aws-cdk/aws-iam/test/role.test.ts @@ -535,3 +535,31 @@ describe('IAM role', () => { expect(() => app.synth()).toThrow(/A PolicyStatement used in a resource-based policy must specify at least one IAM principal/); }); }); + +test('managed policy ARNs are deduplicated', () => { + const app = new App(); + const stack = new Stack(app, 'my-stack'); + const role = new Role(stack, 'MyRole', { + assumedBy: new ServicePrincipal('sns.amazonaws.com'), + managedPolicies: [ + ManagedPolicy.fromAwsManagedPolicyName('SuperDeveloper'), + ManagedPolicy.fromAwsManagedPolicyName('SuperDeveloper'), + ], + }); + role.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('SuperDeveloper')); + + expect(stack).toHaveResource('AWS::IAM::Role', { + ManagedPolicyArns: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/SuperDeveloper', + ], + ], + }, + ], + }); +}); \ No newline at end of file From dfc765af44c755f10be8f6c1c2eae55f62e2aa08 Mon Sep 17 00:00:00 2001 From: Dominic Fezzie Date: Thu, 28 Jan 2021 10:14:01 -0800 Subject: [PATCH 08/33] feat(appmesh): change VirtualService provider to a union-like class (#11978) Fixes #9490 BREAKING CHANGE: the properties virtualRouter and virtualNode of VirtualServiceProps have been replaced with the union-like class VirtualServiceProvider * **appmesh**: the method `addVirtualService` has been removed from `IMesh` ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/extensions/appmesh.ts | 3 +- packages/@aws-cdk/aws-appmesh/README.md | 14 +- packages/@aws-cdk/aws-appmesh/lib/mesh.ts | 16 -- .../aws-appmesh/lib/virtual-service.ts | 157 +++++++++++------- .../aws-appmesh/test/integ.mesh.expected.json | 83 ++++----- .../@aws-cdk/aws-appmesh/test/integ.mesh.ts | 24 +-- .../aws-appmesh/test/test.gateway-route.ts | 6 +- .../@aws-cdk/aws-appmesh/test/test.mesh.ts | 119 +------------ .../aws-appmesh/test/test.virtual-gateway.ts | 10 +- .../aws-appmesh/test/test.virtual-node.ts | 6 +- .../aws-appmesh/test/test.virtual-router.ts | 8 +- .../aws-appmesh/test/test.virtual-service.ts | 86 ++++++++++ 12 files changed, 252 insertions(+), 280 deletions(-) diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/appmesh.ts b/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/appmesh.ts index fb93375423798..35436d11ce691 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/appmesh.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/appmesh.ts @@ -316,8 +316,7 @@ export class AppMeshExtension extends ServiceExtension { // Now create a virtual service. Relationship goes like this: // virtual service -> virtual router -> virtual node this.virtualService = new appmesh.VirtualService(this.scope, `${this.parentService.id}-virtual-service`, { - mesh: this.mesh, - virtualRouter: this.virtualRouter, + virtualServiceProvider: appmesh.VirtualServiceProvider.virtualRouter(this.virtualRouter), virtualServiceName: serviceName, }); } diff --git a/packages/@aws-cdk/aws-appmesh/README.md b/packages/@aws-cdk/aws-appmesh/README.md index 2253b12d2987d..c400cbb0af05d 100644 --- a/packages/@aws-cdk/aws-appmesh/README.md +++ b/packages/@aws-cdk/aws-appmesh/README.md @@ -109,23 +109,21 @@ When creating a virtual service: Adding a virtual router as the provider: ```ts -mesh.addVirtualService('virtual-service', { - virtualRouter: router, - virtualServiceName: 'my-service.default.svc.cluster.local', +new appmesh.VirtualService('virtual-service', { + virtualServiceName: 'my-service.default.svc.cluster.local', // optional + virtualServiceProvider: appmesh.VirtualServiceProvider.virtualRouter(router), }); ``` Adding a virtual node as the provider: ```ts -mesh.addVirtualService('virtual-service', { - virtualNode: node, - virtualServiceName: `my-service.default.svc.cluster.local`, +new appmesh.VirtualService('virtual-service', { + virtualServiceName: `my-service.default.svc.cluster.local`, // optional + virtualServiceProvider: appmesh.VirtualServiceProvider.virtualNode(node), }); ``` -**Note** that only one must of `virtualNode` or `virtualRouter` must be chosen. - ## Adding a VirtualNode A `virtual node` acts as a logical pointer to a particular task group, such as an Amazon ECS service or a Kubernetes deployment. diff --git a/packages/@aws-cdk/aws-appmesh/lib/mesh.ts b/packages/@aws-cdk/aws-appmesh/lib/mesh.ts index cc0596695aae2..a1cd5b49904a5 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/mesh.ts @@ -4,7 +4,6 @@ import { CfnMesh } from './appmesh.generated'; import { VirtualGateway, VirtualGatewayBaseProps } from './virtual-gateway'; import { VirtualNode, VirtualNodeBaseProps } from './virtual-node'; import { VirtualRouter, VirtualRouterBaseProps } from './virtual-router'; -import { VirtualService, VirtualServiceBaseProps } from './virtual-service'; /** * A utility enum defined for the egressFilter type property, the default of DROP_ALL, @@ -46,11 +45,6 @@ export interface IMesh extends cdk.IResource { */ addVirtualRouter(id: string, props?: VirtualRouterBaseProps): VirtualRouter; - /** - * Adds a VirtualService with the given id - */ - addVirtualService(id: string, props?: VirtualServiceBaseProps): VirtualService; - /** * Adds a VirtualNode to the Mesh */ @@ -86,16 +80,6 @@ abstract class MeshBase extends cdk.Resource implements IMesh { }); } - /** - * Adds a VirtualService with the given id - */ - public addVirtualService(id: string, props: VirtualServiceBaseProps = {}): VirtualService { - return new VirtualService(this, id, { - ...props, - mesh: this, - }); - } - /** * Adds a VirtualNode to the Mesh */ diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts index 9ca5d5010b7f4..5685b8b08c1f8 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts @@ -36,9 +36,9 @@ export interface IVirtualService extends cdk.IResource { } /** - * The base properties which all classes in VirtualService will inherit from + * The properties applied to the VirtualService being defined */ -export interface VirtualServiceBaseProps { +export interface VirtualServiceProps { /** * The name of the VirtualService. * @@ -50,36 +50,17 @@ export interface VirtualServiceBaseProps { */ readonly virtualServiceName?: string; - /** - * The VirtualRouter which the VirtualService uses as provider - * - * @default - At most one of virtualRouter and virtualNode is allowed. - */ - readonly virtualRouter?: IVirtualRouter; - - /** - * The VirtualNode attached to the virtual service - * - * @default - At most one of virtualRouter and virtualNode is allowed. - */ - readonly virtualNode?: IVirtualNode; - /** * Client policy for this Virtual Service * * @default - none */ readonly clientPolicy?: ClientPolicy; -} -/** - * The properties applied to the VirtualService being define - */ -export interface VirtualServiceProps extends VirtualServiceBaseProps { /** - * The Mesh which the VirtualService belongs to + * The VirtualNode or VirtualRouter which the VirtualService uses as its provider */ - readonly mesh: IMesh; + readonly virtualServiceProvider: VirtualServiceProvider; } /** @@ -135,59 +116,35 @@ export class VirtualService extends cdk.Resource implements IVirtualService { public readonly clientPolicy?: ClientPolicy; - private readonly virtualServiceProvider?: CfnVirtualService.VirtualServiceProviderProperty; - constructor(scope: Construct, id: string, props: VirtualServiceProps) { super(scope, id, { physicalName: props.virtualServiceName || cdk.Lazy.string({ produce: () => cdk.Names.uniqueId(this) }), }); - if (props.virtualNode && props.virtualRouter) { - throw new Error('Must provide only one of virtualNode or virtualRouter for the provider'); - } - - this.mesh = props.mesh; this.clientPolicy = props.clientPolicy; - - // Check which provider to use node or router (or neither) - if (props.virtualRouter) { - this.virtualServiceProvider = this.addVirtualRouter(props.virtualRouter.virtualRouterName); - } - if (props.virtualNode) { - this.virtualServiceProvider = this.addVirtualNode(props.virtualNode.virtualNodeName); - } + const providerConfig = props.virtualServiceProvider.bind(this); + this.mesh = providerConfig.mesh; const svc = new CfnVirtualService(this, 'Resource', { meshName: this.mesh.meshName, virtualServiceName: this.physicalName, spec: { - provider: this.virtualServiceProvider, + provider: providerConfig.virtualNodeProvider || providerConfig.virtualRouterProvider + ? { + virtualNode: providerConfig.virtualNodeProvider, + virtualRouter: providerConfig.virtualRouterProvider, + } + : undefined, }, }); this.virtualServiceName = this.getResourceNameAttribute(svc.attrVirtualServiceName); this.virtualServiceArn = this.getResourceArnAttribute(svc.ref, { service: 'appmesh', - resource: `mesh/${props.mesh.meshName}/virtualService`, + resource: `mesh/${this.mesh.meshName}/virtualService`, resourceName: this.physicalName, }); } - - private addVirtualRouter(name: string): CfnVirtualService.VirtualServiceProviderProperty { - return { - virtualRouter: { - virtualRouterName: name, - }, - }; - } - - private addVirtualNode(name: string): CfnVirtualService.VirtualServiceProviderProperty { - return { - virtualNode: { - virtualNodeName: name, - }, - }; - } } /** @@ -211,3 +168,91 @@ export interface VirtualServiceAttributes { */ readonly clientPolicy?: ClientPolicy; } + +/** + * Properties for a VirtualService provider + */ +export interface VirtualServiceProviderConfig { + /** + * Virtual Node based provider + * + * @default - none + */ + readonly virtualNodeProvider?: CfnVirtualService.VirtualNodeServiceProviderProperty; + + /** + * Virtual Router based provider + * + * @default - none + */ + readonly virtualRouterProvider?: CfnVirtualService.VirtualRouterServiceProviderProperty; + + /** + * Mesh the Provider is using + * + * @default - none + */ + readonly mesh: IMesh; +} + +/** + * Represents the properties needed to define the provider for a VirtualService + */ +export abstract class VirtualServiceProvider { + /** + * Returns a VirtualNode based Provider for a VirtualService + */ + public static virtualNode(virtualNode: IVirtualNode): VirtualServiceProvider { + return new VirtualServiceProviderImpl(virtualNode, undefined); + } + + /** + * Returns a VirtualRouter based Provider for a VirtualService + */ + public static virtualRouter(virtualRouter: IVirtualRouter): VirtualServiceProvider { + return new VirtualServiceProviderImpl(undefined, virtualRouter); + } + + /** + * Returns an Empty Provider for a VirtualService. This provides no routing capabilities + * and should only be used as a placeholder + */ + public static none(mesh: IMesh): VirtualServiceProvider { + return new VirtualServiceProviderImpl(undefined, undefined, mesh); + } + + /** + * Enforces mutual exclusivity for VirtualService provider types. + */ + public abstract bind(_construct: Construct): VirtualServiceProviderConfig; +} + +class VirtualServiceProviderImpl extends VirtualServiceProvider { + private readonly virtualNode?: IVirtualNode; + private readonly virtualRouter?: IVirtualRouter; + private readonly mesh: IMesh; + + constructor(virtualNode?: IVirtualNode, virtualRouter?: IVirtualRouter, mesh?: IMesh) { + super(); + this.virtualNode = virtualNode; + this.virtualRouter = virtualRouter; + const providedMesh = this.virtualNode?.mesh ?? this.virtualRouter?.mesh ?? mesh!; + this.mesh = providedMesh; + } + + public bind(_construct: Construct): VirtualServiceProviderConfig { + return { + mesh: this.mesh, + virtualNodeProvider: this.virtualNode + ? { + virtualNodeName: this.virtualNode.virtualNodeName, + } + : undefined, + virtualRouterProvider: this.virtualRouter + ? { + virtualRouterName: this.virtualRouter.virtualRouterName, + } + : undefined, + }; + } +} diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json index db9277d071432..5f4a9ca206725 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json @@ -625,41 +625,6 @@ } } }, - "meshserviceE06ECED5": { - "Type": "AWS::AppMesh::VirtualService", - "Properties": { - "MeshName": { - "Fn::GetAtt": [ - "meshACDFE68E", - "MeshName" - ] - }, - "Spec": { - "Provider": { - "VirtualRouter": { - "VirtualRouterName": { - "Fn::GetAtt": [ - "meshrouter81B8087E", - "VirtualRouterName" - ] - } - } - } - }, - "VirtualServiceName": "service1.domain.local" - } - }, - "cert56CA94EB": { - "Type": "AWS::CertificateManager::Certificate", - "Properties": { - "DomainName":"node1.domain.local", - "DomainValidationOptions": [{ - "DomainName":"node1.domain.local", - "ValidationDomain":"local" - }], - "ValidationMethod": "EMAIL" - } - }, "meshnode726C787D": { "Type": "AWS::AppMesh::VirtualNode", "Properties": { @@ -675,7 +640,7 @@ "VirtualService": { "VirtualServiceName": { "Fn::GetAtt": [ - "meshserviceE06ECED5", + "service6D174F83", "VirtualServiceName" ] } @@ -706,16 +671,6 @@ "PortMapping": { "Port": 8080, "Protocol": "http" - }, - "TLS": { - "Certificate": { - "ACM": { - "CertificateArn": { - "Ref": "cert56CA94EB" - } - } - }, - "Mode": "STRICT" } } ], @@ -743,8 +698,8 @@ "TLS": { "Validation": { "Trust": { - "ACM": { - "CertificateAuthorityArns": ["arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012"] + "File": { + "CertificateChain": "path/to/cert" } } } @@ -891,7 +846,7 @@ "VirtualService": { "VirtualServiceName": { "Fn::GetAtt": [ - "meshserviceE06ECED5", + "service6D174F83", "VirtualServiceName" ] } @@ -928,7 +883,7 @@ "VirtualService": { "VirtualServiceName": { "Fn::GetAtt": [ - "meshserviceE06ECED5", + "service6D174F83", "VirtualServiceName" ] } @@ -965,7 +920,7 @@ "VirtualService": { "VirtualServiceName": { "Fn::GetAtt": [ - "meshserviceE06ECED5", + "service6D174F83", "VirtualServiceName" ] } @@ -975,7 +930,7 @@ "Match": { "ServiceName": { "Fn::GetAtt": [ - "meshserviceE06ECED5", + "service6D174F83", "VirtualServiceName" ] } @@ -990,6 +945,30 @@ } } }, + "service6D174F83": { + "Type": "AWS::AppMesh::VirtualService", + "Properties": { + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "Provider": { + "VirtualRouter": { + "VirtualRouterName": { + "Fn::GetAtt": [ + "meshrouter81B8087E", + "VirtualRouterName" + ] + } + } + } + }, + "VirtualServiceName": "service1.domain.local" + } + }, "service27C65CF7D": { "Type": "AWS::AppMesh::VirtualService", "Properties": { diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts index 730418aef7970..90e54586f7f51 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts @@ -1,5 +1,3 @@ -import * as acmpca from '@aws-cdk/aws-acmpca'; -import * as acm from '@aws-cdk/aws-certificatemanager'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import * as cdk from '@aws-cdk/core'; @@ -25,15 +23,11 @@ const router = mesh.addVirtualRouter('router', { ], }); -const virtualService = mesh.addVirtualService('service', { - virtualRouter: router, +const virtualService = new appmesh.VirtualService(stack, 'service', { + virtualServiceProvider: appmesh.VirtualServiceProvider.virtualRouter(router), virtualServiceName: 'service1.domain.local', }); -const cert = new acm.Certificate(stack, 'cert', { - domainName: `node1.${namespace.namespaceName}`, -}); - const node = mesh.addVirtualNode('node', { serviceDiscovery: appmesh.ServiceDiscovery.dns(`node1.${namespace.namespaceName}`), listeners: [appmesh.VirtualNodeListener.http({ @@ -41,10 +35,6 @@ const node = mesh.addVirtualNode('node', { healthyThreshold: 3, path: '/check-path', }, - tlsCertificate: appmesh.TlsCertificate.acm({ - certificate: cert, - tlsMode: appmesh.TlsMode.STRICT, - }), })], backends: [ virtualService, @@ -53,7 +43,7 @@ const node = mesh.addVirtualNode('node', { node.addBackend(new appmesh.VirtualService(stack, 'service-2', { virtualServiceName: 'service2.domain.local', - mesh, + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), }), ); @@ -75,8 +65,6 @@ router.addRoute('route-1', { }), }); -const certificateAuthorityArn = 'arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012'; - const node2 = mesh.addVirtualNode('node2', { serviceDiscovery: appmesh.ServiceDiscovery.dns(`node2.${namespace.namespaceName}`), listeners: [appmesh.VirtualNodeListener.http({ @@ -90,13 +78,13 @@ const node2 = mesh.addVirtualNode('node2', { unhealthyThreshold: 2, }, })], - backendsDefaultClientPolicy: appmesh.ClientPolicy.acmTrust({ - certificateAuthorities: [acmpca.CertificateAuthority.fromCertificateAuthorityArn(stack, 'certificate', certificateAuthorityArn)], + backendsDefaultClientPolicy: appmesh.ClientPolicy.fileTrust({ + certificateChain: 'path/to/cert', }), backends: [ new appmesh.VirtualService(stack, 'service-3', { virtualServiceName: 'service3.domain.local', - mesh, + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), }), ], }); diff --git a/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts b/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts index a741ec0b0d1d8..fed290d36a3e2 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts @@ -21,7 +21,7 @@ export = { }); const virtualService = new appmesh.VirtualService(stack, 'vs-1', { - mesh: mesh, + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), virtualServiceName: 'target.local', }); @@ -121,7 +121,9 @@ export = { meshName: 'test-mesh', }); - const virtualService = mesh.addVirtualService('testVirtualService'); + const virtualService = new appmesh.VirtualService(stack, 'testVirtualService', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + }); test.throws(() => appmesh.GatewayRouteSpec.http({ routeTarget: virtualService, match: { diff --git a/packages/@aws-cdk/aws-appmesh/test/test.mesh.ts b/packages/@aws-cdk/aws-appmesh/test/test.mesh.ts index c6d2fbfbbb294..ce50c1402a7c3 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.mesh.ts @@ -125,120 +125,6 @@ export = { test.done(); }, - 'When adding a VirtualService to a mesh': { - 'with VirtualRouter and VirtualNode as providers': { - 'should throw error'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const mesh = new appmesh.Mesh(stack, 'mesh', { - meshName: 'test-mesh', - }); - - const testNode = new appmesh.VirtualNode(stack, 'test-node', { - mesh, - serviceDiscovery: appmesh.ServiceDiscovery.dns('test-node'), - }); - - const testRouter = mesh.addVirtualRouter('router', { - listeners: [ - appmesh.VirtualRouterListener.http(), - ], - }); - - // THEN - test.throws(() => { - mesh.addVirtualService('service', { - virtualServiceName: 'test-service.domain.local', - virtualNode: testNode, - virtualRouter: testRouter, - }); - }); - - test.done(); - }, - }, - 'with single virtual router provider resource': { - 'should create service'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const mesh = new appmesh.Mesh(stack, 'mesh', { - meshName: 'test-mesh', - }); - - const testRouter = mesh.addVirtualRouter('test-router', { - listeners: [ - appmesh.VirtualRouterListener.http(), - ], - }); - - mesh.addVirtualService('service', { - virtualServiceName: 'test-service.domain.local', - virtualRouter: testRouter, - }); - - // THEN - expect(stack).to( - haveResource('AWS::AppMesh::VirtualService', { - Spec: { - Provider: { - VirtualRouter: { - VirtualRouterName: { - 'Fn::GetAtt': ['meshtestrouterF78D72DD', 'VirtualRouterName'], - }, - }, - }, - }, - }), - ); - - test.done(); - }, - }, - 'with single virtual node provider resource': { - 'should create service'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const mesh = new appmesh.Mesh(stack, 'mesh', { - meshName: 'test-mesh', - }); - - const node = mesh.addVirtualNode('test-node', { - serviceDiscovery: appmesh.ServiceDiscovery.dns('test.domain.local'), - listeners: [appmesh.VirtualNodeListener.http({ - port: 8080, - })], - }); - - mesh.addVirtualService('service2', { - virtualServiceName: 'test-service.domain.local', - virtualNode: node, - }); - - // THEN - expect(stack).to( - haveResource('AWS::AppMesh::VirtualService', { - Spec: { - Provider: { - VirtualNode: { - VirtualNodeName: { - 'Fn::GetAtt': ['meshtestnodeF93946D4', 'VirtualNodeName'], - }, - }, - }, - }, - }), - ); - - test.done(); - }, - }, - }, 'When adding a VirtualNode to a mesh': { 'with empty default listeners and backends': { 'should create default resource'(test: Test) { @@ -376,7 +262,7 @@ export = { const service1 = new appmesh.VirtualService(stack, 'service-1', { virtualServiceName: 'service1.domain.local', - mesh, + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), }); mesh.addVirtualNode('test-node', { @@ -415,8 +301,9 @@ export = { const stack2 = new cdk.Stack(); const mesh2 = appmesh.Mesh.fromMeshName(stack2, 'imported-mesh', 'abc'); - mesh2.addVirtualService('service', { + new appmesh.VirtualService(stack2, 'service', { virtualServiceName: 'test.domain.local', + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh2), }); // THEN diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts index 7b4a563c90d34..b9d3ed70cae43 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts @@ -306,7 +306,9 @@ export = { mesh: mesh, }); - const virtualService = mesh.addVirtualService('virtualService', {}); + const virtualService = new appmesh.VirtualService(stack, 'virtualService', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + }); virtualGateway.addGatewayRoute('testGatewayRoute', { gatewayRouteName: 'test-gateway-route', @@ -324,7 +326,7 @@ export = { Target: { VirtualService: { VirtualServiceName: { - 'Fn::GetAtt': ['meshvirtualService93460D43', 'VirtualServiceName'], + 'Fn::GetAtt': ['virtualService03A04B87', 'VirtualServiceName'], }, }, }, @@ -349,7 +351,9 @@ export = { meshName: 'test-mesh', }); - const virtualService = mesh.addVirtualService('virtualService', {}); + const virtualService = new appmesh.VirtualService(stack, 'virtualService', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + }); const virtualGateway = mesh.addVirtualGateway('gateway'); virtualGateway.addGatewayRoute('testGatewayRoute', { diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts index 9fb05931a2a44..4337973230854 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts @@ -19,11 +19,11 @@ export = { const service1 = new appmesh.VirtualService(stack, 'service-1', { virtualServiceName: 'service1.domain.local', - mesh, + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), }); const service2 = new appmesh.VirtualService(stack, 'service-2', { virtualServiceName: 'service2.domain.local', - mesh, + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), }); const node = new appmesh.VirtualNode(stack, 'test-node', { @@ -319,7 +319,7 @@ export = { const service1 = new appmesh.VirtualService(stack, 'service-1', { virtualServiceName: 'service1.domain.local', - mesh, + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), clientPolicy: appmesh.ClientPolicy.fileTrust({ certificateChain: 'path-to-certificate', ports: [8080, 8081], diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts index aafe3dff8ce7f..2732adb4cba17 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts @@ -101,7 +101,7 @@ export = { const service1 = new appmesh.VirtualService(stack, 'service-1', { virtualServiceName: 'service1.domain.local', - mesh, + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), }); const node = mesh.addVirtualNode('test-node', { @@ -170,11 +170,11 @@ export = { const service1 = new appmesh.VirtualService(stack, 'service-1', { virtualServiceName: 'service1.domain.local', - mesh, + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), }); const service2 = new appmesh.VirtualService(stack, 'service-2', { virtualServiceName: 'service2.domain.local', - mesh, + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), }); const node = mesh.addVirtualNode('test-node', { @@ -332,7 +332,7 @@ export = { const service1 = new appmesh.VirtualService(stack, 'service-1', { virtualServiceName: 'service1.domain.local', - mesh, + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), }); const node = mesh.addVirtualNode('test-node', { diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-service.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-service.ts index c09c156ac75ea..c60c98f8e7b94 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-service.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-service.ts @@ -1,3 +1,4 @@ +import { expect, haveResource } from '@aws-cdk/assert'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; @@ -19,6 +20,7 @@ export = { test.done(); }, + 'Can import Virtual Services using attributes'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -34,6 +36,90 @@ export = { // THEN test.equal(virtualService.mesh.meshName, meshName); test.equal(virtualService.virtualServiceName, virtualServiceName); + test.done(); }, + + 'When adding a VirtualService to a mesh': { + 'with single virtual router provider resource': { + 'should create service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const testRouter = mesh.addVirtualRouter('test-router', { + listeners: [ + appmesh.VirtualRouterListener.http(), + ], + }); + + new appmesh.VirtualService(stack, 'service', { + virtualServiceName: 'test-service.domain.local', + virtualServiceProvider: appmesh.VirtualServiceProvider.virtualRouter(testRouter), + }); + + // THEN + expect(stack).to( + haveResource('AWS::AppMesh::VirtualService', { + Spec: { + Provider: { + VirtualRouter: { + VirtualRouterName: { + 'Fn::GetAtt': ['meshtestrouterF78D72DD', 'VirtualRouterName'], + }, + }, + }, + }, + }), + ); + + test.done(); + }, + }, + + 'with single virtual node provider resource': { + 'should create service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const node = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test.domain.local'), + listeners: [appmesh.VirtualNodeListener.http({ + port: 8080, + })], + }); + + new appmesh.VirtualService(stack, 'service2', { + virtualServiceName: 'test-service.domain.local', + virtualServiceProvider: appmesh.VirtualServiceProvider.virtualNode(node), + }); + + // THEN + expect(stack).to( + haveResource('AWS::AppMesh::VirtualService', { + Spec: { + Provider: { + VirtualNode: { + VirtualNodeName: { + 'Fn::GetAtt': ['meshtestnodeF93946D4', 'VirtualNodeName'], + }, + }, + }, + }, + }), + ); + + test.done(); + }, + }, + }, }; From ffe7e425e605144a465cea9befa68d4fe19f9d8c Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Thu, 28 Jan 2021 18:50:19 +0000 Subject: [PATCH 09/33] fix(apigateway): stack update fails to replace api key (#12745) This reverts commit 96cbe32d2399d82a2ad6c3bf6dc1fd65396882d4. The above commit changed the logical id layout of API keys. It turns out that ApiKey resource types cannot be replaced without explicitly specifying, and changing, the API key name. See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-apikey.html#cfn-apigateway-apikey-name fixes #12698 ---- *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/usage-plan.ts | 4 ++- .../test/integ.restapi.expected.json | 2 +- .../integ.usage-plan.multikey.expected.json | 2 +- .../aws-apigateway/test/usage-plan.test.ts | 28 ------------------- 4 files changed, 5 insertions(+), 31 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts b/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts index 6a1c5a5091bda..ad807d4a7d2d0 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts @@ -179,8 +179,10 @@ export class UsagePlan extends Resource { * @param apiKey */ public addApiKey(apiKey: IApiKey): void { + const prefix = 'UsagePlanKeyResource'; + // Postfixing apikey id only from the 2nd child, to keep physicalIds of UsagePlanKey for existing CDK apps unmodifed. - const id = `UsagePlanKeyResource:${Names.nodeUniqueId(apiKey.node)}`; + const id = this.node.tryFindChild(prefix) ? `${prefix}:${Names.nodeUniqueId(apiKey.node)}` : prefix; new CfnUsagePlanKey(this, id, { keyId: apiKey.keyId, 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 a0fb6357db3c7..91af3471593eb 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json @@ -602,7 +602,7 @@ "UsagePlanName": "Basic" } }, - "myapiUsagePlanUsagePlanKeyResourcetestapigatewayrestapimyapiApiKeyC43601CB600D112D": { + "myapiUsagePlanUsagePlanKeyResource050D133F": { "Type": "AWS::ApiGateway::UsagePlanKey", "Properties": { "KeyId": { diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.usage-plan.multikey.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.usage-plan.multikey.expected.json index 9dee2e7aa07b0..8e761f40e2a26 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.usage-plan.multikey.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.usage-plan.multikey.expected.json @@ -3,7 +3,7 @@ "myusageplan4B391740": { "Type": "AWS::ApiGateway::UsagePlan" }, - "myusageplanUsagePlanKeyResourcetestapigatewayusageplanmultikeymyapikey1DDABC389A2809A73": { + "myusageplanUsagePlanKeyResource095B4EA9": { "Type": "AWS::ApiGateway::UsagePlanKey", "Properties": { "KeyId": { diff --git a/packages/@aws-cdk/aws-apigateway/test/usage-plan.test.ts b/packages/@aws-cdk/aws-apigateway/test/usage-plan.test.ts index 854c0a65a6562..f183d08796388 100644 --- a/packages/@aws-cdk/aws-apigateway/test/usage-plan.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/usage-plan.test.ts @@ -205,32 +205,4 @@ describe('usage plan', () => { }, }, ResourcePart.Properties); }); - - test('UsagePlanKeys have unique logical ids', () => { - // GIVEN - const app = new cdk.App(); - const stack = new cdk.Stack(app, 'my-stack'); - const usagePlan = new apigateway.UsagePlan(stack, 'my-usage-plan'); - const apiKey1 = new apigateway.ApiKey(stack, 'my-api-key-1', { - apiKeyName: 'my-api-key-1', - }); - const apiKey2 = new apigateway.ApiKey(stack, 'my-api-key-2', { - apiKeyName: 'my-api-key-2', - }); - - // WHEN - usagePlan.addApiKey(apiKey1); - usagePlan.addApiKey(apiKey2); - - // THEN - const template = app.synth().getStackByName(stack.stackName).template; - const logicalIds = Object.entries(template.Resources) - .filter(([_, v]) => (v as any).Type === 'AWS::ApiGateway::UsagePlanKey') - .map(([k, _]) => k); - - expect(logicalIds).toEqual([ - 'myusageplanUsagePlanKeyResourcemystackmyapikey1EE9AA1B359121274', - 'myusageplanUsagePlanKeyResourcemystackmyapikey2B4E8EB1456DC88E9', - ]); - }); }); From 238742e4323310ce850d8edc70abe4b0e9f53186 Mon Sep 17 00:00:00 2001 From: Matthew Bonig Date: Thu, 28 Jan 2021 12:26:38 -0700 Subject: [PATCH 10/33] fix(codedeploy): wrong syntax on Windows 'installAgent' flag (#12736) Resolves #12734 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/server/deployment-group.ts | 3 +- .../test/server/test.deployment-group.ts | 72 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-group.ts index eb4bbafade216..0864d603d2e28 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-group.ts @@ -372,7 +372,8 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { asg.addUserData( 'Set-Variable -Name TEMPDIR -Value (New-TemporaryFile).DirectoryName', `aws s3 cp s3://aws-codedeploy-${cdk.Stack.of(this).region}/latest/codedeploy-agent.msi $TEMPDIR\\codedeploy-agent.msi`, - '$TEMPDIR\\codedeploy-agent.msi /quiet /l c:\\temp\\host-agent-install-log.txt', + 'cd $TEMPDIR', + '.\\codedeploy-agent.msi /quiet /l c:\\temp\\host-agent-install-log.txt', ); break; } diff --git a/packages/@aws-cdk/aws-codedeploy/test/server/test.deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/test/server/test.deployment-group.ts index fd486f20f1eb2..761d0e38eb6a9 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/server/test.deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/server/test.deployment-group.ts @@ -42,6 +42,78 @@ export = { test.done(); }, + 'uses good linux install agent script'(test: Test) { + const stack = new cdk.Stack(); + + const asg = new autoscaling.AutoScalingGroup(stack, 'ASG', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.STANDARD3, ec2.InstanceSize.SMALL), + machineImage: new ec2.AmazonLinuxImage(), + vpc: new ec2.Vpc(stack, 'VPC'), + }); + + new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { + autoScalingGroups: [asg], + installAgent: true, + }); + + expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + 'UserData': { + 'Fn::Base64': { + 'Fn::Join': [ + '', + [ + '#!/bin/bash\nPKG_CMD=`which yum 2>/dev/null`\nif [ -z "$PKG_CMD" ]; then\nPKG_CMD=apt-get\nelse\nPKG=CMD=yum\nfi\n$PKG_CMD update -y\n$PKG_CMD install -y ruby2.0\nif [ $? -ne 0 ]; then\n$PKG_CMD install -y ruby\nfi\n$PKG_CMD install -y awscli\nTMP_DIR=`mktemp -d`\ncd $TMP_DIR\naws s3 cp s3://aws-codedeploy-', + { + 'Ref': 'AWS::Region', + }, + '/latest/install . --region ', + { + 'Ref': 'AWS::Region', + }, + '\nchmod +x ./install\n./install auto\nrm -fr $TMP_DIR', + ], + ], + }, + }, + })); + + test.done(); + }, + + 'uses good windows install agent script'(test: Test) { + const stack = new cdk.Stack(); + + const asg = new autoscaling.AutoScalingGroup(stack, 'ASG', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.STANDARD3, ec2.InstanceSize.SMALL), + machineImage: new ec2.WindowsImage(ec2.WindowsVersion.WINDOWS_SERVER_2019_ENGLISH_FULL_BASE, {}), + vpc: new ec2.Vpc(stack, 'VPC'), + }); + + new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { + autoScalingGroups: [asg], + installAgent: true, + }); + + expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + 'UserData': { + 'Fn::Base64': { + 'Fn::Join': [ + '', + [ + 'Set-Variable -Name TEMPDIR -Value (New-TemporaryFile).DirectoryName\naws s3 cp s3://aws-codedeploy-', + { + 'Ref': 'AWS::Region', + }, + '/latest/codedeploy-agent.msi $TEMPDIR\\codedeploy-agent.msi\ncd $TEMPDIR\n.\\codedeploy-agent.msi /quiet /l c:\\temp\\host-agent-install-log.txt', + ], + ], + }, + }, + })); + + test.done(); + }, + 'created with ASGs contains the ASG names'(test: Test) { const stack = new cdk.Stack(); From 99fd074a07ead624f64d3fe64685ba67c798976e Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Fri, 29 Jan 2021 01:09:57 -0800 Subject: [PATCH 11/33] fix(codepipeline): permission denied for Action-level environment variables (#12761) We correctly added permissions for SSM and SecretsManager-type environment variables set on the CodeBuild Project itself, but we forgot that environment variables could also be set on the CodeBuild CodePipeline action. Fixes #12742 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-codebuild/lib/project.ts | 98 +++++++++---------- .../lib/codebuild/build-action.ts | 2 +- ...g.pipeline-code-commit-build.expected.json | 26 ++++- 3 files changed, 70 insertions(+), 56 deletions(-) diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index c98718bd744bf..2f31bc897f9f8 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -695,9 +695,11 @@ export class Project extends ProjectBase { * @returns an array of {@link CfnProject.EnvironmentVariableProperty} instances */ public static serializeEnvVariables(environmentVariables: { [name: string]: BuildEnvironmentVariable }, - validateNoPlainTextSecrets: boolean = false): CfnProject.EnvironmentVariableProperty[] { + validateNoPlainTextSecrets: boolean = false, principal?: iam.IGrantable): CfnProject.EnvironmentVariableProperty[] { const ret = new Array(); + const ssmVariables = new Array(); + const secretsManagerSecrets = new Array(); for (const [name, envVariable] of Object.entries(environmentVariables)) { const cfnEnvVariable: CfnProject.EnvironmentVariableProperty = { @@ -720,6 +722,46 @@ export class Project extends ProjectBase { } } } + + if (principal) { + // save the SSM env variables + if (envVariable.type === BuildEnvironmentVariableType.PARAMETER_STORE) { + const envVariableValue = envVariable.value.toString(); + ssmVariables.push(Stack.of(principal).formatArn({ + service: 'ssm', + resource: 'parameter', + // If the parameter name starts with / the resource name is not separated with a double '/' + // arn:aws:ssm:region:1111111111:parameter/PARAM_NAME + resourceName: envVariableValue.startsWith('/') + ? envVariableValue.substr(1) + : envVariableValue, + })); + } + + // save SecretsManager env variables + if (envVariable.type === BuildEnvironmentVariableType.SECRETS_MANAGER) { + secretsManagerSecrets.push(Stack.of(principal).formatArn({ + service: 'secretsmanager', + resource: 'secret', + // we don't know the exact ARN of the Secret just from its name, but we can get close + resourceName: `${envVariable.value}-??????`, + sep: ':', + })); + } + } + } + + if (ssmVariables.length !== 0) { + principal?.grantPrincipal.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: ['ssm:GetParameters'], + resources: ssmVariables, + })); + } + if (secretsManagerSecrets.length !== 0) { + principal?.grantPrincipal.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: ['secretsmanager:GetSecretValue'], + resources: secretsManagerSecrets, + })); } return ret; @@ -854,7 +896,6 @@ export class Project extends ProjectBase { this.projectName = this.getResourceNameAttribute(resource.ref); this.addToRolePolicy(this.createLoggingPermission()); - this.addEnvVariablesPermissions(props.environmentVariables); // add permissions to create and use test report groups // with names starting with the project's name, // unless the customer explicitly opts out of it @@ -1007,57 +1048,6 @@ export class Project extends ProjectBase { }); } - private addEnvVariablesPermissions(environmentVariables: { [name: string]: BuildEnvironmentVariable } | undefined): void { - this.addParameterStorePermissions(environmentVariables); - this.addSecretsManagerPermissions(environmentVariables); - } - - private addParameterStorePermissions(environmentVariables: { [name: string]: BuildEnvironmentVariable } | undefined): void { - const resources = Object.values(environmentVariables || {}) - .filter(envVariable => envVariable.type === BuildEnvironmentVariableType.PARAMETER_STORE) - .map(envVariable => - // If the parameter name starts with / the resource name is not separated with a double '/' - // arn:aws:ssm:region:1111111111:parameter/PARAM_NAME - (envVariable.value as string).startsWith('/') - ? (envVariable.value as string).substr(1) - : envVariable.value) - .map(envVariable => Stack.of(this).formatArn({ - service: 'ssm', - resource: 'parameter', - resourceName: envVariable, - })); - - if (resources.length === 0) { - return; - } - - this.addToRolePolicy(new iam.PolicyStatement({ - actions: ['ssm:GetParameters'], - resources, - })); - } - - private addSecretsManagerPermissions(environmentVariables: { [name: string]: BuildEnvironmentVariable } | undefined): void { - const resources = Object.values(environmentVariables || {}) - .filter(envVariable => envVariable.type === BuildEnvironmentVariableType.SECRETS_MANAGER) - .map(envVariable => Stack.of(this).formatArn({ - service: 'secretsmanager', - resource: 'secret', - // we don't know the exact ARN of the Secret just from its name, but we can get close - resourceName: `${envVariable.value}-??????`, - sep: ':', - })); - - if (resources.length === 0) { - return; - } - - this.addToRolePolicy(new iam.PolicyStatement({ - actions: ['secretsmanager:GetSecretValue'], - resources, - })); - } - private renderEnvironment( props: ProjectProps, projectVars: { [name: string]: BuildEnvironmentVariable } = {}): CfnProject.EnvironmentProperty { @@ -1118,7 +1108,7 @@ export class Project extends ProjectBase { privilegedMode: env.privileged || false, computeType: env.computeType || this.buildImage.defaultComputeType, environmentVariables: hasEnvironmentVars - ? Project.serializeEnvVariables(vars, props.checkSecretsInPlainTextEnvVariables ?? true) + ? Project.serializeEnvVariables(vars, props.checkSecretsInPlainTextEnvVariables ?? true, this) : undefined, }; } 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 238efebae4658..1fc1611bd9ff2 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 @@ -207,7 +207,7 @@ export class CodeBuildAction extends Action { ProjectName: this.props.project.projectName, EnvironmentVariables: this.props.environmentVariables && cdk.Stack.of(scope).toJsonString(codebuild.Project.serializeEnvVariables(this.props.environmentVariables, - this.props.checkSecretsInPlainTextEnvVariables ?? true)), + this.props.checkSecretsInPlainTextEnvVariables ?? true, this.props.project)), }; if ((this.actionProperties.inputs || []).length > 1) { // lazy, because the Artifact name might be generated lazily diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit-build.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit-build.expected.json index 7fe649e0c2f8c..dbf8c85f91394 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit-build.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit-build.expected.json @@ -149,6 +149,30 @@ ] } }, + { + "Action": "ssm:GetParameters", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter/param_store" + ] + ] + } + }, { "Action": [ "s3:GetObject*", @@ -898,4 +922,4 @@ } } } -} \ No newline at end of file +} From 7dc45b2d8fc2f63497adb624fdfe4207c4eca269 Mon Sep 17 00:00:00 2001 From: Robert Djurasaj Date: Fri, 29 Jan 2021 03:27:32 -0700 Subject: [PATCH 12/33] chore(cloudfront): use standard file naming convention for OAI and Web Distribution (#12752) cc @njlynch ---- *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-cloudfront/lib/index.ts | 4 ++-- .../{origin_access_identity.ts => origin-access-identity.ts} | 0 .../lib/{web_distribution.ts => web-distribution.ts} | 2 +- .../{web_distribution.test.ts => web-distribution.test.ts} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename packages/@aws-cdk/aws-cloudfront/lib/{origin_access_identity.ts => origin-access-identity.ts} (100%) rename packages/@aws-cdk/aws-cloudfront/lib/{web_distribution.ts => web-distribution.ts} (99%) rename packages/@aws-cdk/aws-cloudfront/test/{web_distribution.test.ts => web-distribution.test.ts} (100%) diff --git a/packages/@aws-cdk/aws-cloudfront/lib/index.ts b/packages/@aws-cdk/aws-cloudfront/lib/index.ts index b0bd550231be3..726a1d1d01948 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/index.ts @@ -2,9 +2,9 @@ export * from './cache-policy'; export * from './distribution'; export * from './geo-restriction'; export * from './origin'; -export * from './origin_access_identity'; +export * from './origin-access-identity'; export * from './origin-request-policy'; -export * from './web_distribution'; +export * from './web-distribution'; export * as experimental from './experimental'; diff --git a/packages/@aws-cdk/aws-cloudfront/lib/origin_access_identity.ts b/packages/@aws-cdk/aws-cloudfront/lib/origin-access-identity.ts similarity index 100% rename from packages/@aws-cdk/aws-cloudfront/lib/origin_access_identity.ts rename to packages/@aws-cdk/aws-cloudfront/lib/origin-access-identity.ts diff --git a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/web-distribution.ts similarity index 99% rename from packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts rename to packages/@aws-cdk/aws-cloudfront/lib/web-distribution.ts index dad54c3b1488a..c62e1e09ed3f4 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/web-distribution.ts @@ -7,7 +7,7 @@ import { Construct } from 'constructs'; import { CfnDistribution } from './cloudfront.generated'; import { HttpVersion, IDistribution, LambdaEdgeEventType, OriginProtocolPolicy, PriceClass, ViewerProtocolPolicy, SSLMethod, SecurityPolicyProtocol } from './distribution'; import { GeoRestriction } from './geo-restriction'; -import { IOriginAccessIdentity } from './origin_access_identity'; +import { IOriginAccessIdentity } from './origin-access-identity'; /** * HTTP status code to failover to second origin diff --git a/packages/@aws-cdk/aws-cloudfront/test/web_distribution.test.ts b/packages/@aws-cdk/aws-cloudfront/test/web-distribution.test.ts similarity index 100% rename from packages/@aws-cdk/aws-cloudfront/test/web_distribution.test.ts rename to packages/@aws-cdk/aws-cloudfront/test/web-distribution.test.ts From 3cbf38b09a9e66a6c009f833481fb25b8c5fc26c Mon Sep 17 00:00:00 2001 From: Richie Hughes Date: Sat, 30 Jan 2021 00:54:23 +0000 Subject: [PATCH 13/33] feat(ecs-patterns): Add PlatformVersion option to ScheduledFargateTask props (#12676) Add the platformversion as an extra option to the Fargate Scheduled Task closes #12623 ---- *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 | 14 +++++ .../lib/base/scheduled-task-base.ts | 11 +++- .../lib/fargate/scheduled-fargate-task.ts | 25 ++++++++- .../fargate/test.scheduled-fargate-task.ts | 54 +++++++++++++++++++ 4 files changed, 101 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs-patterns/README.md b/packages/@aws-cdk/aws-ecs-patterns/README.md index 7a80d93aad346..38915031f4346 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/README.md +++ b/packages/@aws-cdk/aws-ecs-patterns/README.md @@ -427,3 +427,17 @@ const loadBalancedFargateService = new ApplicationLoadBalancedFargateService(sta }, }); ``` + +### Set PlatformVersion for ScheduledFargateTask + +```ts +const scheduledFargateTask = new ScheduledFargateTask(stack, 'ScheduledFargateTask', { + cluster, + scheduledFargateTaskImageOptions: { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }, + schedule: events.Schedule.expression('rate(1 minute)'), + platformVersion: ecs.FargatePlatformVersion.VERSION1_4, +}); +``` diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/scheduled-task-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/scheduled-task-base.ts index 259e375b1973c..38ac4e30a79af 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/scheduled-task-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/scheduled-task-base.ts @@ -165,11 +165,20 @@ export abstract class ScheduledTaskBase extends CoreConstruct { subnetSelection: this.subnetSelection, }); - this.eventRule.addTarget(eventRuleTarget); + this.addTaskAsTarget(eventRuleTarget); return eventRuleTarget; } + /** + * Adds task as a target of the scheduled event rule. + * + * @param ecsTaskTarget the EcsTask to add to the event rule + */ + protected addTaskAsTarget(ecsTaskTarget: EcsTask) { + this.eventRule.addTarget(ecsTaskTarget); + } + /** * Returns the default cluster. */ diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/scheduled-fargate-task.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/scheduled-fargate-task.ts index 8ad898693b6bd..27b9d8b6ad224 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/scheduled-fargate-task.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/scheduled-fargate-task.ts @@ -1,4 +1,5 @@ -import { FargateTaskDefinition } from '@aws-cdk/aws-ecs'; +import { FargateTaskDefinition, FargatePlatformVersion } from '@aws-cdk/aws-ecs'; +import { EcsTask } from '@aws-cdk/aws-events-targets'; import { Construct } from 'constructs'; import { ScheduledTaskBase, ScheduledTaskBaseProps, ScheduledTaskImageProps } from '../base/scheduled-task-base'; @@ -21,6 +22,17 @@ export interface ScheduledFargateTaskProps extends ScheduledTaskBaseProps { * @default none */ readonly scheduledFargateTaskImageOptions?: ScheduledFargateTaskImageOptions; + + /** + * The platform version on which to run your service. + * + * If one is not specified, the LATEST platform version is used by default. For more information, see + * [AWS Fargate Platform Versions](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/platform_versions.html) + * in the Amazon Elastic Container Service Developer Guide. + * + * @default Latest + */ + readonly platformVersion?: FargatePlatformVersion; } /** @@ -109,6 +121,15 @@ export class ScheduledFargateTask extends ScheduledTaskBase { throw new Error('You must specify one of: taskDefinition or image'); } - this.addTaskDefinitionToEventTarget(this.taskDefinition); + // Use the EcsTask as the target of the EventRule + const eventRuleTarget = new EcsTask( { + cluster: this.cluster, + taskDefinition: this.taskDefinition, + taskCount: this.desiredTaskCount, + subnetSelection: this.subnetSelection, + platformVersion: props.platformVersion, + }); + + this.addTaskAsTarget(eventRuleTarget); } } diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.scheduled-fargate-task.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.scheduled-fargate-task.ts index 27367249ee3cb..fa62f079862f1 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.scheduled-fargate-task.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.scheduled-fargate-task.ts @@ -294,6 +294,60 @@ export = { ], })); + test.done(); + }, + 'Scheduled Fargate Task - with platformVersion defined'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + + new ScheduledFargateTask(stack, 'ScheduledFargateTask', { + cluster, + scheduledFargateTaskImageOptions: { + image: ecs.ContainerImage.fromRegistry('henk'), + memoryLimitMiB: 512, + }, + schedule: events.Schedule.expression('rate(1 minute)'), + platformVersion: ecs.FargatePlatformVersion.VERSION1_4, + }); + + // THEN + expect(stack).to(haveResource('AWS::Events::Rule', { + Targets: [ + { + Arn: { 'Fn::GetAtt': ['EcsCluster97242B84', 'Arn'] }, + EcsParameters: { + LaunchType: 'FARGATE', + NetworkConfiguration: { + AwsVpcConfiguration: { + AssignPublicIp: 'DISABLED', + SecurityGroups: [ + { + 'Fn::GetAtt': [ + 'ScheduledFargateTaskScheduledTaskDefSecurityGroupE075BC19', + 'GroupId', + ], + }, + ], + Subnets: [ + { + Ref: 'VpcPrivateSubnet1Subnet536B997A', + }, + ], + }, + }, + PlatformVersion: '1.4.0', + TaskCount: 1, + TaskDefinitionArn: { Ref: 'ScheduledFargateTaskScheduledTaskDef521FA675' }, + }, + Id: 'Target0', + Input: '{}', + RoleArn: { 'Fn::GetAtt': ['ScheduledFargateTaskScheduledTaskDefEventsRole6CE19522', 'Arn'] }, + }, + ], + })); + test.done(); }, }; From 1a9f2a8100a64405c6cd2006a9682048b6ff0b80 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Sun, 31 Jan 2021 01:35:28 -0800 Subject: [PATCH 14/33] chore: add new interfaces for Assets (#12700) In V2, we want to get rid of the `@aws-cdk/assets` module, as it's considered deprecated in V1. Unfortunately, the module contains an interface, `CopyOptions`, that is used, through interface inheritance, in the public API of many stable CDK modules like ECS, Lambda, ECR, CodeBuild, etc. While we have a `CopyOptions` interface in `@aws-cdk/core`, it unfortunately shares the same name, `follow`, with the property in the "old" `CopyOptions`. But the two different `follow` properties have different types. For that reason, if we're going to remove the "old" `CopyOptions` using JSII's "strip deprecated" option, we can't use the "new" `CopyOptions` in the inheritance hierarchy alongside the "old" `CopyOptions`, as the two definitions of `follow` would conflict. Because of that, create a new `FileCopyOptions` interface which renames the `follow` property to `followSymlinks`. Also add a `FileFingerprintOptions` interface that does a similar trick to the `FingerprintOptions` interface (which extends `CopyOptions`), which is used in the public API of modules that use Docker assets. Also extract a few module-private interfaces to avoid duplication of properties between all of these interfaces. After this change, an interface from one of the non-deprecated assets libraries (S3 or ECR) using `FileOptions` from `@aws-cdk/assets` will look like this: ```ts // this is in @aws-cdk/aws-s3-assets export interface AssetOptions extends assets.CopyOptions, cdk.FileCopyOptions, cdk.AssetOptions { // ... ``` Then, when we enable stripping the deprecated elements using JSII on the V2 branch, this will be turned to: ```ts export interface AssetOptions extends cdk.FileCopyOptions, cdk.AssetOptions { // ... ``` Allowing us to deprecate the `@aws-cdk/assets` module, and not ship it with `aws-cdk-lib`. ---- *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/fs/options.ts | 1 + .../aws-ecr-assets/lib/image-asset.ts | 19 +++++-- packages/@aws-cdk/aws-s3-assets/lib/asset.ts | 4 +- packages/@aws-cdk/core/lib/fs/options.ts | 51 ++++++++++++++----- 4 files changed, 56 insertions(+), 19 deletions(-) diff --git a/packages/@aws-cdk/assets/lib/fs/options.ts b/packages/@aws-cdk/assets/lib/fs/options.ts index 3ccc107d3700d..548fa4bda42ee 100644 --- a/packages/@aws-cdk/assets/lib/fs/options.ts +++ b/packages/@aws-cdk/assets/lib/fs/options.ts @@ -10,6 +10,7 @@ export interface CopyOptions { * A strategy for how to handle symlinks. * * @default Never + * @deprecated use `followSymlinks` instead */ readonly follow?: FollowMode; diff --git a/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts b/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts index 2f6f5ff436baa..91d3f06b5f6a2 100644 --- a/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts +++ b/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts @@ -2,14 +2,16 @@ import * as fs from 'fs'; import * as path from 'path'; import * as assets from '@aws-cdk/assets'; import * as ecr from '@aws-cdk/aws-ecr'; -import { Annotations, Construct as CoreConstruct, FeatureFlags, IgnoreMode, Stack, Token } from '@aws-cdk/core'; +import { + Annotations, AssetStaging, Construct as CoreConstruct, FeatureFlags, FileFingerprintOptions, IgnoreMode, Stack, SymlinkFollowMode, Token, +} from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; /** * Options for DockerImageAsset */ -export interface DockerImageAssetOptions extends assets.FingerprintOptions { +export interface DockerImageAssetOptions extends assets.FingerprintOptions, FileFingerprintOptions { /** * ECR repository name * @@ -137,8 +139,9 @@ export class DockerImageAsset extends CoreConstruct implements assets.IAsset { // deletion of the ECR repository the app used). extraHash.version = '1.21.0'; - const staging = new assets.Staging(this, 'Staging', { + const staging = new AssetStaging(this, 'Staging', { ...props, + follow: props.followSymlinks ?? toSymlinkFollow(props.follow), exclude, ignoreMode, sourcePath: dir, @@ -181,3 +184,13 @@ function validateBuildArgs(buildArgs?: { [key: string]: string }) { } } } + +function toSymlinkFollow(follow?: assets.FollowMode): SymlinkFollowMode | undefined { + switch (follow) { + case undefined: return undefined; + case assets.FollowMode.NEVER: return SymlinkFollowMode.NEVER; + case assets.FollowMode.ALWAYS: return SymlinkFollowMode.ALWAYS; + case assets.FollowMode.BLOCK_EXTERNAL: return SymlinkFollowMode.BLOCK_EXTERNAL; + case assets.FollowMode.EXTERNAL: return SymlinkFollowMode.EXTERNAL; + } +} diff --git a/packages/@aws-cdk/aws-s3-assets/lib/asset.ts b/packages/@aws-cdk/aws-s3-assets/lib/asset.ts index 938778d1381f4..d674d083b248b 100644 --- a/packages/@aws-cdk/aws-s3-assets/lib/asset.ts +++ b/packages/@aws-cdk/aws-s3-assets/lib/asset.ts @@ -15,7 +15,7 @@ import { Construct as CoreConstruct } from '@aws-cdk/core'; const ARCHIVE_EXTENSIONS = ['.zip', '.jar']; -export interface AssetOptions extends assets.CopyOptions, cdk.AssetOptions { +export interface AssetOptions extends assets.CopyOptions, cdk.FileCopyOptions, 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. @@ -128,7 +128,7 @@ export class Asset extends CoreConstruct implements cdk.IAsset { const staging = new cdk.AssetStaging(this, 'Stage', { ...props, sourcePath: path.resolve(props.path), - follow: toSymlinkFollow(props.follow), + follow: props.followSymlinks ?? toSymlinkFollow(props.follow), assetHash: props.assetHash ?? props.sourceHash, }); diff --git a/packages/@aws-cdk/core/lib/fs/options.ts b/packages/@aws-cdk/core/lib/fs/options.ts index 3ea836a24e831..baf73bd7ffd30 100644 --- a/packages/@aws-cdk/core/lib/fs/options.ts +++ b/packages/@aws-cdk/core/lib/fs/options.ts @@ -56,19 +56,9 @@ export enum IgnoreMode { * context flag is set. */ DOCKER = 'docker' -}; - -/** - * Obtains applied when copying directories into the staging location. - */ -export interface CopyOptions { - /** - * A strategy for how to handle symlinks. - * - * @default SymlinkFollowMode.NEVER - */ - readonly follow?: SymlinkFollowMode; +} +interface FileOptions { /** * Glob patterns to exclude from the copy. * @@ -85,9 +75,30 @@ export interface CopyOptions { } /** - * Options related to calculating source hash. + * Options applied when copying directories + */ +export interface CopyOptions extends FileOptions { + /** + * A strategy for how to handle symlinks. + * + * @default SymlinkFollowMode.NEVER + */ + readonly follow?: SymlinkFollowMode; +} + +/** + * Options applied when copying directories into the staging location. */ -export interface FingerprintOptions extends CopyOptions { +export interface FileCopyOptions extends FileOptions { + /** + * A strategy for how to handle symlinks. + * + * @default SymlinkFollowMode.NEVER + */ + readonly followSymlinks?: SymlinkFollowMode; +} + +interface ExtraHashOptions { /** * Extra information to encode into the fingerprint (e.g. build instructions * and other inputs) @@ -96,3 +107,15 @@ export interface FingerprintOptions extends CopyOptions { */ readonly extraHash?: string; } + +/** + * Options related to calculating source hash. + */ +export interface FingerprintOptions extends CopyOptions, ExtraHashOptions { +} + +/** + * Options related to calculating source hash. + */ +export interface FileFingerprintOptions extends FileCopyOptions, ExtraHashOptions { +} From 84342943ad9f2ea8a83773f00816a0b8117c4d17 Mon Sep 17 00:00:00 2001 From: Eli Polonsky Date: Sun, 31 Jan 2021 12:52:18 +0200 Subject: [PATCH 15/33] feat(ecr): Public Gallery authorization token (#12775) API for granting permissions to retrieve an authorization token for the [Public ECR Gallery](https://gallery.ecr.aws/), similarly to the [existing API](https://github.com/aws/aws-cdk/blob/master/packages/@aws-cdk/aws-ecr/lib/auth-token.ts) for private ECR registries. Also added a note in the README encouraging users to prefer authenticated pulls over anonymous ones to benefit from higher limits. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-ecr/README.md | 20 +++++++++++- packages/@aws-cdk/aws-ecr/lib/auth-token.ts | 28 +++++++++++++++- .../@aws-cdk/aws-ecr/test/test.auth-token.ts | 32 +++++++++++++++++-- 3 files changed, 76 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-ecr/README.md b/packages/@aws-cdk/aws-ecr/README.md index 6278238b84eab..9bce0c80e132c 100644 --- a/packages/@aws-cdk/aws-ecr/README.md +++ b/packages/@aws-cdk/aws-ecr/README.md @@ -51,11 +51,29 @@ grants an IAM user access to call this API. ```ts import * as iam from '@aws-cdk/aws-iam'; +import * as ecr from '@aws-cdk/aws-ecr'; const user = new iam.User(this, 'User', { ... }); -iam.AuthorizationToken.grantRead(user); +ecr.AuthorizationToken.grantRead(user); ``` +If you access images in the [Public ECR Gallery](https://gallery.ecr.aws/) as well, it is recommended you authenticate to the regsitry to benefit from +higher rate and bandwidth limits. + +> See `Pricing` in https://aws.amazon.com/blogs/aws/amazon-ecr-public-a-new-public-container-registry/ and [Service quotas](https://docs.aws.amazon.com/AmazonECR/latest/public/public-service-quotas.html). + +The following code snippet grants an IAM user access to retrieve an authorization token for the public gallery. + +```ts +import * as iam from '@aws-cdk/aws-iam'; +import * as ecr from '@aws-cdk/aws-ecr'; + +const user = new iam.User(this, 'User', { ... }); +ecr.PublicGalleryAuthorizationToken.grantRead(user); +``` + +This user can then proceed to login to the registry using one of the [authentication methods](https://docs.aws.amazon.com/AmazonECR/latest/public/public-registries.html#public-registry-auth). + ## Automatically clean up repositories You can set life cycle rules to automatically clean up old images from your diff --git a/packages/@aws-cdk/aws-ecr/lib/auth-token.ts b/packages/@aws-cdk/aws-ecr/lib/auth-token.ts index 52c10cc513d0a..63484bbed0199 100644 --- a/packages/@aws-cdk/aws-ecr/lib/auth-token.ts +++ b/packages/@aws-cdk/aws-ecr/lib/auth-token.ts @@ -1,7 +1,9 @@ import * as iam from '@aws-cdk/aws-iam'; /** - * Authorization token to access ECR repositories via Docker CLI. + * Authorization token to access private ECR repositories in the current environment via Docker CLI. + * + * @see https://docs.aws.amazon.com/AmazonECR/latest/userguide/registry_auth.html */ export class AuthorizationToken { /** @@ -18,3 +20,27 @@ export class AuthorizationToken { private constructor() { } } + +/** + * Authorization token to access the global public ECR Gallery via Docker CLI. + * + * @see https://docs.aws.amazon.com/AmazonECR/latest/public/public-registries.html#public-registry-auth + */ +export class PublicGalleryAuthorizationToken { + + /** + * Grant access to retrieve an authorization token. + */ + public static grantRead(grantee: iam.IGrantable) { + grantee.grantPrincipal.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: ['ecr-public:GetAuthorizationToken', 'sts:GetServiceBearerToken'], + // GetAuthorizationToken only allows '*'. See https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonelasticcontainerregistry.html#amazonelasticcontainerregistry-actions-as-permissions + // GetServiceBearerToken only allows '*'. See https://docs.aws.amazon.com/service-authorization/latest/reference/list_awssecuritytokenservice.html#awssecuritytokenservice-actions-as-permissions + resources: ['*'], + })); + } + + private constructor() { + } + +} diff --git a/packages/@aws-cdk/aws-ecr/test/test.auth-token.ts b/packages/@aws-cdk/aws-ecr/test/test.auth-token.ts index 4e9e12e4fb078..bb1e13c5566b4 100644 --- a/packages/@aws-cdk/aws-ecr/test/test.auth-token.ts +++ b/packages/@aws-cdk/aws-ecr/test/test.auth-token.ts @@ -2,10 +2,10 @@ import { expect, haveResourceLike } from '@aws-cdk/assert'; import * as iam from '@aws-cdk/aws-iam'; import { Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; -import { AuthorizationToken } from '../lib'; +import { AuthorizationToken, PublicGalleryAuthorizationToken } from '../lib'; export = { - 'grant()'(test: Test) { + 'AuthorizationToken.grantRead()'(test: Test) { // GIVEN const stack = new Stack(); const user = new iam.User(stack, 'User'); @@ -28,4 +28,32 @@ export = { test.done(); }, + + 'PublicGalleryAuthorizationToken.grantRead()'(test: Test) { + // GIVEN + const stack = new Stack(); + const user = new iam.User(stack, 'User'); + + // WHEN + PublicGalleryAuthorizationToken.grantRead(user); + + // THEN + expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'ecr-public:GetAuthorizationToken', + 'sts:GetServiceBearerToken', + ], + Effect: 'Allow', + Resource: '*', + }, + ], + }, + })); + + test.done(); + }, + }; \ No newline at end of file From 5060782b00e17bdf44e225f8f5ef03344be238c7 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Sun, 31 Jan 2021 06:56:30 -0800 Subject: [PATCH 16/33] fix(cfn-include): AWS::CloudFormation resources fail in monocdk (#12758) When we did the changes to fix cloudformation-include in #11595, we did not account for the fact that the `@aws-cdk/core` is not mapped to `uberpackage/core`, but instead just to the `uberpackage` root namespace. Special-case the `@aws-cdk/core` module in ubergen when transforming the `cfn-types-2-classes.json` file. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../uberpackage/cfn-include-app/example-template.json | 3 +++ tools/ubergen/bin/ubergen.ts | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk/test/integ/uberpackage/cfn-include-app/example-template.json b/packages/aws-cdk/test/integ/uberpackage/cfn-include-app/example-template.json index 0385b58961413..8ad9310b8fd4d 100644 --- a/packages/aws-cdk/test/integ/uberpackage/cfn-include-app/example-template.json +++ b/packages/aws-cdk/test/integ/uberpackage/cfn-include-app/example-template.json @@ -1,5 +1,8 @@ { "Resources": { + "NoopHandle": { + "Type": "AWS::CloudFormation::WaitConditionHandle" + }, "Bucket": { "Type": "AWS::S3::Bucket", "Properties": { diff --git a/tools/ubergen/bin/ubergen.ts b/tools/ubergen/bin/ubergen.ts index ea5c529f77f3e..9047fc28c371a 100644 --- a/tools/ubergen/bin/ubergen.ts +++ b/tools/ubergen/bin/ubergen.ts @@ -333,8 +333,11 @@ async function copyOrTransformFiles(from: string, to: string, libraries: readonl const cfnTypes2Classes: { [key: string]: string } = await fs.readJson(source); for (const cfnType of Object.keys(cfnTypes2Classes)) { const fqn = cfnTypes2Classes[cfnType]; - // replace @aws-cdk/aws- with /aws- - cfnTypes2Classes[cfnType] = fqn.replace('@aws-cdk', uberPackageJson.name); + // replace @aws-cdk/aws- with /aws-, + // except for @aws-cdk/core, which maps just to the name of the uberpackage + cfnTypes2Classes[cfnType] = fqn.startsWith('@aws-cdk/core.') + ? fqn.replace('@aws-cdk/core', uberPackageJson.name) + : fqn.replace('@aws-cdk', uberPackageJson.name); } await fs.writeJson(destination, cfnTypes2Classes, { spaces: 2 }); } else { From 889d6734c10174f2661e45057c345cd112a44187 Mon Sep 17 00:00:00 2001 From: Daisuke Yoshimoto Date: Mon, 1 Feb 2021 03:44:15 +0900 Subject: [PATCH 17/33] fix(efs): EFS fails to create when using a VPC with multiple subnets per availability zone (#12097) Fixes #10170 ### Fixes This PR has been modified so that it does not create a mount target if only VPC is specified in the EFS file system. ### Bug Currently, if a VPC is specified and no subnet is specified, the subnet search criteria are passed as undefined, all subnets in the VPC are retrieved, and mount targets are generated for all subnets in the VPC. I will. Since mount targets can only be created in one subnet for each Availability Zone, the above behavior will result in duplicate Availability Zones between mount targets, resulting in an error when creating the mount target. ---- *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/lib/efs-file-system.ts | 2 +- .../aws-efs/test/efs-file-system.test.ts | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-efs/lib/efs-file-system.ts b/packages/@aws-cdk/aws-efs/lib/efs-file-system.ts index a93ea1b76d5a4..60af6fde51752 100644 --- a/packages/@aws-cdk/aws-efs/lib/efs-file-system.ts +++ b/packages/@aws-cdk/aws-efs/lib/efs-file-system.ts @@ -272,7 +272,7 @@ export class FileSystem extends Resource implements IFileSystem { defaultPort: ec2.Port.tcp(FileSystem.DEFAULT_PORT), }); - const subnets = props.vpc.selectSubnets(props.vpcSubnets); + const subnets = props.vpc.selectSubnets(props.vpcSubnets ?? { onePerAz: true }); // We now have to create the mount target for each of the mentioned subnet let mountTargetCount = 0; diff --git a/packages/@aws-cdk/aws-efs/test/efs-file-system.test.ts b/packages/@aws-cdk/aws-efs/test/efs-file-system.test.ts index 5b9f3c6539a45..d3868a841e0a5 100644 --- a/packages/@aws-cdk/aws-efs/test/efs-file-system.test.ts +++ b/packages/@aws-cdk/aws-efs/test/efs-file-system.test.ts @@ -1,4 +1,4 @@ -import { expect as expectCDK, haveResource, ResourcePart } from '@aws-cdk/assert'; +import { expect as expectCDK, haveResource, ResourcePart, countResources } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as kms from '@aws-cdk/aws-kms'; import { RemovalPolicy, Size, Stack, Tags } from '@aws-cdk/core'; @@ -240,3 +240,17 @@ test('can specify backup policy', () => { }, })); }); + +test('can create when using a VPC with multiple subnets per availability zone', () => { + // create a vpc with two subnets in the same availability zone. + const oneAzVpc = new ec2.Vpc(stack, 'Vpc', { + maxAzs: 1, + subnetConfiguration: [{ name: 'One', subnetType: ec2.SubnetType.ISOLATED }, { name: 'Two', subnetType: ec2.SubnetType.ISOLATED }], + natGateways: 0, + }); + new FileSystem(stack, 'EfsFileSystem', { + vpc: oneAzVpc, + }); + // make sure only one mount target is created. + expectCDK(stack).to(countResources('AWS::EFS::MountTarget', 1)); +}); From 2c8a40913dd86a1fb464b57275492d260e4abd0f Mon Sep 17 00:00:00 2001 From: flemjame-at-amazon <57235867+flemjame-at-amazon@users.noreply.github.com> Date: Mon, 1 Feb 2021 08:03:53 -0500 Subject: [PATCH 18/33] docs(lambda): Example to guarantee new version creation (#12598) This is created to provide an example of a workaround for https://github.com/aws/aws-cdk/issues/10136 ---- *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/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/@aws-cdk/aws-lambda/README.md b/packages/@aws-cdk/aws-lambda/README.md index e2b317b8faba3..a59dc1a311936 100644 --- a/packages/@aws-cdk/aws-lambda/README.md +++ b/packages/@aws-cdk/aws-lambda/README.md @@ -143,6 +143,19 @@ of a new version resource. You can specify options for this version through the > of code providers (such as `lambda.Code.fromBucket`) require that you define a > `lambda.Version` resource directly since the CDK is unable to determine if > their contents had changed. +> +> An alternative to defining a `lambda.Version` is to set an environment variable +> which changes at least as often as your code does. This makes sure the function +> always has the latest code. +> +> ```ts +> const codeVersion = "stringOrMethodToGetCodeVersion"; +> const fn = new lambda.Function(this, 'MyFunction', { +> environment: { +> 'CodeVersionString': codeVersion +> } +> }); +> ``` The `version.addAlias()` method can be used to define an AWS Lambda alias that points to a specific version. From 415eb861c65829cc53eabbbb8706f83f08c74570 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 1 Feb 2021 14:40:16 +0100 Subject: [PATCH 19/33] feat(iam): Permissions Boundaries (#12777) Allow configuring Permissions Boundaries for an entire subtree using Aspects, add a sample policy which can be used to reduce future misconfiguration risk for untrusted CodeBuild projects as an example. Addresses one part of aws/aws-cdk-rfcs#5. Fixes #3242. ALSO IN THIS COMMIT: Fix a bug in the `assert` library, where `haveResource()` would *never* match any resource that didn't have a `Properties` block (even if we tested for no property in particular, or the absence of properties). This fix caused two ECS tests to fail, which were asserting the wrong thing anyway (both were asserting `notTo(haveResource(...))` where they actually meant to assert `to(haveResource())`. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../assert/lib/assertions/have-resource.ts | 2 +- packages/@aws-cdk/aws-codebuild/lib/index.ts | 1 + .../lib/untrusted-code-boundary-policy.ts | 94 ++++++++++++++++ .../test/test.untrusted-code-boundary.ts | 56 ++++++++++ .../test/ec2/test.ec2-task-definition.ts | 2 +- .../aws-ecs/test/test.aws-log-driver.ts | 4 +- packages/@aws-cdk/aws-iam/README.md | 44 ++++++++ packages/@aws-cdk/aws-iam/lib/index.ts | 1 + .../aws-iam/lib/permissions-boundary.ts | 53 +++++++++ .../aws-iam/test/permissions-boundary.test.ts | 101 ++++++++++++++++++ 10 files changed, 354 insertions(+), 4 deletions(-) create mode 100644 packages/@aws-cdk/aws-codebuild/lib/untrusted-code-boundary-policy.ts create mode 100644 packages/@aws-cdk/aws-codebuild/test/test.untrusted-code-boundary.ts create mode 100644 packages/@aws-cdk/aws-iam/lib/permissions-boundary.ts create mode 100644 packages/@aws-cdk/aws-iam/test/permissions-boundary.test.ts diff --git a/packages/@aws-cdk/assert/lib/assertions/have-resource.ts b/packages/@aws-cdk/assert/lib/assertions/have-resource.ts index 2f3352bee16ef..3e44013188b2c 100644 --- a/packages/@aws-cdk/assert/lib/assertions/have-resource.ts +++ b/packages/@aws-cdk/assert/lib/assertions/have-resource.ts @@ -66,7 +66,7 @@ export class HaveResourceAssertion extends JestFriendlyAssertion for (const logicalId of Object.keys(inspector.value.Resources || {})) { const resource = inspector.value.Resources[logicalId]; if (resource.Type === this.resourceType) { - const propsToCheck = this.part === ResourcePart.Properties ? resource.Properties : resource; + const propsToCheck = this.part === ResourcePart.Properties ? (resource.Properties ?? {}) : resource; // Pass inspection object as 2nd argument, initialize failure with default string, // to maintain backwards compatibility with old predicate API. diff --git a/packages/@aws-cdk/aws-codebuild/lib/index.ts b/packages/@aws-cdk/aws-codebuild/lib/index.ts index 96731b2130043..5c2de5f3119c2 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/index.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/index.ts @@ -10,6 +10,7 @@ export * from './cache'; export * from './build-spec'; export * from './file-location'; export * from './linux-gpu-build-image'; +export * from './untrusted-code-boundary-policy'; // AWS::CodeBuild CloudFormation Resources: export * from './codebuild.generated'; diff --git a/packages/@aws-cdk/aws-codebuild/lib/untrusted-code-boundary-policy.ts b/packages/@aws-cdk/aws-codebuild/lib/untrusted-code-boundary-policy.ts new file mode 100644 index 0000000000000..229cb547e7c1f --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/lib/untrusted-code-boundary-policy.ts @@ -0,0 +1,94 @@ +import * as iam from '@aws-cdk/aws-iam'; +import { Construct } from 'constructs'; + +/** + * Construction properties for UntrustedCodeBoundaryPolicy + */ +export interface UntrustedCodeBoundaryPolicyProps { + /** + * The name of the managed policy. + * + * @default - A name is automatically generated. + */ + readonly managedPolicyName?: string; + + /** + * Additional statements to add to the default set of statements + * + * @default - No additional statements + */ + readonly additionalStatements?: iam.PolicyStatement[]; +} + +/** + * Permissions Boundary for a CodeBuild Project running untrusted code + * + * This class is a Policy, intended to be used as a Permissions Boundary + * for a CodeBuild project. It allows most of the actions necessary to run + * the CodeBuild project, but disallows reading from Parameter Store + * and Secrets Manager. + * + * Use this when your CodeBuild project is running untrusted code (for + * example, if you are using one to automatically build Pull Requests + * that anyone can submit), and you want to prevent your future self + * from accidentally exposing Secrets to this build. + * + * (The reason you might want to do this is because otherwise anyone + * who can submit a Pull Request to your project can write a script + * to email those secrets to themselves). + * + * @example + * + * iam.PermissionsBoundary.of(project).apply(new UntrustedCodeBoundaryPolicy(this, 'Boundary')); + */ +export class UntrustedCodeBoundaryPolicy extends iam.ManagedPolicy { + constructor(scope: Construct, id: string, props: UntrustedCodeBoundaryPolicyProps = {}) { + super(scope, id, { + managedPolicyName: props.managedPolicyName, + description: 'Permissions Boundary Policy for CodeBuild Projects running untrusted code', + statements: [ + new iam.PolicyStatement({ + actions: [ + // For logging + 'logs:CreateLogGroup', + 'logs:CreateLogStream', + 'logs:PutLogEvents', + + // For test reports + 'codebuild:CreateReportGroup', + 'codebuild:CreateReport', + 'codebuild:UpdateReport', + 'codebuild:BatchPutTestCases', + 'codebuild:BatchPutCodeCoverages', + + // For batch builds + 'codebuild:StartBuild', + 'codebuild:StopBuild', + 'codebuild:RetryBuild', + + // For pulling ECR images + 'ecr:GetDownloadUrlForLayer', + 'ecr:BatchGetImage', + 'ecr:BatchCheckLayerAvailability', + + // For running in a VPC + 'ec2:CreateNetworkInterfacePermission', + 'ec2:CreateNetworkInterface', + 'ec2:DescribeNetworkInterfaces', + 'ec2:DeleteNetworkInterface', + 'ec2:DescribeSubnets', + 'ec2:DescribeSecurityGroups', + 'ec2:DescribeDhcpOptions', + 'ec2:DescribeVpcs', + + // NOTABLY MISSING: + // - Reading secrets + // - Reading parameterstore + ], + resources: ['*'], + }), + ...props.additionalStatements ?? [], + ], + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codebuild/test/test.untrusted-code-boundary.ts b/packages/@aws-cdk/aws-codebuild/test/test.untrusted-code-boundary.ts new file mode 100644 index 0000000000000..04196e631f5c8 --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/test.untrusted-code-boundary.ts @@ -0,0 +1,56 @@ +import { expect, haveResourceLike, arrayWith } from '@aws-cdk/assert'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import * as codebuild from '../lib'; + +export = { + 'can attach permissions boundary to Project'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const project = new codebuild.Project(stack, 'Project', { + source: codebuild.Source.gitHub({ owner: 'a', repo: 'b' }), + }); + iam.PermissionsBoundary.of(project).apply(new codebuild.UntrustedCodeBoundaryPolicy(stack, 'Boundary')); + + // THEN + expect(stack).to(haveResourceLike('AWS::IAM::Role', { + PermissionsBoundary: { Ref: 'BoundaryEA298153' }, + })); + + test.done(); + }, + + 'can add additional statements Boundary'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const project = new codebuild.Project(stack, 'Project', { + source: codebuild.Source.gitHub({ owner: 'a', repo: 'b' }), + }); + iam.PermissionsBoundary.of(project).apply(new codebuild.UntrustedCodeBoundaryPolicy(stack, 'Boundary', { + additionalStatements: [ + new iam.PolicyStatement({ + actions: ['a:a'], + resources: ['b'], + }), + ], + })); + + // THEN + expect(stack).to(haveResourceLike('AWS::IAM::ManagedPolicy', { + PolicyDocument: { + Statement: arrayWith({ + Effect: 'Allow', + Action: 'a:a', + Resource: 'b', + }), + }, + })); + + test.done(); + }, +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts index 8581738c6da51..9264c47e4550c 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts @@ -543,7 +543,7 @@ export = { }); // THEN - expect(stack).notTo(haveResource('AWS::ECR::Repository', {})); + expect(stack).to(haveResource('AWS::ECR::Repository', {})); test.done(); }, diff --git a/packages/@aws-cdk/aws-ecs/test/test.aws-log-driver.ts b/packages/@aws-cdk/aws-ecs/test/test.aws-log-driver.ts index 1ea5942386ad0..a4d5c9af9c53d 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.aws-log-driver.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.aws-log-driver.ts @@ -126,7 +126,7 @@ export = { test.done(); }, - 'without a defined log group'(test: Test) { + 'without a defined log group: creates one anyway'(test: Test) { // GIVEN td.addContainer('Container', { image, @@ -136,7 +136,7 @@ export = { }); // THEN - expect(stack).notTo(haveResource('AWS::Logs::LogGroup', {})); + expect(stack).to(haveResource('AWS::Logs::LogGroup', {})); test.done(); }, diff --git a/packages/@aws-cdk/aws-iam/README.md b/packages/@aws-cdk/aws-iam/README.md index d9488e7d081c8..f2b86ccff2f59 100644 --- a/packages/@aws-cdk/aws-iam/README.md +++ b/packages/@aws-cdk/aws-iam/README.md @@ -264,6 +264,50 @@ const newPolicy = new Policy(stack, 'MyNewPolicy', { }); ``` +## Permissions Boundaries + +[Permissions +Boundaries](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) +can be used as a mechanism to prevent privilege esclation by creating new +`Role`s. Permissions Boundaries are a Managed Policy, attached to Roles or +Users, that represent the *maximum* set of permissions they can have. The +effective set of permissions of a Role (or User) will be the intersection of +the Identity Policy and the Permissions Boundary attached to the Role (or +User). Permissions Boundaries are typically created by account +Administrators, and their use on newly created `Role`s will be enforced by +IAM policies. + +It is possible to attach Permissions Boundaries to all Roles created in a construct +tree all at once: + +```ts +// This imports an existing policy. +const boundary = iam.ManagedPolicy.fromManagedPolicyArn(this, 'Boundary', 'arn:aws:iam::123456789012:policy/boundary'); + +// This creates a new boundary +const boundary2 = new iam.ManagedPolicy(this, 'Boundary2', { + statements: [ + new iam.PolicyStatement({ + effect: iam.Effect.DENY, + actions: ['iam:*'], + resources: ['*'], + }), + ], +}); + +// Directly apply the boundary to a Role you create +iam.PermissionsBoundary.of(role).apply(boundary); + +// Apply the boundary to an Role that was implicitly created for you +iam.PermissionsBoundary.of(lambdaFunction).apply(boundary); + +// Apply the boundary to all Roles in a stack +iam.PermissionsBoundary.of(stack).apply(boundary); + +// Remove a Permissions Boundary that is inherited, for example from the Stack level +iam.PermissionsBoundary.of(customResource).clear(); +``` + ## OpenID Connect Providers OIDC identity providers are entities in IAM that describe an external identity diff --git a/packages/@aws-cdk/aws-iam/lib/index.ts b/packages/@aws-cdk/aws-iam/lib/index.ts index ba9250ca1e08e..19b8a156ba598 100644 --- a/packages/@aws-cdk/aws-iam/lib/index.ts +++ b/packages/@aws-cdk/aws-iam/lib/index.ts @@ -11,6 +11,7 @@ export * from './identity-base'; export * from './grant'; export * from './unknown-principal'; export * from './oidc-provider'; +export * from './permissions-boundary'; // AWS::IAM CloudFormation Resources: export * from './iam.generated'; diff --git a/packages/@aws-cdk/aws-iam/lib/permissions-boundary.ts b/packages/@aws-cdk/aws-iam/lib/permissions-boundary.ts new file mode 100644 index 0000000000000..7b4320462a1ac --- /dev/null +++ b/packages/@aws-cdk/aws-iam/lib/permissions-boundary.ts @@ -0,0 +1,53 @@ +import { Node, IConstruct } from 'constructs'; +import { CfnRole, CfnUser } from './iam.generated'; +import { IManagedPolicy } from './managed-policy'; + +/** + * Modify the Permissions Boundaries of Users and Roles in a construct tree + * + * @example + * + * const policy = ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess'); + * PermissionsBoundary.of(stack).apply(policy); + */ +export class PermissionsBoundary { + /** + * Access the Permissions Boundaries of a construct tree + */ + public static of(scope: IConstruct): PermissionsBoundary { + return new PermissionsBoundary(scope); + } + + private constructor(private readonly scope: IConstruct) { + } + + /** + * Apply the given policy as Permissions Boundary to all Roles in the scope + * + * Will override any Permissions Boundaries configured previously; in case + * a Permission Boundary is applied in multiple scopes, the Boundary applied + * closest to the Role wins. + */ + public apply(boundaryPolicy: IManagedPolicy) { + Node.of(this.scope).applyAspect({ + visit(node: IConstruct) { + if (node instanceof CfnRole || node instanceof CfnUser) { + node.permissionsBoundary = boundaryPolicy.managedPolicyArn; + } + }, + }); + } + + /** + * Remove previously applied Permissions Boundaries + */ + public clear() { + Node.of(this.scope).applyAspect({ + visit(node: IConstruct) { + if (node instanceof CfnRole || node instanceof CfnUser) { + node.permissionsBoundary = undefined; + } + }, + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/test/permissions-boundary.test.ts b/packages/@aws-cdk/aws-iam/test/permissions-boundary.test.ts new file mode 100644 index 0000000000000..99a14de55f2e1 --- /dev/null +++ b/packages/@aws-cdk/aws-iam/test/permissions-boundary.test.ts @@ -0,0 +1,101 @@ +import { ABSENT } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import { App, Stack } from '@aws-cdk/core'; +import * as iam from '../lib'; + +let app: App; +let stack: Stack; +beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack'); +}); + +test('apply imported boundary to a role', () => { + // GIVEN + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('service.amazonaws.com'), + }); + + // WHEN + iam.PermissionsBoundary.of(role).apply(iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess')); + + // THEN + expect(stack).toHaveResource('AWS::IAM::Role', { + PermissionsBoundary: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/ReadOnlyAccess', + ]], + }, + }); +}); + +test('apply imported boundary to a user', () => { + // GIVEN + const user = new iam.User(stack, 'User'); + + // WHEN + iam.PermissionsBoundary.of(user).apply(iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess')); + + // THEN + expect(stack).toHaveResource('AWS::IAM::User', { + PermissionsBoundary: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/ReadOnlyAccess', + ]], + }, + }); +}); + +test('apply newly created boundary to a role', () => { + // GIVEN + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('service.amazonaws.com'), + }); + + // WHEN + iam.PermissionsBoundary.of(role).apply(new iam.ManagedPolicy(stack, 'Policy', { + statements: [ + new iam.PolicyStatement({ + actions: ['*'], + resources: ['*'], + }), + ], + })); + + // THEN + expect(stack).toHaveResource('AWS::IAM::Role', { + PermissionsBoundary: { Ref: 'Policy23B91518' }, + }); +}); + +test('unapply inherited boundary from a user: order 1', () => { + // GIVEN + const user = new iam.User(stack, 'User'); + + // WHEN + iam.PermissionsBoundary.of(stack).apply(iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess')); + iam.PermissionsBoundary.of(user).clear(); + + // THEN + expect(stack).toHaveResource('AWS::IAM::User', { + PermissionsBoundary: ABSENT, + }); +}); + +test('unapply inherited boundary from a user: order 2', () => { + // GIVEN + const user = new iam.User(stack, 'User'); + + // WHEN + iam.PermissionsBoundary.of(user).clear(); + iam.PermissionsBoundary.of(stack).apply(iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess')); + + // THEN + expect(stack).toHaveResource('AWS::IAM::User', { + PermissionsBoundary: ABSENT, + }); +}); \ No newline at end of file From 3b66088010b6f2315a215e92505d5279680f16d4 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 1 Feb 2021 15:17:41 +0100 Subject: [PATCH 20/33] feat(core): `stack.exportValue()` can be used to solve "deadly embrace" (#12778) Deadly embrace (<3 who came up with this term) is an issue where a consumer stack depends on a producer stack via CloudFormation Exports, and you want to remove the use from the consumer. Removal of the resource sharing implicitly removes the CloudFormation Export, but now CloudFormation won't let you deploy that because the deployment order is always forced to be (1st) producer (2nd) consumer, and when the producer deploys and tries to remove the Export, the consumer is still using it. The best way to work around it is to manually ensure the CloudFormation Export exists while you remove the consuming relationship. @skinny85 has a [blog post] about this, but the mechanism can be more smooth. Add a method, `stack.exportValue(...)` which can be used to create the Export for the duration of the deployment that breaks the relationship, and add an explanation of how to use it. Genericize the method a bit so it also solves a long-standing issue about no L2 support for exports. Fixes #7602, fixes #2036. [blog post]: https://www.endoflineblog.com/cdk-tips-03-how-to-unblock-cross-stack-references ---- *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/README.md | 59 +++++++++ packages/@aws-cdk/core/lib/private/refs.ts | 79 ++++-------- packages/@aws-cdk/core/lib/stack.ts | 143 +++++++++++++++++++-- packages/@aws-cdk/core/lib/token.ts | 26 ++++ packages/@aws-cdk/core/test/stack.test.ts | 66 +++++++++- 5 files changed, 310 insertions(+), 63 deletions(-) diff --git a/packages/@aws-cdk/core/README.md b/packages/@aws-cdk/core/README.md index 8c5ce270d8dc1..880aea79a55fc 100644 --- a/packages/@aws-cdk/core/README.md +++ b/packages/@aws-cdk/core/README.md @@ -92,6 +92,65 @@ nested stack and referenced using `Fn::GetAtt "Outputs.Xxx"` from the parent. Nested stacks also support the use of Docker image and file assets. +## Accessing resources in a different stack + +You can access resources in a different stack, as long as they are in the +same account and AWS Region. The following example defines the stack `stack1`, +which defines an Amazon S3 bucket. Then it defines a second stack, `stack2`, +which takes the bucket from stack1 as a constructor property. + +```ts +const prod = { account: '123456789012', region: 'us-east-1' }; + +const stack1 = new StackThatProvidesABucket(app, 'Stack1' , { env: prod }); + +// stack2 will take a property { bucket: IBucket } +const stack2 = new StackThatExpectsABucket(app, 'Stack2', { + bucket: stack1.bucket, + env: prod +}); +``` + +If the AWS CDK determines that the resource is in the same account and +Region, but in a different stack, it automatically synthesizes AWS +CloudFormation +[Exports](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-stack-exports.html) +in the producing stack and an +[Fn::ImportValue](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-importvalue.html) +in the consuming stack to transfer that information from one stack to the +other. + +### Removing automatic cross-stack references + +The automatic references created by CDK when you use resources across stacks +are convenient, but may block your deployments if you want to remove the +resources that are referenced in this way. You will see an error like: + +```text +Export Stack1:ExportsOutputFnGetAtt-****** cannot be deleted as it is in use by Stack1 +``` + +Let's say there is a Bucket in the `stack1`, and the `stack2` references its +`bucket.bucketName`. You now want to remove the bucket and run into the error above. + +It's not safe to remove `stack1.bucket` while `stack2` is still using it, so +unblocking yourself from this is a two-step process. This is how it works: + +DEPLOYMENT 1: break the relationship + +- Make sure `stack2` no longer references `bucket.bucketName` (maybe the consumer + stack now uses its own bucket, or it writes to an AWS DynamoDB table, or maybe you just + remove the Lambda Function altogether). +- In the `stack1` class, call `this.exportAttribute(this.bucket.bucketName)`. This + will make sure the CloudFormation Export continues to exist while the relationship + between the two stacks is being broken. +- Deploy (this will effectively only change the `stack2`, but it's safe to deploy both). + +DEPLOYMENT 2: remove the resource + +- You are now free to remove the `bucket` resource from `stack1`. +- Don't forget to remove the `exportAttribute()` call as well. +- Deploy again (this time only the `stack1` will be changed -- the bucket will be deleted). ## Durations diff --git a/packages/@aws-cdk/core/lib/private/refs.ts b/packages/@aws-cdk/core/lib/private/refs.ts index 46d44563b4a96..27618d6776f21 100644 --- a/packages/@aws-cdk/core/lib/private/refs.ts +++ b/packages/@aws-cdk/core/lib/private/refs.ts @@ -1,22 +1,19 @@ // ---------------------------------------------------- // CROSS REFERENCES // ---------------------------------------------------- -import * as cxapi from '@aws-cdk/cx-api'; import { CfnElement } from '../cfn-element'; import { CfnOutput } from '../cfn-output'; import { CfnParameter } from '../cfn-parameter'; -import { Construct, IConstruct } from '../construct-compat'; -import { FeatureFlags } from '../feature-flags'; +import { IConstruct } from '../construct-compat'; import { Names } from '../names'; import { Reference } from '../reference'; import { IResolvable } from '../resolvable'; import { Stack } from '../stack'; -import { Token } from '../token'; +import { Token, Tokenization } from '../token'; import { CfnReference } from './cfn-reference'; import { Intrinsic } from './intrinsic'; import { findTokens } from './resolve'; -import { makeUniqueId } from './uniqueid'; /** * This is called from the App level to resolve all references defined. Each @@ -167,55 +164,10 @@ function findAllReferences(root: IConstruct) { function createImportValue(reference: Reference): Intrinsic { const exportingStack = Stack.of(reference.target); - // Ensure a singleton "Exports" scoping Construct - // This mostly exists to trigger LogicalID munging, which would be - // disabled if we parented constructs directly under Stack. - // Also it nicely prevents likely construct name clashes - const exportsScope = getCreateExportsScope(exportingStack); + const importExpr = exportingStack.exportValue(reference); - // Ensure a singleton CfnOutput for this value - const resolved = exportingStack.resolve(reference); - const id = 'Output' + JSON.stringify(resolved); - const exportName = generateExportName(exportsScope, id); - - if (Token.isUnresolved(exportName)) { - throw new Error(`unresolved token in generated export name: ${JSON.stringify(exportingStack.resolve(exportName))}`); - } - - const output = exportsScope.node.tryFindChild(id) as CfnOutput; - if (!output) { - new CfnOutput(exportsScope, id, { value: Token.asString(reference), exportName }); - } - - // We want to return an actual FnImportValue Token here, but Fn.importValue() returns a 'string', - // so construct one in-place. - return new Intrinsic({ 'Fn::ImportValue': exportName }); -} - -function getCreateExportsScope(stack: Stack) { - const exportsName = 'Exports'; - let stackExports = stack.node.tryFindChild(exportsName) as Construct; - if (stackExports === undefined) { - stackExports = new Construct(stack, exportsName); - } - - return stackExports; -} - -function generateExportName(stackExports: Construct, id: string) { - const stackRelativeExports = FeatureFlags.of(stackExports).isEnabled(cxapi.STACK_RELATIVE_EXPORTS_CONTEXT); - const stack = Stack.of(stackExports); - - const components = [ - ...stackExports.node.scopes - .slice(stackRelativeExports ? stack.node.scopes.length : 2) - .map(c => c.node.id), - id, - ]; - const prefix = stack.stackName ? stack.stackName + ':' : ''; - const localPart = makeUniqueId(components); - const maxLength = 255; - return prefix + localPart.slice(Math.max(0, localPart.length - maxLength + prefix.length)); + // I happen to know this returns a Fn.importValue() which implements Intrinsic. + return Tokenization.reverseCompleteString(importExpr) as Intrinsic; } // ------------------------------------------------------------------------------------------------ @@ -262,6 +214,25 @@ function createNestedStackOutput(producer: Stack, reference: Reference): CfnRefe return producer.nestedStackResource.getAtt(`Outputs.${output.logicalId}`) as CfnReference; } +/** + * Translate a Reference into a nested stack into a value in the parent stack + * + * Will create Outputs along the chain of Nested Stacks, and return the final `{ Fn::GetAtt }`. + */ +export function referenceNestedStackValueInParent(reference: Reference, targetStack: Stack) { + let currentStack = Stack.of(reference.target); + if (currentStack !== targetStack && !isNested(currentStack, targetStack)) { + throw new Error(`Referenced resource must be in stack '${targetStack.node.path}', got '${reference.target.node.path}'`); + } + + while (currentStack !== targetStack) { + reference = createNestedStackOutput(Stack.of(reference.target), reference); + currentStack = Stack.of(reference.target); + } + + return reference; +} + /** * @returns true if this stack is a direct or indirect parent of the nested * stack `nested`. @@ -282,4 +253,4 @@ function isNested(nested: Stack, parent: Stack): boolean { // recurse with the child's direct parent return isNested(nested.nestedStackParent, parent); -} +} \ 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 c6a8a56916f2f..53e7adfdc206e 100644 --- a/packages/@aws-cdk/core/lib/stack.ts +++ b/packages/@aws-cdk/core/lib/stack.ts @@ -775,6 +775,93 @@ export class Stack extends CoreConstruct implements ITaggable { } } + /** + * Create a CloudFormation Export for a value + * + * Returns a string representing the corresponding `Fn.importValue()` + * expression for this Export. You can control the name for the export by + * passing the `name` option. + * + * If you don't supply a value for `name`, the value you're exporting must be + * a Resource attribute (for example: `bucket.bucketName`) and it will be + * given the same name as the automatic cross-stack reference that would be created + * if you used the attribute in another Stack. + * + * One of the uses for this method is to *remove* the relationship between + * two Stacks established by automatic cross-stack references. It will + * temporarily ensure that the CloudFormation Export still exists while you + * remove the reference from the consuming stack. After that, you can remove + * the resource and the manual export. + * + * ## Example + * + * Here is how the process works. Let's say there are two stacks, + * `producerStack` and `consumerStack`, and `producerStack` has a bucket + * called `bucket`, which is referenced by `consumerStack` (perhaps because + * an AWS Lambda Function writes into it, or something like that). + * + * It is not safe to remove `producerStack.bucket` because as the bucket is being + * deleted, `consumerStack` might still be using it. + * + * Instead, the process takes two deployments: + * + * ### Deployment 1: break the relationship + * + * - Make sure `consumerStack` no longer references `bucket.bucketName` (maybe the consumer + * stack now uses its own bucket, or it writes to an AWS DynamoDB table, or maybe you just + * remove the Lambda Function altogether). + * - In the `ProducerStack` class, call `this.exportValue(this.bucket.bucketName)`. This + * will make sure the CloudFormation Export continues to exist while the relationship + * between the two stacks is being broken. + * - Deploy (this will effectively only change the `consumerStack`, but it's safe to deploy both). + * + * ### Deployment 2: remove the bucket resource + * + * - You are now free to remove the `bucket` resource from `producerStack`. + * - Don't forget to remove the `exportValue()` call as well. + * - Deploy again (this time only the `producerStack` will be changed -- the bucket will be deleted). + */ + public exportValue(exportedValue: any, options: ExportValueOptions = {}) { + if (options.name) { + new CfnOutput(this, `Export${options.name}`, { + value: exportedValue, + exportName: options.name, + }); + return Fn.importValue(options.name); + } + + const resolvable = Tokenization.reverse(exportedValue); + if (!resolvable || !Reference.isReference(resolvable)) { + throw new Error('exportValue: either supply \'name\' or make sure to export a resource attribute (like \'bucket.bucketName\')'); + } + + // "teleport" the value here, in case it comes from a nested stack. This will also + // ensure the value is from our own scope. + const exportable = referenceNestedStackValueInParent(resolvable, this); + + // Ensure a singleton "Exports" scoping Construct + // This mostly exists to trigger LogicalID munging, which would be + // disabled if we parented constructs directly under Stack. + // Also it nicely prevents likely construct name clashes + const exportsScope = getCreateExportsScope(this); + + // Ensure a singleton CfnOutput for this value + const resolved = this.resolve(exportable); + const id = 'Output' + JSON.stringify(resolved); + const exportName = generateExportName(exportsScope, id); + + if (Token.isUnresolved(exportName)) { + throw new Error(`unresolved token in generated export name: ${JSON.stringify(this.resolve(exportName))}`); + } + + const output = exportsScope.node.tryFindChild(id) as CfnOutput; + if (!output) { + new CfnOutput(exportsScope, id, { value: Token.asString(exportable), exportName }); + } + + return Fn.importValue(exportName); + } + /** * Returns the naming scheme used to allocate logical IDs. By default, uses * the `HashedAddressingScheme` but this method can be overridden to customize @@ -1143,18 +1230,58 @@ function makeStackName(components: string[]) { return makeUniqueId(components); } +function getCreateExportsScope(stack: Stack) { + const exportsName = 'Exports'; + let stackExports = stack.node.tryFindChild(exportsName) as CoreConstruct; + if (stackExports === undefined) { + stackExports = new CoreConstruct(stack, exportsName); + } + + return stackExports; +} + +function generateExportName(stackExports: CoreConstruct, id: string) { + const stackRelativeExports = FeatureFlags.of(stackExports).isEnabled(cxapi.STACK_RELATIVE_EXPORTS_CONTEXT); + const stack = Stack.of(stackExports); + + const components = [ + ...stackExports.node.scopes + .slice(stackRelativeExports ? stack.node.scopes.length : 2) + .map(c => c.node.id), + id, + ]; + const prefix = stack.stackName ? stack.stackName + ':' : ''; + const localPart = makeUniqueId(components); + const maxLength = 255; + return prefix + localPart.slice(Math.max(0, localPart.length - maxLength + prefix.length)); +} + +interface StackDependency { + stack: Stack; + reasons: string[]; +} + +/** + * Options for the `stack.exportValue()` method + */ +export interface ExportValueOptions { + /** + * The name of the export to create + * + * @default - A name is automatically chosen + */ + readonly name?: string; +} + // These imports have to be at the end to prevent circular imports +import { CfnOutput } from './cfn-output'; import { addDependency } from './deps'; +import { FileSystem } from './fs'; +import { Names } from './names'; 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'; -import { FileSystem } from './fs'; -import { Names } from './names'; - -interface StackDependency { - stack: Stack; - reasons: string[]; -} +import { Token, Tokenization } from './token'; +import { referenceNestedStackValueInParent } from './private/refs'; diff --git a/packages/@aws-cdk/core/lib/token.ts b/packages/@aws-cdk/core/lib/token.ts index 5f98db7a4f11f..4bbcdb454f9bd 100644 --- a/packages/@aws-cdk/core/lib/token.ts +++ b/packages/@aws-cdk/core/lib/token.ts @@ -132,6 +132,19 @@ export class Tokenization { return TokenMap.instance().splitString(s); } + /** + * Un-encode a string which is either a complete encoded token, or doesn't contain tokens at all + * + * It's illegal for the string to be a concatenation of an encoded token and something else. + */ + public static reverseCompleteString(s: string): IResolvable | undefined { + const fragments = Tokenization.reverseString(s); + if (fragments.length !== 1) { + throw new Error(`Tokenzation.reverseCompleteString: argument must not be a concatentation, got '${s}'`); + } + return fragments.firstToken; + } + /** * Un-encode a Tokenized value from a number */ @@ -146,6 +159,19 @@ export class Tokenization { return TokenMap.instance().lookupList(l); } + /** + * Reverse any value into a Resolvable, if possible + * + * In case of a string, the string must not be a concatenation. + */ + public static reverse(x: any): IResolvable | undefined { + if (Tokenization.isResolvable(x)) { return x; } + if (typeof x === 'string') { return Tokenization.reverseCompleteString(x); } + if (Array.isArray(x)) { return Tokenization.reverseList(x); } + if (typeof x === 'number') { return Tokenization.reverseNumber(x); } + return undefined; + } + /** * Resolves an object by evaluating all tokens and removing any undefined or empty objects or arrays. * Values can only be primitives, arrays or tokens. Other objects (i.e. with methods) will be rejected. diff --git a/packages/@aws-cdk/core/test/stack.test.ts b/packages/@aws-cdk/core/test/stack.test.ts index ceaf6a7c96e99..8891dbaa138c6 100644 --- a/packages/@aws-cdk/core/test/stack.test.ts +++ b/packages/@aws-cdk/core/test/stack.test.ts @@ -2,7 +2,9 @@ import * as cxapi from '@aws-cdk/cx-api'; import { testFutureBehavior, testLegacyBehavior } from 'cdk-build-tools/lib/feature-flag'; import { App, CfnCondition, CfnInclude, CfnOutput, CfnParameter, - CfnResource, Construct, Lazy, ScopedAws, Stack, validateString, ISynthesisSession, Tags, LegacyStackSynthesizer, DefaultStackSynthesizer, + CfnResource, Construct, Lazy, ScopedAws, Stack, validateString, + ISynthesisSession, Tags, LegacyStackSynthesizer, DefaultStackSynthesizer, + NestedStack, } from '../lib'; import { Intrinsic } from '../lib/private/intrinsic'; import { resolveReferences } from '../lib/private/refs'; @@ -535,7 +537,69 @@ describe('stack', () => { expect(assembly.getStackArtifact(child1.artifactId).dependencies.map((x: { id: any; }) => x.id)).toEqual([]); expect(assembly.getStackArtifact(child2.artifactId).dependencies.map((x: { id: any; }) => x.id)).toEqual(['ParentChild18FAEF419']); + }); + + test('automatic cross-stack references and manual exports look the same', () => { + // GIVEN: automatic + const appA = new App(); + const producerA = new Stack(appA, 'Producer'); + const consumerA = new Stack(appA, 'Consumer'); + const resourceA = new CfnResource(producerA, 'Resource', { type: 'AWS::Resource' }); + new CfnOutput(consumerA, 'SomeOutput', { value: `${resourceA.getAtt('Att')}` }); + + // GIVEN: manual + const appM = new App(); + const producerM = new Stack(appM, 'Producer'); + const resourceM = new CfnResource(producerM, 'Resource', { type: 'AWS::Resource' }); + producerM.exportValue(resourceM.getAtt('Att')); + + // THEN - producers are the same + const templateA = appA.synth().getStackByName(producerA.stackName).template; + const templateM = appM.synth().getStackByName(producerM.stackName).template; + + expect(templateA).toEqual(templateM); + }); + + test('automatic cross-stack references and manual exports look the same: nested stack edition', () => { + // GIVEN: automatic + const appA = new App(); + const producerA = new Stack(appA, 'Producer'); + const nestedA = new NestedStack(producerA, 'Nestor'); + const resourceA = new CfnResource(nestedA, 'Resource', { type: 'AWS::Resource' }); + + const consumerA = new Stack(appA, 'Consumer'); + new CfnOutput(consumerA, 'SomeOutput', { value: `${resourceA.getAtt('Att')}` }); + + // GIVEN: manual + const appM = new App(); + const producerM = new Stack(appM, 'Producer'); + const nestedM = new NestedStack(producerM, 'Nestor'); + const resourceM = new CfnResource(nestedM, 'Resource', { type: 'AWS::Resource' }); + producerM.exportValue(resourceM.getAtt('Att')); + + // THEN - producers are the same + const templateA = appA.synth().getStackByName(producerA.stackName).template; + const templateM = appM.synth().getStackByName(producerM.stackName).template; + + expect(templateA).toEqual(templateM); + }); + + test('manual exports require a name if not supplying a resource attribute', () => { + const app = new App(); + const stack = new Stack(app, 'Stack'); + + expect(() => { + stack.exportValue('someValue'); + }).toThrow(/or make sure to export a resource attribute/); + }); + + test('manual exports can also just be used to create an export of anything', () => { + const app = new App(); + const stack = new Stack(app, 'Stack'); + + const importV = stack.exportValue('someValue', { name: 'MyExport' }); + expect(stack.resolve(importV)).toEqual({ 'Fn::ImportValue': 'MyExport' }); }); test('CfnSynthesisError is ignored when preparing cross references', () => { From 898acfe16adc981cd4e660ca300bb728bfd6b51b Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Mon, 1 Feb 2021 07:33:04 -0800 Subject: [PATCH 21/33] docs(lambda): recommend NODEJS_12_X instead of NODEJS_10_X (#12713) This PR recommends NODEJS_12_X instead of NODEJS_10_X for deprecated versions of lambda.Runtime
Screenshot ![Screen Shot 2021-01-26 at 11 10 05 AM](https://user-images.githubusercontent.com/16024985/105892863-44109480-5fc7-11eb-9cb2-c91e8e13d9f3.png)
Reasons for recommending NODEJS_12_X: * Node.js 12.x went LTS in October 2019, and has been actively supported till October 2020. * Node.js 10.x is going end-of-life on April 30th, 2021 https://endoflife.date/nodejs ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-lambda/lib/runtime.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-lambda/lib/runtime.ts b/packages/@aws-cdk/aws-lambda/lib/runtime.ts index 1d98212628588..1f94401afa17f 100644 --- a/packages/@aws-cdk/aws-lambda/lib/runtime.ts +++ b/packages/@aws-cdk/aws-lambda/lib/runtime.ts @@ -43,28 +43,28 @@ export class Runtime { /** * The NodeJS runtime (nodejs) * - * @deprecated Use {@link NODEJS_10_X} + * @deprecated Use {@link NODEJS_12_X} */ public static readonly NODEJS = new Runtime('nodejs', RuntimeFamily.NODEJS, { supportsInlineCode: true }); /** * The NodeJS 4.3 runtime (nodejs4.3) * - * @deprecated Use {@link NODEJS_10_X} + * @deprecated Use {@link NODEJS_12_X} */ public static readonly NODEJS_4_3 = new Runtime('nodejs4.3', RuntimeFamily.NODEJS, { supportsInlineCode: true }); /** * The NodeJS 6.10 runtime (nodejs6.10) * - * @deprecated Use {@link NODEJS_10_X} + * @deprecated Use {@link NODEJS_12_X} */ public static readonly NODEJS_6_10 = new Runtime('nodejs6.10', RuntimeFamily.NODEJS, { supportsInlineCode: true }); /** * The NodeJS 8.10 runtime (nodejs8.10) * - * @deprecated Use {@link NODEJS_10_X} + * @deprecated Use {@link NODEJS_12_X} */ public static readonly NODEJS_8_10 = new Runtime('nodejs8.10', RuntimeFamily.NODEJS, { supportsInlineCode: true }); From 40b32bbda272b6e2f92fd5dd8de7ca5bf405ce52 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 1 Feb 2021 17:09:52 +0100 Subject: [PATCH 22/33] fix(cli, codepipeline): renamed bootstrap stack still not supported (#12771) Two mistakes in the previous attempt at fixing this (#12594): * There was a big fat `if (!bootstrapStack.found) { throw; }` line still in the middle of the code path. We had written an integ test to validate that the new situation would work, however the test was incorrect: it would create a non-default bootstrap stack, but if the account already happened to be default-bootstrapped before, the CLI would accidentally find that default bootstrap stack and use it, thereby never triggering the offending line. * The `BootsraplessSynthesizer` set `requiresBootstrapStackVersion`, which is pretty silly. This synthesizer was being used by CodePipeline's cross-region support stacks, so for cross-region deployments we would still require a bootstrap stack. Both of these are fixed and the test has been updated to force the CLI to look up a definitely nonexistent bootstrap stack. Fixes #12732. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-codepipeline/test/cross-env.test.ts | 212 +++++++++--------- .../bootstrapless-synthesizer.ts | 1 - .../lib/api/cloudformation-deployments.ts | 4 - packages/aws-cdk/lib/api/cxapp/exec.ts | 2 +- .../api/cloudformation-deployments.test.ts | 2 +- .../test/integ/cli/bootstrapping.integtest.ts | 5 +- 6 files changed, 114 insertions(+), 112 deletions(-) diff --git a/packages/@aws-cdk/aws-codepipeline/test/cross-env.test.ts b/packages/@aws-cdk/aws-codepipeline/test/cross-env.test.ts index 917f4c833858f..08fd0160b90f4 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/cross-env.test.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/cross-env.test.ts @@ -6,130 +6,136 @@ import * as codepipeline from '../lib'; import { FakeBuildAction } from './fake-build-action'; import { FakeSourceAction } from './fake-source-action'; -let app: App; -let stack: Stack; -let sourceArtifact: codepipeline.Artifact; -let initialStages: codepipeline.StageProps[]; - -beforeEach(() => { - app = new App(); - stack = new Stack(app, 'PipelineStack', { env: { account: '2222', region: 'us-east-1' } }); - sourceArtifact = new codepipeline.Artifact(); - initialStages = [ - { - stageName: 'Source', - actions: [new FakeSourceAction({ - actionName: 'Source', - output: sourceArtifact, - })], - }, - { - stageName: 'Build', - actions: [new FakeBuildAction({ - actionName: 'Build', - input: sourceArtifact, - })], - }, - ]; -}); - -describe('crossAccountKeys=false', () => { - let pipeline: codepipeline.Pipeline; +describe.each(['legacy', 'modern'])('with %s synthesis', (synthesisStyle: string) => { + let app: App; + let stack: Stack; + let sourceArtifact: codepipeline.Artifact; + let initialStages: codepipeline.StageProps[]; + beforeEach(() => { - pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { - crossAccountKeys: false, - stages: initialStages, + app = new App({ + context: { + ...synthesisStyle === 'modern' ? { '@aws-cdk/core:newStyleStackSynthesis': true } : undefined, + }, }); + stack = new Stack(app, 'PipelineStack', { env: { account: '2222', region: 'us-east-1' } }); + sourceArtifact = new codepipeline.Artifact(); + initialStages = [ + { + stageName: 'Source', + actions: [new FakeSourceAction({ + actionName: 'Source', + output: sourceArtifact, + })], + }, + { + stageName: 'Build', + actions: [new FakeBuildAction({ + actionName: 'Build', + input: sourceArtifact, + })], + }, + ]; }); - test('creates a bucket but no keys', () => { - // THEN - expect(stack).not.toHaveResource('AWS::KMS::Key'); - expect(stack).toHaveResource('AWS::S3::Bucket'); - }); - - describe('prevents adding a cross-account action', () => { - const expectedError = 'crossAccountKeys: true'; - - let stage: codepipeline.IStage; + describe('crossAccountKeys=false', () => { + let pipeline: codepipeline.Pipeline; beforeEach(() => { - stage = pipeline.addStage({ stageName: 'Deploy' }); + pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { + crossAccountKeys: false, + stages: initialStages, + }); }); - test('by role', () => { - // WHEN - expect(() => { - stage.addAction(new FakeBuildAction({ - actionName: 'Deploy', - input: sourceArtifact, - role: iam.Role.fromRoleArn(stack, 'Role', 'arn:aws:iam::1111:role/some-role'), - })); - }).toThrow(expectedError); + test('creates a bucket but no keys', () => { + // THEN + expect(stack).not.toHaveResource('AWS::KMS::Key'); + expect(stack).toHaveResource('AWS::S3::Bucket'); }); - test('by resource', () => { - // WHEN - expect(() => { - stage.addAction(new FakeBuildAction({ - actionName: 'Deploy', - input: sourceArtifact, - resource: s3.Bucket.fromBucketAttributes(stack, 'Bucket', { - bucketName: 'foo', + describe('prevents adding a cross-account action', () => { + const expectedError = 'crossAccountKeys: true'; + + let stage: codepipeline.IStage; + beforeEach(() => { + stage = pipeline.addStage({ stageName: 'Deploy' }); + }); + + test('by role', () => { + // WHEN + expect(() => { + stage.addAction(new FakeBuildAction({ + actionName: 'Deploy', + input: sourceArtifact, + role: iam.Role.fromRoleArn(stack, 'Role', 'arn:aws:iam::1111:role/some-role'), + })); + }).toThrow(expectedError); + }); + + test('by resource', () => { + // WHEN + expect(() => { + stage.addAction(new FakeBuildAction({ + actionName: 'Deploy', + input: sourceArtifact, + resource: s3.Bucket.fromBucketAttributes(stack, 'Bucket', { + bucketName: 'foo', + account: '1111', + }), + })); + }).toThrow(expectedError); + }); + + test('by declared account', () => { + // WHEN + expect(() => { + stage.addAction(new FakeBuildAction({ + actionName: 'Deploy', + input: sourceArtifact, account: '1111', - }), - })); - }).toThrow(expectedError); + })); + }).toThrow(expectedError); + }); }); - test('by declared account', () => { - // WHEN - expect(() => { + describe('also affects cross-region support stacks', () => { + let stage: codepipeline.IStage; + beforeEach(() => { + stage = pipeline.addStage({ stageName: 'Deploy' }); + }); + + test('when making a support stack', () => { + // WHEN stage.addAction(new FakeBuildAction({ actionName: 'Deploy', input: sourceArtifact, - account: '1111', + // No resource to grab onto forces creating a fresh support stack + region: 'eu-west-1', })); - }).toThrow(expectedError); - }); - }); - describe('also affects cross-region support stacks', () => { - let stage: codepipeline.IStage; - beforeEach(() => { - stage = pipeline.addStage({ stageName: 'Deploy' }); - }); - - test('when making a support stack', () => { - // WHEN - stage.addAction(new FakeBuildAction({ - actionName: 'Deploy', - input: sourceArtifact, - // No resource to grab onto forces creating a fresh support stack - region: 'eu-west-1', - })); - - // THEN - const asm = app.synth(); - const supportStack = asm.getStack(`${stack.stackName}-support-eu-west-1`); + // THEN + const asm = app.synth(); + const supportStack = asm.getStack(`${stack.stackName}-support-eu-west-1`); - // THEN - expect(supportStack).not.toHaveResource('AWS::KMS::Key'); - expect(supportStack).toHaveResource('AWS::S3::Bucket'); - }); + // THEN + expect(supportStack).not.toHaveResource('AWS::KMS::Key'); + expect(supportStack).toHaveResource('AWS::S3::Bucket'); + }); - test('when twiddling another stack', () => { - const stack2 = new Stack(app, 'Stack2', { env: { account: '2222', region: 'eu-west-1' } }); + test('when twiddling another stack', () => { + const stack2 = new Stack(app, 'Stack2', { env: { account: '2222', region: 'eu-west-1' } }); - // WHEN - stage.addAction(new FakeBuildAction({ - actionName: 'Deploy', - input: sourceArtifact, - resource: new iam.User(stack2, 'DoesntMatterWhatThisIs'), - })); + // WHEN + stage.addAction(new FakeBuildAction({ + actionName: 'Deploy', + input: sourceArtifact, + resource: new iam.User(stack2, 'DoesntMatterWhatThisIs'), + })); - // THEN - expect(stack2).not.toHaveResource('AWS::KMS::Key'); - expect(stack2).toHaveResource('AWS::S3::Bucket'); + // THEN + expect(stack2).not.toHaveResource('AWS::KMS::Key'); + expect(stack2).toHaveResource('AWS::S3::Bucket'); + }); }); }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts index 16ea69c1b2302..1a9a2ab8ee0cc 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts @@ -57,7 +57,6 @@ export class BootstraplessSynthesizer extends DefaultStackSynthesizer { this.emitStackArtifact(this.stack, session, { assumeRoleArn: this.deployRoleArn, cloudFormationExecutionRoleArn: this.cloudFormationExecutionRoleArn, - requiresBootstrapStackVersion: 1, }); } } diff --git a/packages/aws-cdk/lib/api/cloudformation-deployments.ts b/packages/aws-cdk/lib/api/cloudformation-deployments.ts index 2eec90afdb790..3fe5fed118a76 100644 --- a/packages/aws-cdk/lib/api/cloudformation-deployments.ts +++ b/packages/aws-cdk/lib/api/cloudformation-deployments.ts @@ -282,10 +282,6 @@ export class CloudFormationDeployments { if (requiresBootstrapStackVersion === undefined) { return; } - if (!toolkitInfo.found) { - throw new Error(`${stackName}: publishing assets requires bootstrap stack version '${requiresBootstrapStackVersion}', no bootstrap stack found. Please run 'cdk bootstrap'.`); - } - try { await toolkitInfo.validateVersion(requiresBootstrapStackVersion, bootstrapStackVersionSsmParameter); } catch (e) { diff --git a/packages/aws-cdk/lib/api/cxapp/exec.ts b/packages/aws-cdk/lib/api/cxapp/exec.ts index 376ee542f4580..facaf24a3bfe0 100644 --- a/packages/aws-cdk/lib/api/cxapp/exec.ts +++ b/packages/aws-cdk/lib/api/cxapp/exec.ts @@ -92,7 +92,7 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom } async function exec() { - return new Promise((ok, fail) => { + return new Promise((ok, fail) => { // We use a slightly lower-level interface to: // // - Pass arguments in an array instead of a string, to get around a diff --git a/packages/aws-cdk/test/api/cloudformation-deployments.test.ts b/packages/aws-cdk/test/api/cloudformation-deployments.test.ts index 7afea33be6a0b..7a4e29628564c 100644 --- a/packages/aws-cdk/test/api/cloudformation-deployments.test.ts +++ b/packages/aws-cdk/test/api/cloudformation-deployments.test.ts @@ -76,7 +76,7 @@ test('deployment fails if bootstrap stack is missing', async () => { requiresBootstrapStackVersion: 99, }, }), - })).rejects.toThrow(/no bootstrap stack found/); + })).rejects.toThrow(/requires a bootstrap stack/); }); test('deployment fails if bootstrap stack is too old', async () => { diff --git a/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts b/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts index e11e3b48bfcc0..c6ba2e53ec2d8 100644 --- a/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts +++ b/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts @@ -259,8 +259,9 @@ integTest('can deploy modern-synthesized stack even if bootstrap stack name is u // Deploy stack that uses file assets await fixture.cdkDeploy('lambda', { options: [ - // Next line explicitly commented to show that we don't pass it! - // '--toolkit-stack-name', bootstrapStackName, + // Explicity pass a name that's sure to not exist, otherwise the CLI might accidentally find a + // default bootstracp stack if that happens to be in the account already. + '--toolkit-stack-name', 'DefinitelyDoesNotExist', '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, '--context', '@aws-cdk/core:newStyleStackSynthesis=1', ], From 3264a7b9cb4df454b69b5ee60e409cdfbfe8817a Mon Sep 17 00:00:00 2001 From: Clarence Date: Tue, 2 Feb 2021 02:35:15 +0800 Subject: [PATCH 23/33] docs(route53): route53 fromHostedZoneId doc error (#12805) `fromHostedZoneId` is use parameter not object https://github.com/aws/aws-cdk/blob/3b66088010b6f2315a215e92505d5279680f16d4/packages/%40aws-cdk/aws-route53/lib/hosted-zone.ts#L70 So I fix doc ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-route53/README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-route53/README.md b/packages/@aws-cdk/aws-route53/README.md index 49947e4791092..b9f75fb1b9d4e 100644 --- a/packages/@aws-cdk/aws-route53/README.md +++ b/packages/@aws-cdk/aws-route53/README.md @@ -169,9 +169,7 @@ Alternatively, use the `HostedZone.fromHostedZoneId` to import hosted zones if you know the ID and the retrieval for the `zoneName` is undesirable. ```ts -const zone = HostedZone.fromHostedZoneId(this, 'MyZone', { - hostedZoneId: 'ZOJJZC49E0EPZ', -}); +const zone = HostedZone.fromHostedZoneId(this, 'MyZone', 'ZOJJZC49E0EPZ'); ``` ## VPC Endpoint Service Private DNS From 9a81faaafb46512acae917b3374ed1ffb12ba744 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Tue, 2 Feb 2021 06:24:44 +0000 Subject: [PATCH 24/33] chore(eslint-plugin-cdk): tests for rule no-qualified-construct (#12809) Add missing tests ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../eslint-plugin-cdk/test/rules/eslintrc.js | 12 ---- .../test/rules/fixtures.test.ts | 63 +++++++++++++++++++ .../qualified-heritage.expected.ts | 8 +++ .../qualified-heritage.ts | 4 ++ .../qualified-usage.expected.ts | 4 ++ .../no-qualified-construct/qualified-usage.ts | 3 + .../test/rules/no-core-construct.test.ts | 44 ------------- 7 files changed, 82 insertions(+), 56 deletions(-) delete mode 100644 tools/eslint-plugin-cdk/test/rules/eslintrc.js create mode 100644 tools/eslint-plugin-cdk/test/rules/fixtures.test.ts create mode 100644 tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-heritage.expected.ts create mode 100644 tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-heritage.ts create mode 100644 tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-usage.expected.ts create mode 100644 tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-usage.ts delete mode 100644 tools/eslint-plugin-cdk/test/rules/no-core-construct.test.ts diff --git a/tools/eslint-plugin-cdk/test/rules/eslintrc.js b/tools/eslint-plugin-cdk/test/rules/eslintrc.js deleted file mode 100644 index c68b2066acce3..0000000000000 --- a/tools/eslint-plugin-cdk/test/rules/eslintrc.js +++ /dev/null @@ -1,12 +0,0 @@ -const path = require('path'); -const rulesDirPlugin = require('eslint-plugin-rulesdir'); -rulesDirPlugin.RULES_DIR = path.join(__dirname, '../../lib/rules'); - -module.exports = { - parser: '@typescript-eslint/parser', - plugins: ['rulesdir'], - rules: { - quotes: [ 'error', 'single', { avoidEscape: true }], - 'rulesdir/no-core-construct': [ 'error' ], - } -} diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures.test.ts b/tools/eslint-plugin-cdk/test/rules/fixtures.test.ts new file mode 100644 index 0000000000000..89f0568048fbd --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures.test.ts @@ -0,0 +1,63 @@ +import { ESLint } from 'eslint'; +import * as fs from 'fs-extra'; +import * as path from 'path'; + +const rulesDirPlugin = require('eslint-plugin-rulesdir'); +rulesDirPlugin.RULES_DIR = path.join(__dirname, '../../lib/rules'); + +const linter = new ESLint({ + baseConfig: { + parser: '@typescript-eslint/parser', + plugins: ['rulesdir'], + rules: { + quotes: [ 'error', 'single', { avoidEscape: true }], + 'rulesdir/no-core-construct': [ 'error' ], + 'rulesdir/no-qualified-construct': [ 'error' ], + } + }, + rulePaths: [ + path.join(__dirname, '../../lib/rules'), + ], + fix: true, +}); + +const outputRoot = path.join(process.cwd(), '.test-output'); +fs.mkdirpSync(outputRoot); + +const fixturesRoot = path.join(__dirname, 'fixtures'); + +fs.readdirSync(fixturesRoot).filter(f => fs.lstatSync(path.join(fixturesRoot, f)).isDirectory()).forEach(d => { + describe(d, () => { + const outputDir = path.join(outputRoot, d); + fs.mkdirpSync(outputDir); + + const fixturesDir = path.join(fixturesRoot, d); + const fixtureFiles = fs.readdirSync(fixturesDir).filter(f => f.endsWith('.ts') && !f.endsWith('.expected.ts')); + + fixtureFiles.forEach(f => { + test(f, async (done) => { + const actualFile = await lintAndFix(path.join(fixturesDir, f), outputDir); + const expectedFile = path.join(fixturesDir, `${path.basename(f, '.ts')}.expected.ts`); + if (!fs.existsSync(expectedFile)) { + done.fail(`Expected file not found. Generated output at ${actualFile}`); + } + const actual = await fs.readFile(actualFile, { encoding: 'utf8' }); + const expected = await fs.readFile(expectedFile, { encoding: 'utf8' }); + if (actual !== expected) { + done.fail(`Linted file did not match expectations. Expected: ${expectedFile}. Actual: ${actualFile}`); + } + done(); + }); + }); + }); +}); + +async function lintAndFix(file: string, outputDir: string) { + const newPath = path.join(outputDir, path.basename(file)) + let result = await linter.lintFiles(file); + await ESLint.outputFixes(result.map(r => { + r.filePath = newPath; + return r; + })); + return newPath; +} diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-heritage.expected.ts b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-heritage.expected.ts new file mode 100644 index 0000000000000..6510d7dd5542f --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-heritage.expected.ts @@ -0,0 +1,8 @@ +import * as cdk from '@aws-cdk/core'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + +class MyConstruct extends Construct { +} \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-heritage.ts b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-heritage.ts new file mode 100644 index 0000000000000..3f8b877e32c2e --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-heritage.ts @@ -0,0 +1,4 @@ +import * as cdk from '@aws-cdk/core'; + +class MyConstruct extends cdk.Construct { +} \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-usage.expected.ts b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-usage.expected.ts new file mode 100644 index 0000000000000..af7c0f393f307 --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-usage.expected.ts @@ -0,0 +1,4 @@ +import * as cdk from '@aws-cdk/core'; +import * as constructs from 'constructs'; + +let x: constructs.Construct; \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-usage.ts b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-usage.ts new file mode 100644 index 0000000000000..d2ebc12dc01ff --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-usage.ts @@ -0,0 +1,3 @@ +import * as cdk from '@aws-cdk/core'; + +let x: cdk.Construct; \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/no-core-construct.test.ts b/tools/eslint-plugin-cdk/test/rules/no-core-construct.test.ts deleted file mode 100644 index c2272cfd39353..0000000000000 --- a/tools/eslint-plugin-cdk/test/rules/no-core-construct.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ESLint } from 'eslint'; -import * as fs from 'fs-extra'; -import * as path from 'path'; - -const linter = new ESLint({ - overrideConfigFile: path.join(__dirname, 'eslintrc.js'), - rulePaths: [ - path.join(__dirname, '../../lib/rules'), - ], - fix: true, -}); - -const outputDir = path.join(process.cwd(), '.test-output'); -fs.mkdirpSync(outputDir); -const fixturesDir = path.join(__dirname, 'fixtures', 'no-core-construct'); - -describe('no-core-construct', () => { - const fixtureFiles = fs.readdirSync(fixturesDir).filter(f => f.endsWith('.ts') && !f.endsWith('.expected.ts')); - fixtureFiles.forEach(f => { - test(f, async (done) => { - const actualFile = await lintAndFix(path.join(fixturesDir, f)); - const expectedFile = path.join(fixturesDir, `${path.basename(f, '.ts')}.expected.ts`); - if (!fs.existsSync(expectedFile)) { - done.fail(`Expected file not found. Generated output at ${actualFile}`); - } - const actual = await fs.readFile(actualFile, { encoding: 'utf8' }); - const expected = await fs.readFile(expectedFile, { encoding: 'utf8' }); - if (actual !== expected) { - done.fail(`Linted file did not match expectations. Expected: ${expectedFile}. Actual: ${actualFile}`); - } - done(); - }); - }); -}); - -async function lintAndFix(file: string) { - const newPath = path.join(outputDir, path.basename(file)) - let result = await linter.lintFiles(file); - await ESLint.outputFixes(result.map(r => { - r.filePath = newPath; - return r; - })); - return newPath; -} From 721344b592c93094a278ed5525be8cba2a549762 Mon Sep 17 00:00:00 2001 From: Christoph Gysin Date: Tue, 2 Feb 2021 19:05:18 +0200 Subject: [PATCH 25/33] chore(stepfunctions): Fix examples (#12824) fixes #12823 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-stepfunctions/README.md | 71 +++++++++---------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions/README.md b/packages/@aws-cdk/aws-stepfunctions/README.md index 1fb164cc6e8e2..ae6635358d0ec 100644 --- a/packages/@aws-cdk/aws-stepfunctions/README.md +++ b/packages/@aws-cdk/aws-stepfunctions/README.md @@ -84,9 +84,9 @@ definition. The definition is specified by its start state, and encompasses all states reachable from the start state: ```ts -const startState = new stepfunctions.Pass(this, 'StartState'); +const startState = new sfn.Pass(this, 'StartState'); -new stepfunctions.StateMachine(this, 'StateMachine', { +new sfn.StateMachine(this, 'StateMachine', { definition: startState }); ``` @@ -138,8 +138,8 @@ will be passed as the state's output. ```ts // Makes the current JSON state { ..., "subObject": { "hello": "world" } } -const pass = new stepfunctions.Pass(this, 'Add Hello World', { - result: stepfunctions.Result.fromObject({ hello: 'world' }), +const pass = new sfn.Pass(this, 'Add Hello World', { + result: sfn.Result.fromObject({ hello: 'world' }), resultPath: '$.subObject', }); @@ -154,9 +154,9 @@ The following example filters the `greeting` field from the state input and also injects a field called `otherData`. ```ts -const pass = new stepfunctions.Pass(this, 'Filter input and inject data', { +const pass = new sfn.Pass(this, 'Filter input and inject data', { parameters: { // input to the pass state - input: stepfunctions.JsonPath.stringAt('$.input.greeting'), + input: sfn.JsonPath.stringAt('$.input.greeting'), otherData: 'some-extra-stuff' }, }); @@ -177,8 +177,8 @@ state. ```ts // Wait until it's the time mentioned in the the state object's "triggerTime" // field. -const wait = new stepfunctions.Wait(this, 'Wait For Trigger Time', { - time: stepfunctions.WaitTime.timestampPath('$.triggerTime'), +const wait = new sfn.Wait(this, 'Wait For Trigger Time', { + time: sfn.WaitTime.timestampPath('$.triggerTime'), }); // Set the next state @@ -191,11 +191,11 @@ A `Choice` state can take a different path through the workflow based on the values in the execution's JSON state: ```ts -const choice = new stepfunctions.Choice(this, 'Did it work?'); +const choice = new sfn.Choice(this, 'Did it work?'); // Add conditions with .when() -choice.when(stepfunctions.Condition.stringEqual('$.status', 'SUCCESS'), successState); -choice.when(stepfunctions.Condition.numberGreaterThan('$.attempts', 5), failureState); +choice.when(sfn.Condition.stringEquals('$.status', 'SUCCESS'), successState); +choice.when(sfn.Condition.numberGreaterThan('$.attempts', 5), failureState); // Use .otherwise() to indicate what should be done if none of the conditions match choice.otherwise(tryAgainState); @@ -206,9 +206,9 @@ all branches come together and continuing as one (similar to how an `if ... then ... else` works in a programming language), use the `.afterwards()` method: ```ts -const choice = new stepfunctions.Choice(this, 'What color is it?'); -choice.when(stepfunctions.Condition.stringEqual('$.color', 'BLUE'), handleBlueItem); -choice.when(stepfunctions.Condition.stringEqual('$.color', 'RED'), handleRedItem); +const choice = new sfn.Choice(this, 'What color is it?'); +choice.when(sfn.Condition.stringEquals('$.color', 'BLUE'), handleBlueItem); +choice.when(sfn.Condition.stringEquals('$.color', 'RED'), handleRedItem); choice.otherwise(handleOtherItemColor); // Use .afterwards() to join all possible paths back together and continue @@ -275,7 +275,7 @@ A `Parallel` state executes one or more subworkflows in parallel. It can also be used to catch and recover from errors in subworkflows. ```ts -const parallel = new stepfunctions.Parallel(this, 'Do the work in parallel'); +const parallel = new sfn.Parallel(this, 'Do the work in parallel'); // Add branches to be executed in parallel parallel.branch(shipItem); @@ -298,7 +298,7 @@ Reaching a `Succeed` state terminates the state machine execution with a succesful status. ```ts -const success = new stepfunctions.Succeed(this, 'We did it!'); +const success = new sfn.Succeed(this, 'We did it!'); ``` ### Fail @@ -308,7 +308,7 @@ failure status. The fail state should report the reason for the failure. Failures can be caught by encompassing `Parallel` states. ```ts -const success = new stepfunctions.Fail(this, 'Fail', { +const success = new sfn.Fail(this, 'Fail', { error: 'WorkflowFailure', cause: "Something went wrong" }); @@ -323,11 +323,11 @@ While the `Parallel` state executes multiple branches of steps using the same in execute the same steps for multiple entries of an array in the state input. ```ts -const map = new stepfunctions.Map(this, 'Map State', { +const map = new sfn.Map(this, 'Map State', { maxConcurrency: 1, - itemsPath: stepfunctions.JsonPath.stringAt('$.inputForMap') + itemsPath: sfn.JsonPath.stringAt('$.inputForMap') }); -map.iterator(new stepfunctions.Pass(this, 'Pass State')); +map.iterator(new sfn.Pass(this, 'Pass State')); ``` ### Custom State @@ -420,7 +420,7 @@ const definition = step1 .branch(step9.next(step10))) .next(finish); -new stepfunctions.StateMachine(this, 'StateMachine', { +new sfn.StateMachine(this, 'StateMachine', { definition, }); ``` @@ -429,14 +429,13 @@ If you don't like the visual look of starting a chain directly off the first step, you can use `Chain.start`: ```ts -const definition = stepfunctions.Chain +const definition = sfn.Chain .start(step1) .next(step2) .next(step3) // ... ``` - ## State Machine Fragments It is possible to define reusable (or abstracted) mini-state machines by @@ -461,16 +460,16 @@ interface MyJobProps { jobFlavor: string; } -class MyJob extends stepfunctions.StateMachineFragment { - public readonly startState: State; - public readonly endStates: INextable[]; +class MyJob extends sfn.StateMachineFragment { + public readonly startState: sfn.State; + public readonly endStates: sfn.INextable[]; constructor(parent: cdk.Construct, id: string, props: MyJobProps) { super(parent, id); - const first = new stepfunctions.Task(this, 'First', { ... }); + const first = new sfn.Task(this, 'First', { ... }); // ... - const last = new stepfunctions.Task(this, 'Last', { ... }); + const last = new sfn.Task(this, 'Last', { ... }); this.startState = first; this.endStates = [last]; @@ -478,7 +477,7 @@ class MyJob extends stepfunctions.StateMachineFragment { } // Do 3 different variants of MyJob in parallel -new stepfunctions.Parallel(this, 'All jobs') +new sfn.Parallel(this, 'All jobs') .branch(new MyJob(this, 'Quick', { jobFlavor: 'quick' }).prefixStates()) .branch(new MyJob(this, 'Medium', { jobFlavor: 'medium' }).prefixStates()) .branch(new MyJob(this, 'Slow', { jobFlavor: 'slow' }).prefixStates()); @@ -500,7 +499,7 @@ You need the ARN to do so, so if you use Activities be sure to pass the Activity ARN into your worker pool: ```ts -const activity = new stepfunctions.Activity(this, 'Activity'); +const activity = new sfn.Activity(this, 'Activity'); // Read this CloudFormation Output from your application and use it to poll for work on // the activity. @@ -512,7 +511,7 @@ new cdk.CfnOutput(this, 'ActivityArn', { value: activity.activityArn }); Granting IAM permissions to an activity can be achieved by calling the `grant(principal, actions)` API: ```ts -const activity = new stepfunctions.Activity(this, 'Activity'); +const activity = new sfn.Activity(this, 'Activity'); const role = new iam.Role(stack, 'Role', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), @@ -564,11 +563,11 @@ destination LogGroup: ```ts const logGroup = new logs.LogGroup(stack, 'MyLogGroup'); -new stepfunctions.StateMachine(stack, 'MyStateMachine', { - definition: stepfunctions.Chain.start(new stepfunctions.Pass(stack, 'Pass')), +new sfn.StateMachine(stack, 'MyStateMachine', { + definition: sfn.Chain.start(new sfn.Pass(stack, 'Pass')), logs: { destination: logGroup, - level: stepfunctions.LogLevel.ALL, + level: sfn.LogLevel.ALL, } }); ``` @@ -580,8 +579,8 @@ Enable X-Ray tracing for StateMachine: ```ts const logGroup = new logs.LogGroup(stack, 'MyLogGroup'); -new stepfunctions.StateMachine(stack, 'MyStateMachine', { - definition: stepfunctions.Chain.start(new stepfunctions.Pass(stack, 'Pass')), +new sfn.StateMachine(stack, 'MyStateMachine', { + definition: sfn.Chain.start(new sfn.Pass(stack, 'Pass')), tracingEnabled: true }); ``` From 06b6d820b4ad7f913b8538bab63ce3a42af4be8f Mon Sep 17 00:00:00 2001 From: Christoph Gysin Date: Tue, 2 Feb 2021 20:43:45 +0200 Subject: [PATCH 26/33] chore(s3): Remove unused KMS key in example (#12826) fixes #12825 ---- *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 | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/@aws-cdk/aws-s3/README.md b/packages/@aws-cdk/aws-s3/README.md index 1ce00f56ba0a9..12ae88bb02d2e 100644 --- a/packages/@aws-cdk/aws-s3/README.md +++ b/packages/@aws-cdk/aws-s3/README.md @@ -67,8 +67,6 @@ assert(bucket.encryptionKey === myKmsKey); Enable KMS-SSE encryption via [S3 Bucket Keys](https://docs.aws.amazon.com/AmazonS3/latest/dev/bucket-key.html): ```ts -const myKmsKey = new kms.Key(this, 'MyKey'); - const bucket = new Bucket(this, 'MyEncryptedBucket', { encryption: BucketEncryption.KMS, bucketKeyEnabled: true From 0963f786e49fc3cef9cc5c4cde3ecba5540a2749 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Tue, 2 Feb 2021 12:06:21 -0800 Subject: [PATCH 27/33] revert: revert "chore: add new interfaces for Assets (#12700)" (#12832) Needs reverting because of https://github.com/aws/jsii/issues/2256 . This reverts commit 1a9f2a81. ---- *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/fs/options.ts | 1 - .../aws-ecr-assets/lib/image-asset.ts | 19 ++----- packages/@aws-cdk/aws-s3-assets/lib/asset.ts | 4 +- packages/@aws-cdk/core/lib/fs/options.ts | 51 +++++-------------- 4 files changed, 19 insertions(+), 56 deletions(-) diff --git a/packages/@aws-cdk/assets/lib/fs/options.ts b/packages/@aws-cdk/assets/lib/fs/options.ts index 548fa4bda42ee..3ccc107d3700d 100644 --- a/packages/@aws-cdk/assets/lib/fs/options.ts +++ b/packages/@aws-cdk/assets/lib/fs/options.ts @@ -10,7 +10,6 @@ export interface CopyOptions { * A strategy for how to handle symlinks. * * @default Never - * @deprecated use `followSymlinks` instead */ readonly follow?: FollowMode; diff --git a/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts b/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts index 91d3f06b5f6a2..2f6f5ff436baa 100644 --- a/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts +++ b/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts @@ -2,16 +2,14 @@ import * as fs from 'fs'; import * as path from 'path'; import * as assets from '@aws-cdk/assets'; import * as ecr from '@aws-cdk/aws-ecr'; -import { - Annotations, AssetStaging, Construct as CoreConstruct, FeatureFlags, FileFingerprintOptions, IgnoreMode, Stack, SymlinkFollowMode, Token, -} from '@aws-cdk/core'; +import { Annotations, Construct as CoreConstruct, FeatureFlags, IgnoreMode, Stack, Token } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; /** * Options for DockerImageAsset */ -export interface DockerImageAssetOptions extends assets.FingerprintOptions, FileFingerprintOptions { +export interface DockerImageAssetOptions extends assets.FingerprintOptions { /** * ECR repository name * @@ -139,9 +137,8 @@ export class DockerImageAsset extends CoreConstruct implements assets.IAsset { // deletion of the ECR repository the app used). extraHash.version = '1.21.0'; - const staging = new AssetStaging(this, 'Staging', { + const staging = new assets.Staging(this, 'Staging', { ...props, - follow: props.followSymlinks ?? toSymlinkFollow(props.follow), exclude, ignoreMode, sourcePath: dir, @@ -184,13 +181,3 @@ function validateBuildArgs(buildArgs?: { [key: string]: string }) { } } } - -function toSymlinkFollow(follow?: assets.FollowMode): SymlinkFollowMode | undefined { - switch (follow) { - case undefined: return undefined; - case assets.FollowMode.NEVER: return SymlinkFollowMode.NEVER; - case assets.FollowMode.ALWAYS: return SymlinkFollowMode.ALWAYS; - case assets.FollowMode.BLOCK_EXTERNAL: return SymlinkFollowMode.BLOCK_EXTERNAL; - case assets.FollowMode.EXTERNAL: return SymlinkFollowMode.EXTERNAL; - } -} diff --git a/packages/@aws-cdk/aws-s3-assets/lib/asset.ts b/packages/@aws-cdk/aws-s3-assets/lib/asset.ts index d674d083b248b..938778d1381f4 100644 --- a/packages/@aws-cdk/aws-s3-assets/lib/asset.ts +++ b/packages/@aws-cdk/aws-s3-assets/lib/asset.ts @@ -15,7 +15,7 @@ import { Construct as CoreConstruct } from '@aws-cdk/core'; const ARCHIVE_EXTENSIONS = ['.zip', '.jar']; -export interface AssetOptions extends assets.CopyOptions, cdk.FileCopyOptions, cdk.AssetOptions { +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. @@ -128,7 +128,7 @@ export class Asset extends CoreConstruct implements cdk.IAsset { const staging = new cdk.AssetStaging(this, 'Stage', { ...props, sourcePath: path.resolve(props.path), - follow: props.followSymlinks ?? toSymlinkFollow(props.follow), + follow: toSymlinkFollow(props.follow), assetHash: props.assetHash ?? props.sourceHash, }); diff --git a/packages/@aws-cdk/core/lib/fs/options.ts b/packages/@aws-cdk/core/lib/fs/options.ts index baf73bd7ffd30..3ea836a24e831 100644 --- a/packages/@aws-cdk/core/lib/fs/options.ts +++ b/packages/@aws-cdk/core/lib/fs/options.ts @@ -56,9 +56,19 @@ export enum IgnoreMode { * context flag is set. */ DOCKER = 'docker' -} +}; + +/** + * Obtains applied when copying directories into the staging location. + */ +export interface CopyOptions { + /** + * A strategy for how to handle symlinks. + * + * @default SymlinkFollowMode.NEVER + */ + readonly follow?: SymlinkFollowMode; -interface FileOptions { /** * Glob patterns to exclude from the copy. * @@ -75,30 +85,9 @@ interface FileOptions { } /** - * Options applied when copying directories - */ -export interface CopyOptions extends FileOptions { - /** - * A strategy for how to handle symlinks. - * - * @default SymlinkFollowMode.NEVER - */ - readonly follow?: SymlinkFollowMode; -} - -/** - * Options applied when copying directories into the staging location. + * Options related to calculating source hash. */ -export interface FileCopyOptions extends FileOptions { - /** - * A strategy for how to handle symlinks. - * - * @default SymlinkFollowMode.NEVER - */ - readonly followSymlinks?: SymlinkFollowMode; -} - -interface ExtraHashOptions { +export interface FingerprintOptions extends CopyOptions { /** * Extra information to encode into the fingerprint (e.g. build instructions * and other inputs) @@ -107,15 +96,3 @@ interface ExtraHashOptions { */ readonly extraHash?: string; } - -/** - * Options related to calculating source hash. - */ -export interface FingerprintOptions extends CopyOptions, ExtraHashOptions { -} - -/** - * Options related to calculating source hash. - */ -export interface FileFingerprintOptions extends FileCopyOptions, ExtraHashOptions { -} From 59cb6d032a55515ec5e9903f899de588d18d4cb5 Mon Sep 17 00:00:00 2001 From: Robert Djurasaj Date: Wed, 3 Feb 2021 03:02:18 -0700 Subject: [PATCH 28/33] feat(cloudfront): add PublicKey and KeyGroup L2 constructs (#12743) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @njlynch This is my humble start on creating L2 constructs for `PublicKey` and `KeyGroup` for CloudFront module. I'm going to need some guidance/mentorship as this is my first L2 construct from the scratch. I'll convert this PR to draft and I'll post some of my thoughts and ideas around this feature tomorrow. I'm trying to address feature requests in https://github.com/aws/aws-cdk/issues/11791. I've decided to lump `PublicKey` and `KeyGroup` features together as they seem to depend on each other. All in the good spirits of learning how to extend CDK 🍻 . Any ideas and/or constructive criticism is more than welcome... that's the best way to learn.✌️ ---- *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-cloudfront/README.md | 37 ++++ packages/@aws-cdk/aws-cloudfront/lib/index.ts | 2 + .../@aws-cdk/aws-cloudfront/lib/key-group.ts | 75 +++++++ .../@aws-cdk/aws-cloudfront/lib/public-key.ts | 83 ++++++++ packages/@aws-cdk/aws-cloudfront/package.json | 4 +- .../integ.cloudfront-key-group.expected.json | 27 +++ .../test/integ.cloudfront-key-group.ts | 25 +++ .../aws-cloudfront/test/key-group.test.ts | 187 ++++++++++++++++++ .../aws-cloudfront/test/public-key.test.ts | 85 ++++++++ 9 files changed, 524 insertions(+), 1 deletion(-) create mode 100644 packages/@aws-cdk/aws-cloudfront/lib/key-group.ts create mode 100644 packages/@aws-cdk/aws-cloudfront/lib/public-key.ts create mode 100644 packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-key-group.expected.json create mode 100644 packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-key-group.ts create mode 100644 packages/@aws-cdk/aws-cloudfront/test/key-group.test.ts create mode 100644 packages/@aws-cdk/aws-cloudfront/test/public-key.test.ts diff --git a/packages/@aws-cdk/aws-cloudfront/README.md b/packages/@aws-cdk/aws-cloudfront/README.md index f42d6a15f7abc..1355d7a64d31d 100644 --- a/packages/@aws-cdk/aws-cloudfront/README.md +++ b/packages/@aws-cdk/aws-cloudfront/README.md @@ -520,3 +520,40 @@ new CloudFrontWebDistribution(stack, 'ADistribution', { ], }); ``` + +## KeyGroup & PublicKey API + +Now you can create a key group to use with CloudFront signed URLs and signed cookies. You can add public keys to use with CloudFront features such as signed URLs, signed cookies, and field-level encryption. + +The following example command uses OpenSSL to generate an RSA key pair with a length of 2048 bits and save to the file named `private_key.pem`. + +```bash +openssl genrsa -out private_key.pem 2048 +``` + +The resulting file contains both the public and the private key. The following example command extracts the public key from the file named `private_key.pem` and stores it in `public_key.pem`. + +```bash +openssl rsa -pubout -in private_key.pem -out public_key.pem +``` + +Note: Don't forget to copy/paste the contents of `public_key.pem` file including `-----BEGIN PUBLIC KEY-----` and `-----END PUBLIC KEY-----` lines into `encodedKey` parameter when creating a `PublicKey`. + +Example: + +```ts + new cloudfront.KeyGroup(stack, 'MyKeyGroup', { + items: [ + new cloudfront.PublicKey(stack, 'MyPublicKey', { + encodedKey: '...', // contents of public_key.pem file + // comment: 'Key is expiring on ...', + }), + ], + // comment: 'Key group containing public keys ...', + }); +``` + +See: + +* https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html +* https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-trusted-signers.html diff --git a/packages/@aws-cdk/aws-cloudfront/lib/index.ts b/packages/@aws-cdk/aws-cloudfront/lib/index.ts index 726a1d1d01948..7de2aa62b4412 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/index.ts @@ -1,9 +1,11 @@ export * from './cache-policy'; export * from './distribution'; export * from './geo-restriction'; +export * from './key-group'; export * from './origin'; export * from './origin-access-identity'; export * from './origin-request-policy'; +export * from './public-key'; export * from './web-distribution'; export * as experimental from './experimental'; diff --git a/packages/@aws-cdk/aws-cloudfront/lib/key-group.ts b/packages/@aws-cdk/aws-cloudfront/lib/key-group.ts new file mode 100644 index 0000000000000..aea7bf451f305 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/lib/key-group.ts @@ -0,0 +1,75 @@ +import { IResource, Names, Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnKeyGroup } from './cloudfront.generated'; +import { IPublicKey } from './public-key'; + +/** + * Represents a Key Group + */ +export interface IKeyGroup extends IResource { + /** + * The ID of the key group. + * @attribute + */ + readonly keyGroupId: string; +} + +/** + * Properties for creating a Public Key + */ +export interface KeyGroupProps { + /** + * A name to identify the key group. + * @default - generated from the `id` + */ + readonly keyGroupName?: string; + + /** + * A comment to describe the key group. + * @default - no comment + */ + readonly comment?: string; + + /** + * A list of public keys to add to the key group. + */ + readonly items: IPublicKey[]; +} + +/** + * A Key Group configuration + * + * @resource AWS::CloudFront::KeyGroup + */ +export class KeyGroup extends Resource implements IKeyGroup { + + /** Imports a Key Group from its id. */ + public static fromKeyGroupId(scope: Construct, id: string, keyGroupId: string): IKeyGroup { + return new class extends Resource implements IKeyGroup { + public readonly keyGroupId = keyGroupId; + }(scope, id); + } + public readonly keyGroupId: string; + + constructor(scope: Construct, id: string, props: KeyGroupProps) { + super(scope, id); + + const resource = new CfnKeyGroup(this, 'Resource', { + keyGroupConfig: { + name: props.keyGroupName ?? this.generateName(), + comment: props.comment, + items: props.items.map(key => key.publicKeyId), + }, + }); + + this.keyGroupId = resource.ref; + } + + private generateName(): string { + const name = Names.uniqueId(this); + if (name.length > 80) { + return name.substring(0, 40) + name.substring(name.length - 40); + } + return name; + } +} diff --git a/packages/@aws-cdk/aws-cloudfront/lib/public-key.ts b/packages/@aws-cdk/aws-cloudfront/lib/public-key.ts new file mode 100644 index 0000000000000..e2c2b6e044cdb --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/lib/public-key.ts @@ -0,0 +1,83 @@ +import { IResource, Names, Resource, Token } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnPublicKey } from './cloudfront.generated'; + +/** + * Represents a Public Key + */ +export interface IPublicKey extends IResource { + /** + * The ID of the key group. + * @attribute + */ + readonly publicKeyId: string; +} + +/** + * Properties for creating a Public Key + */ +export interface PublicKeyProps { + /** + * A name to identify the public key. + * @default - generated from the `id` + */ + readonly publicKeyName?: string; + + /** + * A comment to describe the public key. + * @default - no comment + */ + readonly comment?: string; + + /** + * The public key that you can use with signed URLs and signed cookies, or with field-level encryption. + * The `encodedKey` parameter must include `-----BEGIN PUBLIC KEY-----` and `-----END PUBLIC KEY-----` lines. + * @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html + * @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/field-level-encryption.html + */ + readonly encodedKey: string; +} + +/** + * A Public Key Configuration + * + * @resource AWS::CloudFront::PublicKey + */ +export class PublicKey extends Resource implements IPublicKey { + + /** Imports a Public Key from its id. */ + public static fromPublicKeyId(scope: Construct, id: string, publicKeyId: string): IPublicKey { + return new class extends Resource implements IPublicKey { + public readonly publicKeyId = publicKeyId; + }(scope, id); + } + + public readonly publicKeyId: string; + + constructor(scope: Construct, id: string, props: PublicKeyProps) { + super(scope, id); + + if (!Token.isUnresolved(props.encodedKey) && !/^-----BEGIN PUBLIC KEY-----/.test(props.encodedKey)) { + throw new Error(`Public key must be in PEM format (with the BEGIN/END PUBLIC KEY lines); got ${props.encodedKey}`); + } + + const resource = new CfnPublicKey(this, 'Resource', { + publicKeyConfig: { + name: props.publicKeyName ?? this.generateName(), + callerReference: this.node.addr, + encodedKey: props.encodedKey, + comment: props.comment, + }, + }); + + this.publicKeyId = resource.ref; + } + + private generateName(): string { + const name = Names.uniqueId(this); + if (name.length > 80) { + return name.substring(0, 40) + name.substring(name.length - 40); + } + return name; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/package.json b/packages/@aws-cdk/aws-cloudfront/package.json index 651d22e95e668..c781a065bbc0a 100644 --- a/packages/@aws-cdk/aws-cloudfront/package.json +++ b/packages/@aws-cdk/aws-cloudfront/package.json @@ -153,7 +153,9 @@ "resource-attribute:@aws-cdk/aws-cloudfront.CachePolicy.cachePolicyLastModifiedTime", "construct-interface-extends-iconstruct:@aws-cdk/aws-cloudfront.IOriginRequestPolicy", "resource-interface-extends-resource:@aws-cdk/aws-cloudfront.IOriginRequestPolicy", - "resource-attribute:@aws-cdk/aws-cloudfront.OriginRequestPolicy.originRequestPolicyLastModifiedTime" + "resource-attribute:@aws-cdk/aws-cloudfront.OriginRequestPolicy.originRequestPolicyLastModifiedTime", + "resource-attribute:@aws-cdk/aws-cloudfront.KeyGroup.keyGroupLastModifiedTime", + "resource-attribute:@aws-cdk/aws-cloudfront.PublicKey.publicKeyCreatedTime" ] }, "awscdkio": { diff --git a/packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-key-group.expected.json b/packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-key-group.expected.json new file mode 100644 index 0000000000000..45191bad86cff --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-key-group.expected.json @@ -0,0 +1,27 @@ +{ + "Resources": { + "AwesomePublicKeyED3E7F55": { + "Type": "AWS::CloudFront::PublicKey", + "Properties": { + "PublicKeyConfig": { + "CallerReference": "c88e460888c5762c9c47ac0cdc669370d787fb2d9f", + "EncodedKey": "-----BEGIN PUBLIC KEY-----\n MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAudf8/iNkQgdvjEdm6xYS\n JAyxd/kGTbJfQNg9YhInb7TSm0dGu0yx8yZ3fnpmxuRPqJIlaVr+fT4YRl71gEYa\n dlhHmnVegyPNjP9dNqZ7zwNqMEPOPnS/NOHbJj1KYKpn1f8pPNycQ5MQCntKGnSj\n 6fc+nbcC0joDvGz80xuy1W4hLV9oC9c3GT26xfZb2jy9MVtA3cppNuTwqrFi3t6e\n 0iGpraxZlT5wewjZLpQkngqYr6s3aucPAZVsGTEYPo4nD5mswmtZOm+tgcOrivtD\n /3sD/qZLQ6c5siqyS8aTraD6y+VXugujfarTU65IeZ6QAUbLMsWuZOIi5Jn8zAwx\n NQIDAQAB\n -----END PUBLIC KEY-----\n ", + "Name": "awscdkcloudfrontcustomAwesomePublicKey0E83393B" + } + } + }, + "AwesomeKeyGroup3EF8348B": { + "Type": "AWS::CloudFront::KeyGroup", + "Properties": { + "KeyGroupConfig": { + "Items": [ + { + "Ref": "AwesomePublicKeyED3E7F55" + } + ], + "Name": "awscdkcloudfrontcustomAwesomeKeyGroup73FD4DCA" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-key-group.ts b/packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-key-group.ts new file mode 100644 index 0000000000000..7bfdbbe645446 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-key-group.ts @@ -0,0 +1,25 @@ +import * as cdk from '@aws-cdk/core'; +import * as cloudfront from '../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-cloudfront-custom'); + +new cloudfront.KeyGroup(stack, 'AwesomeKeyGroup', { + items: [ + new cloudfront.PublicKey(stack, 'AwesomePublicKey', { + encodedKey: `-----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAudf8/iNkQgdvjEdm6xYS + JAyxd/kGTbJfQNg9YhInb7TSm0dGu0yx8yZ3fnpmxuRPqJIlaVr+fT4YRl71gEYa + dlhHmnVegyPNjP9dNqZ7zwNqMEPOPnS/NOHbJj1KYKpn1f8pPNycQ5MQCntKGnSj + 6fc+nbcC0joDvGz80xuy1W4hLV9oC9c3GT26xfZb2jy9MVtA3cppNuTwqrFi3t6e + 0iGpraxZlT5wewjZLpQkngqYr6s3aucPAZVsGTEYPo4nD5mswmtZOm+tgcOrivtD + /3sD/qZLQ6c5siqyS8aTraD6y+VXugujfarTU65IeZ6QAUbLMsWuZOIi5Jn8zAwx + NQIDAQAB + -----END PUBLIC KEY----- + `, + }), + ], +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-cloudfront/test/key-group.test.ts b/packages/@aws-cdk/aws-cloudfront/test/key-group.test.ts new file mode 100644 index 0000000000000..f5a0ae43c0855 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/test/key-group.test.ts @@ -0,0 +1,187 @@ +import '@aws-cdk/assert/jest'; +import { expect as expectStack } from '@aws-cdk/assert'; +import { App, Stack } from '@aws-cdk/core'; +import { KeyGroup, PublicKey } from '../lib'; + +const publicKey1 = `-----BEGIN PUBLIC KEY----- +FIRST_KEYgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAudf8/iNkQgdvjEdm6xYS +JAyxd/kGTbJfQNg9YhInb7TSm0dGu0yx8yZ3fnpmxuRPqJIlaVr+fT4YRl71gEYa +dlhHmnVegyPNjP9dNqZ7zwNqMEPOPnS/NOHbJj1KYKpn1f8pPNycQ5MQCntKGnSj +6fc+nbcC0joDvGz80xuy1W4hLV9oC9c3GT26xfZb2jy9MVtA3cppNuTwqrFi3t6e +0iGpraxZlT5wewjZLpQkngqYr6s3aucPAZVsGTEYPo4nD5mswmtZOm+tgcOrivtD +/3sD/qZLQ6c5siqyS8aTraD6y+VXugujfarTU65IeZ6QAUbLMsWuZOIi5Jn8zAwx +NQIDAQAB +-----END PUBLIC KEY-----`; + +const publicKey2 = `-----BEGIN PUBLIC KEY----- +SECOND_KEYkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAudf8/iNkQgdvjEdm6xYS +JAyxd/kGTbJfQNg9YhInb7TSm0dGu0yx8yZ3fnpmxuRPqJIlaVr+fT4YRl71gEYa +dlhHmnVegyPNjP9dNqZ7zwNqMEPOPnS/NOHbJj1KYKpn1f8pPNycQ5MQCntKGnSj +6fc+nbcC0joDvGz80xuy1W4hLV9oC9c3GT26xfZb2jy9MVtA3cppNuTwqrFi3t6e +0iGpraxZlT5wewjZLpQkngqYr6s3aucPAZVsGTEYPo4nD5mswmtZOm+tgcOrivtD +/3sD/qZLQ6c5siqyS8aTraD6y+VXugujfarTU65IeZ6QAUbLMsWuZOIi5Jn8zAwx +NQIDAQAB +-----END PUBLIC KEY-----`; + +describe('KeyGroup', () => { + let app: App; + let stack: Stack; + + beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack', { + env: { account: '123456789012', region: 'testregion' }, + }); + }); + + test('import existing key group by id', () => { + const keyGroupId = '344f6fe5-7ce5-4df0-a470-3f14177c549c'; + const keyGroup = KeyGroup.fromKeyGroupId(stack, 'MyKeyGroup', keyGroupId); + expect(keyGroup.keyGroupId).toEqual(keyGroupId); + }); + + test('minimal example', () => { + new KeyGroup(stack, 'MyKeyGroup', { + items: [ + new PublicKey(stack, 'MyPublicKey', { + encodedKey: publicKey1, + }), + ], + }); + + expectStack(stack).toMatch({ + Resources: { + MyPublicKey78071F3D: { + Type: 'AWS::CloudFront::PublicKey', + Properties: { + PublicKeyConfig: { + CallerReference: 'c872d91ae0d2943aad25d4b31f1304d0a62c658ace', + EncodedKey: publicKey1, + Name: 'StackMyPublicKey36EDA6AB', + }, + }, + }, + MyKeyGroupAF22FD35: { + Type: 'AWS::CloudFront::KeyGroup', + Properties: { + KeyGroupConfig: { + Items: [ + { + Ref: 'MyPublicKey78071F3D', + }, + ], + Name: 'StackMyKeyGroupC9D82374', + }, + }, + }, + }, + }); + }); + + test('maximum example', () => { + new KeyGroup(stack, 'MyKeyGroup', { + keyGroupName: 'AcmeKeyGroup', + comment: 'Key group created on 1/1/1984', + items: [ + new PublicKey(stack, 'MyPublicKey', { + publicKeyName: 'pub-key', + encodedKey: publicKey1, + comment: 'Key expiring on 1/1/1984', + }), + ], + }); + + expectStack(stack).toMatch({ + Resources: { + MyPublicKey78071F3D: { + Type: 'AWS::CloudFront::PublicKey', + Properties: { + PublicKeyConfig: { + CallerReference: 'c872d91ae0d2943aad25d4b31f1304d0a62c658ace', + EncodedKey: publicKey1, + Name: 'pub-key', + Comment: 'Key expiring on 1/1/1984', + }, + }, + }, + MyKeyGroupAF22FD35: { + Type: 'AWS::CloudFront::KeyGroup', + Properties: { + KeyGroupConfig: { + Items: [ + { + Ref: 'MyPublicKey78071F3D', + }, + ], + Name: 'AcmeKeyGroup', + Comment: 'Key group created on 1/1/1984', + }, + }, + }, + }, + }); + }); + + test('multiple keys example', () => { + new KeyGroup(stack, 'MyKeyGroup', { + keyGroupName: 'AcmeKeyGroup', + comment: 'Key group created on 1/1/1984', + items: [ + new PublicKey(stack, 'BingoKey', { + publicKeyName: 'Bingo-Key', + encodedKey: publicKey1, + comment: 'Key expiring on 1/1/1984', + }), + new PublicKey(stack, 'RollyKey', { + publicKeyName: 'Rolly-Key', + encodedKey: publicKey2, + comment: 'Key expiring on 1/1/1984', + }), + ], + }); + + expectStack(stack).toMatch({ + Resources: { + BingoKeyCBEC786C: { + Type: 'AWS::CloudFront::PublicKey', + Properties: { + PublicKeyConfig: { + CallerReference: 'c847cb3dc23f619c0a1e400a44afaf1060d27a1d1a', + EncodedKey: publicKey1, + Name: 'Bingo-Key', + Comment: 'Key expiring on 1/1/1984', + }, + }, + }, + RollyKey83F8BC5B: { + Type: 'AWS::CloudFront::PublicKey', + Properties: { + PublicKeyConfig: { + CallerReference: 'c83a16945c386bf6cd88a3aaa1aa603eeb4b6c6c57', + EncodedKey: publicKey2, + Name: 'Rolly-Key', + Comment: 'Key expiring on 1/1/1984', + }, + }, + }, + MyKeyGroupAF22FD35: { + Type: 'AWS::CloudFront::KeyGroup', + Properties: { + KeyGroupConfig: { + Items: [ + { + Ref: 'BingoKeyCBEC786C', + }, + { + Ref: 'RollyKey83F8BC5B', + }, + ], + Name: 'AcmeKeyGroup', + Comment: 'Key group created on 1/1/1984', + }, + }, + }, + }, + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/test/public-key.test.ts b/packages/@aws-cdk/aws-cloudfront/test/public-key.test.ts new file mode 100644 index 0000000000000..934b1a9dc8107 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/test/public-key.test.ts @@ -0,0 +1,85 @@ +import '@aws-cdk/assert/jest'; +import { expect as expectStack } from '@aws-cdk/assert'; +import { App, Stack } from '@aws-cdk/core'; +import { PublicKey } from '../lib'; + +const publicKey = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAudf8/iNkQgdvjEdm6xYS +JAyxd/kGTbJfQNg9YhInb7TSm0dGu0yx8yZ3fnpmxuRPqJIlaVr+fT4YRl71gEYa +dlhHmnVegyPNjP9dNqZ7zwNqMEPOPnS/NOHbJj1KYKpn1f8pPNycQ5MQCntKGnSj +6fc+nbcC0joDvGz80xuy1W4hLV9oC9c3GT26xfZb2jy9MVtA3cppNuTwqrFi3t6e +0iGpraxZlT5wewjZLpQkngqYr6s3aucPAZVsGTEYPo4nD5mswmtZOm+tgcOrivtD +/3sD/qZLQ6c5siqyS8aTraD6y+VXugujfarTU65IeZ6QAUbLMsWuZOIi5Jn8zAwx +NQIDAQAB +-----END PUBLIC KEY-----`; + +describe('PublicKey', () => { + let app: App; + let stack: Stack; + + beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack', { + env: { account: '123456789012', region: 'testregion' }, + }); + }); + + test('import existing key group by id', () => { + const publicKeyId = 'K36X4X2EO997HM'; + const pubKey = PublicKey.fromPublicKeyId(stack, 'MyPublicKey', publicKeyId); + expect(pubKey.publicKeyId).toEqual(publicKeyId); + }); + + test('minimal example', () => { + new PublicKey(stack, 'MyPublicKey', { + encodedKey: publicKey, + }); + + expectStack(stack).toMatch({ + Resources: { + MyPublicKey78071F3D: { + Type: 'AWS::CloudFront::PublicKey', + Properties: { + PublicKeyConfig: { + CallerReference: 'c872d91ae0d2943aad25d4b31f1304d0a62c658ace', + EncodedKey: publicKey, + Name: 'StackMyPublicKey36EDA6AB', + }, + }, + }, + }, + }); + }); + + test('maximum example', () => { + new PublicKey(stack, 'MyPublicKey', { + publicKeyName: 'pub-key', + encodedKey: publicKey, + comment: 'Key expiring on 1/1/1984', + }); + + expectStack(stack).toMatch({ + Resources: { + MyPublicKey78071F3D: { + Type: 'AWS::CloudFront::PublicKey', + Properties: { + PublicKeyConfig: { + CallerReference: 'c872d91ae0d2943aad25d4b31f1304d0a62c658ace', + Comment: 'Key expiring on 1/1/1984', + EncodedKey: publicKey, + Name: 'pub-key', + }, + }, + }, + }, + }); + }); + + test('bad key example', () => { + expect(() => new PublicKey(stack, 'MyPublicKey', { + publicKeyName: 'pub-key', + encodedKey: 'bad-key', + comment: 'Key expiring on 1/1/1984', + })).toThrow(/Public key must be in PEM format [(]with the BEGIN\/END PUBLIC KEY lines[)]; got (.*?)/); + }); +}); \ No newline at end of file From 8a1a9b82a36e681334fd45be595f6ecdf904ad34 Mon Sep 17 00:00:00 2001 From: Ayush Goyal Date: Wed, 3 Feb 2021 17:04:11 +0530 Subject: [PATCH 29/33] feat(apigateway): import an existing Resource (#12785) feat(apigateway): add fromResourceAttribute helper for importing Resource closes #4432 NOTE: No change in Readme is done since I was not able to find a good place for it in the Readme. ---- *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/resource.ts | 44 +++++++++++++++++++ .../aws-apigateway/test/resource.test.ts | 21 +++++++++ 2 files changed, 65 insertions(+) diff --git a/packages/@aws-cdk/aws-apigateway/lib/resource.ts b/packages/@aws-cdk/aws-apigateway/lib/resource.ts index 04d9598303ad8..33ec6f1d5f7fd 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/resource.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/resource.ts @@ -373,7 +373,51 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc } } +/** + * Attributes that can be specified when importing a Resource + */ +export interface ResourceAttributes { + /** + * The ID of the resource. + */ + readonly resourceId: string; + + /** + * The rest API that this resource is part of. + */ + readonly restApi: IRestApi; + + /** + * The full path of this resource. + */ + readonly path: string; +} + export class Resource extends ResourceBase { + /** + * Import an existing resource + */ + public static fromResourceAttributes(scope: Construct, id: string, attrs: ResourceAttributes): IResource { + class Import extends ResourceBase { + public readonly api = attrs.restApi; + public readonly resourceId = attrs.resourceId; + public readonly path = attrs.path; + public readonly defaultIntegration?: Integration = undefined; + public readonly defaultMethodOptions?: MethodOptions = undefined; + public readonly defaultCorsPreflightOptions?: CorsOptions = undefined; + + public get parentResource(): IResource { + throw new Error('parentResource is not configured for imported resource.'); + } + + public get restApi(): RestApi { + throw new Error('restApi is not configured for imported resource.'); + } + } + + return new Import(scope, id); + } + public readonly parentResource?: IResource; public readonly api: IRestApi; public readonly resourceId: string; diff --git a/packages/@aws-cdk/aws-apigateway/test/resource.test.ts b/packages/@aws-cdk/aws-apigateway/test/resource.test.ts index ecad61cb1905c..0a20483f4261c 100644 --- a/packages/@aws-cdk/aws-apigateway/test/resource.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/resource.test.ts @@ -236,6 +236,27 @@ describe('resource', () => { }); + test('fromResourceAttributes()', () => { + // GIVEN + const stack = new Stack(); + const resourceId = 'resource-id'; + const api = new apigw.RestApi(stack, 'MyRestApi'); + + // WHEN + const imported = apigw.Resource.fromResourceAttributes(stack, 'imported-resource', { + resourceId, + restApi: api, + path: 'some-path', + }); + imported.addMethod('GET'); + + // THEN + expect(stack).toHaveResource('AWS::ApiGateway::Method', { + HttpMethod: 'GET', + ResourceId: resourceId, + }); + }); + describe('getResource', () => { describe('root resource', () => { From 8d3aabaffe436e6a3eebc0a58fe361c5b4b93f08 Mon Sep 17 00:00:00 2001 From: Christoph Gysin Date: Wed, 3 Feb 2021 14:13:05 +0200 Subject: [PATCH 30/33] feat(lambda): inline code for Python 3.8 (#12788) fixes #6503 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-lambda/lib/runtime.ts | 1 + .../integ.runtime.inlinecode.expected.json | 65 +++++++++++++++++-- .../test/integ.runtime.inlinecode.ts | 9 ++- 3 files changed, 69 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-lambda/lib/runtime.ts b/packages/@aws-cdk/aws-lambda/lib/runtime.ts index 1f94401afa17f..05d4996ff5e4f 100644 --- a/packages/@aws-cdk/aws-lambda/lib/runtime.ts +++ b/packages/@aws-cdk/aws-lambda/lib/runtime.ts @@ -103,6 +103,7 @@ export class Runtime { * The Python 3.8 runtime (python3.8) */ public static readonly PYTHON_3_8 = new Runtime('python3.8', RuntimeFamily.PYTHON, { + supportsInlineCode: true, supportsCodeGuruProfiling: true, }); diff --git a/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.expected.json index 8bbe8cdef572a..30d39828cc39d 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.expected.json +++ b/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.expected.json @@ -37,13 +37,13 @@ "Code": { "ZipFile": "exports.handler = async function(event) { return \"success\" }" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "NODEJS10XServiceRole2FD24B65", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -87,13 +87,13 @@ "Code": { "ZipFile": "exports.handler = async function(event) { return \"success\" }" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "NODEJS12XServiceRole59E71436", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs12.x" }, "DependsOn": [ @@ -137,13 +137,13 @@ "Code": { "ZipFile": "def handler(event, context):\n return \"success\"" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "PYTHON27ServiceRoleF484A17D", "Arn" ] }, + "Handler": "index.handler", "Runtime": "python2.7" }, "DependsOn": [ @@ -187,13 +187,13 @@ "Code": { "ZipFile": "def handler(event, context):\n return \"success\"" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "PYTHON36ServiceRole814B3AD9", "Arn" ] }, + "Handler": "index.handler", "Runtime": "python3.6" }, "DependsOn": [ @@ -237,18 +237,68 @@ "Code": { "ZipFile": "def handler(event, context):\n return \"success\"" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "PYTHON37ServiceRoleDE7E561E", "Arn" ] }, + "Handler": "index.handler", "Runtime": "python3.7" }, "DependsOn": [ "PYTHON37ServiceRoleDE7E561E" ] + }, + "PYTHON38ServiceRole3EA86BBE": { + "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" + ] + ] + } + ] + } + }, + "PYTHON38A180AE47": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "def handler(event, context):\n return \"success\"" + }, + "Role": { + "Fn::GetAtt": [ + "PYTHON38ServiceRole3EA86BBE", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "python3.8" + }, + "DependsOn": [ + "PYTHON38ServiceRole3EA86BBE" + ] } }, "Outputs": { @@ -276,6 +326,11 @@ "Value": { "Ref": "PYTHON37D3A10E04" } + }, + "PYTHON38functionName": { + "Value": { + "Ref": "PYTHON38A180AE47" + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.ts b/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.ts index aa4ef06e6a5e1..56f5bd27f7746 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.ts +++ b/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.ts @@ -50,4 +50,11 @@ const python37 = new Function(stack, 'PYTHON_3_7', { }); new CfnOutput(stack, 'PYTHON_3_7-functionName', { value: python37.functionName }); -app.synth(); \ No newline at end of file +const python38 = new Function(stack, 'PYTHON_3_8', { + code: new InlineCode('def handler(event, context):\n return "success"'), + handler: 'index.handler', + runtime: Runtime.PYTHON_3_8, +}); +new CfnOutput(stack, 'PYTHON_3_8-functionName', { value: python38.functionName }); + +app.synth(); From 2aba609929de3b4517795aa06129b2fe31bf11b6 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Wed, 3 Feb 2021 12:50:28 +0000 Subject: [PATCH 31/33] chore(eslint-plugin-cdk): fix tests and expectations (#12831) The previous change - 9a81faaa - to add test cases to this package had a bug. Two different eslint rules were being applied simultaneously creating corrupt expectation. Fixing the test so it only runs the specific eslint rule for that fixture, and fixing the expectation. At the same time, added a test case that was previously missed. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../test/rules/fixtures.test.ts | 32 +++++++++---------- .../fixtures/no-core-construct/eslintrc.js | 6 ++++ .../both-constructs.expected.ts | 9 ++++++ .../no-qualified-construct/both-constructs.ts | 5 +++ .../no-qualified-construct/eslintrc.js | 6 ++++ .../qualified-usage.expected.ts | 7 ++-- 6 files changed, 47 insertions(+), 18 deletions(-) create mode 100644 tools/eslint-plugin-cdk/test/rules/fixtures/no-core-construct/eslintrc.js create mode 100644 tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/both-constructs.expected.ts create mode 100644 tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/both-constructs.ts create mode 100644 tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/eslintrc.js diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures.test.ts b/tools/eslint-plugin-cdk/test/rules/fixtures.test.ts index 89f0568048fbd..b65670f43b429 100644 --- a/tools/eslint-plugin-cdk/test/rules/fixtures.test.ts +++ b/tools/eslint-plugin-cdk/test/rules/fixtures.test.ts @@ -5,21 +5,7 @@ import * as path from 'path'; const rulesDirPlugin = require('eslint-plugin-rulesdir'); rulesDirPlugin.RULES_DIR = path.join(__dirname, '../../lib/rules'); -const linter = new ESLint({ - baseConfig: { - parser: '@typescript-eslint/parser', - plugins: ['rulesdir'], - rules: { - quotes: [ 'error', 'single', { avoidEscape: true }], - 'rulesdir/no-core-construct': [ 'error' ], - 'rulesdir/no-qualified-construct': [ 'error' ], - } - }, - rulePaths: [ - path.join(__dirname, '../../lib/rules'), - ], - fix: true, -}); +let linter: ESLint; const outputRoot = path.join(process.cwd(), '.test-output'); fs.mkdirpSync(outputRoot); @@ -28,10 +14,24 @@ const fixturesRoot = path.join(__dirname, 'fixtures'); fs.readdirSync(fixturesRoot).filter(f => fs.lstatSync(path.join(fixturesRoot, f)).isDirectory()).forEach(d => { describe(d, () => { + const fixturesDir = path.join(fixturesRoot, d); + + beforeAll(() => { + linter = new ESLint({ + baseConfig: { + parser: '@typescript-eslint/parser', + }, + overrideConfigFile: path.join(fixturesDir, 'eslintrc.js'), + rulePaths: [ + path.join(__dirname, '../../lib/rules'), + ], + fix: true, + }); + }); + const outputDir = path.join(outputRoot, d); fs.mkdirpSync(outputDir); - const fixturesDir = path.join(fixturesRoot, d); const fixtureFiles = fs.readdirSync(fixturesDir).filter(f => f.endsWith('.ts') && !f.endsWith('.expected.ts')); fixtureFiles.forEach(f => { diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/no-core-construct/eslintrc.js b/tools/eslint-plugin-cdk/test/rules/fixtures/no-core-construct/eslintrc.js new file mode 100644 index 0000000000000..3bd78e797f728 --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/no-core-construct/eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: ['rulesdir'], + rules: { + 'rulesdir/no-core-construct': [ 'error' ], + } +} \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/both-constructs.expected.ts b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/both-constructs.expected.ts new file mode 100644 index 0000000000000..20caf8244cd1b --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/both-constructs.expected.ts @@ -0,0 +1,9 @@ +import { Construct } from 'constructs' +import * as cdk from '@aws-cdk/core'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; + +let x: CoreConstruct; +let y: Construct; \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/both-constructs.ts b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/both-constructs.ts new file mode 100644 index 0000000000000..bd92a909af763 --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/both-constructs.ts @@ -0,0 +1,5 @@ +import { Construct } from 'constructs' +import * as cdk from '@aws-cdk/core'; + +let x: cdk.Construct; +let y: Construct; \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/eslintrc.js b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/eslintrc.js new file mode 100644 index 0000000000000..30de0b87a63df --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: ['rulesdir'], + rules: { + 'rulesdir/no-qualified-construct': [ 'error' ], + } +} \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-usage.expected.ts b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-usage.expected.ts index af7c0f393f307..bba5c3ae8aa50 100644 --- a/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-usage.expected.ts +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-usage.expected.ts @@ -1,4 +1,7 @@ import * as cdk from '@aws-cdk/core'; -import * as constructs from 'constructs'; -let x: constructs.Construct; \ No newline at end of file +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + +let x: Construct; \ No newline at end of file From ff1e5b3c580119c107fe26c67fe3cc220f9ee7c9 Mon Sep 17 00:00:00 2001 From: Ayush Goyal Date: Wed, 3 Feb 2021 19:38:17 +0530 Subject: [PATCH 32/33] feat(apigateway): cognito user pool authorizer (#12786) feat(apigateway): add support for cognito user pool authorizer closes #5618 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-apigateway/README.md | 20 ++ .../aws-apigateway/lib/authorizers/cognito.ts | 115 +++++++++++ .../aws-apigateway/lib/authorizers/index.ts | 1 + packages/@aws-cdk/aws-apigateway/package.json | 2 + .../test/authorizers/cognito.test.ts | 66 ++++++ .../integ.cognito-authorizer.expected.json | 191 ++++++++++++++++++ .../authorizers/integ.cognito-authorizer.ts | 43 ++++ 7 files changed, 438 insertions(+) create mode 100644 packages/@aws-cdk/aws-apigateway/lib/authorizers/cognito.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/authorizers/cognito.test.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/authorizers/integ.cognito-authorizer.expected.json create mode 100644 packages/@aws-cdk/aws-apigateway/test/authorizers/integ.cognito-authorizer.ts diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 60d2cdcfa3654..4d4d1a79eb797 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -32,6 +32,7 @@ running on AWS Lambda, or any web application. - [IAM-based authorizer](#iam-based-authorizer) - [Lambda-based token authorizer](#lambda-based-token-authorizer) - [Lambda-based request authorizer](#lambda-based-request-authorizer) + - [Cognito User Pools authorizer](#cognito-user-pools-authorizer) - [Mutual TLS](#mutal-tls-mtls) - [Deployments](#deployments) - [Deep dive: Invalidation of deployments](#deep-dive-invalidation-of-deployments) @@ -580,6 +581,25 @@ Authorizers can also be passed via the `defaultMethodOptions` property within th explicitly overridden, the specified defaults will be applied across all `Method`s across the `RestApi` or across all `Resource`s, depending on where the defaults were specified. +### Cognito User Pools authorizer + +API Gateway also allows [Amazon Cognito user pools as authorizer](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-integrate-with-cognito.html) + +The following snippet configures a Cognito user pool as an authorizer: + +```ts +const userPool = new cognito.UserPool(stack, 'UserPool'); + +const auth = new apigateway.CognitoUserPoolsAuthorizer(this, 'booksAuthorizer', { + cognitoUserPools: [userPool] +}); + +books.addMethod('GET', new apigateway.HttpIntegration('http://amazon.com'), { + authorizer: auth, + authorizationType: apigateway.AuthorizationType.COGNITO, +}); +``` + ## Mutual TLS (mTLS) Mutual TLS can be configured to limit access to your API based by using client certificates instead of (or as an extension of) using authorization headers. diff --git a/packages/@aws-cdk/aws-apigateway/lib/authorizers/cognito.ts b/packages/@aws-cdk/aws-apigateway/lib/authorizers/cognito.ts new file mode 100644 index 0000000000000..a1d000189354c --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/authorizers/cognito.ts @@ -0,0 +1,115 @@ +import * as cognito from '@aws-cdk/aws-cognito'; +import { Duration, Lazy, Names, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnAuthorizer } from '../apigateway.generated'; +import { Authorizer, IAuthorizer } from '../authorizer'; +import { AuthorizationType } from '../method'; +import { IRestApi } from '../restapi'; + +/** + * Properties for CognitoUserPoolsAuthorizer + */ +export interface CognitoUserPoolsAuthorizerProps { + /** + * An optional human friendly name for the authorizer. Note that, this is not the primary identifier of the authorizer. + * + * @default - the unique construct ID + */ + readonly authorizerName?: string; + + /** + * The user pools to associate with this authorizer. + */ + readonly cognitoUserPools: cognito.IUserPool[]; + + /** + * How long APIGateway should cache the results. Max 1 hour. + * Disable caching by setting this to 0. + * + * @default Duration.minutes(5) + */ + readonly resultsCacheTtl?: Duration; + + /** + * The request header mapping expression for the bearer token. This is typically passed as part of the header, in which case + * this should be `method.request.header.Authorizer` where Authorizer is the header containing the bearer token. + * @see https://docs.aws.amazon.com/apigateway/api-reference/link-relation/authorizer-create/#identitySource + * @default `IdentitySource.header('Authorization')` + */ + readonly identitySource?: string; +} + +/** + * Cognito user pools based custom authorizer + * + * @resource AWS::ApiGateway::Authorizer + */ +export class CognitoUserPoolsAuthorizer extends Authorizer implements IAuthorizer { + /** + * The id of the authorizer. + * @attribute + */ + public readonly authorizerId: string; + + /** + * The ARN of the authorizer to be used in permission policies, such as IAM and resource-based grants. + * @attribute + */ + public readonly authorizerArn: string; + + /** + * The authorization type of this authorizer. + */ + public readonly authorizationType?: AuthorizationType; + + private restApiId?: string; + + constructor(scope: Construct, id: string, props: CognitoUserPoolsAuthorizerProps) { + super(scope, id); + + const restApiId = this.lazyRestApiId(); + const resource = new CfnAuthorizer(this, 'Resource', { + name: props.authorizerName ?? Names.uniqueId(this), + restApiId, + type: 'COGNITO_USER_POOLS', + providerArns: props.cognitoUserPools.map(userPool => userPool.userPoolArn), + authorizerResultTtlInSeconds: props.resultsCacheTtl?.toSeconds(), + identitySource: props.identitySource || 'method.request.header.Authorization', + }); + + this.authorizerId = resource.ref; + this.authorizerArn = Stack.of(this).formatArn({ + service: 'execute-api', + resource: restApiId, + resourceName: `authorizers/${this.authorizerId}`, + }); + this.authorizationType = AuthorizationType.COGNITO; + } + + /** + * Attaches this authorizer to a specific REST API. + * @internal + */ + public _attachToApi(restApi: IRestApi): void { + if (this.restApiId && this.restApiId !== restApi.restApiId) { + throw new Error('Cannot attach authorizer to two different rest APIs'); + } + + this.restApiId = restApi.restApiId; + } + + /** + * Returns a token that resolves to the Rest Api Id at the time of synthesis. + * Throws an error, during token resolution, if no RestApi is attached to this authorizer. + */ + private lazyRestApiId() { + return Lazy.string({ + produce: () => { + if (!this.restApiId) { + throw new Error(`Authorizer (${this.node.path}) must be attached to a RestApi`); + } + return this.restApiId; + }, + }); + } +} diff --git a/packages/@aws-cdk/aws-apigateway/lib/authorizers/index.ts b/packages/@aws-cdk/aws-apigateway/lib/authorizers/index.ts index 57289c931f760..fd93db036fefe 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/authorizers/index.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/authorizers/index.ts @@ -1,2 +1,3 @@ export * from './lambda'; export * from './identity-source'; +export * from './cognito'; diff --git a/packages/@aws-cdk/aws-apigateway/package.json b/packages/@aws-cdk/aws-apigateway/package.json index c9650deeb9a4b..3a0641e8b1162 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -80,6 +80,7 @@ "dependencies": { "@aws-cdk/aws-certificatemanager": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", + "@aws-cdk/aws-cognito": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", @@ -95,6 +96,7 @@ "peerDependencies": { "@aws-cdk/aws-certificatemanager": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", + "@aws-cdk/aws-cognito": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/cognito.test.ts b/packages/@aws-cdk/aws-apigateway/test/authorizers/cognito.test.ts new file mode 100644 index 0000000000000..e59339177d5d4 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/cognito.test.ts @@ -0,0 +1,66 @@ +import '@aws-cdk/assert/jest'; +import * as cognito from '@aws-cdk/aws-cognito'; +import { Duration, Stack } from '@aws-cdk/core'; +import { AuthorizationType, CognitoUserPoolsAuthorizer, RestApi } from '../../lib'; + +describe('Cognito Authorizer', () => { + test('default cognito authorizer', () => { + // GIVEN + const stack = new Stack(); + const userPool = new cognito.UserPool(stack, 'UserPool'); + + // WHEN + const authorizer = new CognitoUserPoolsAuthorizer(stack, 'myauthorizer', { + cognitoUserPools: [userPool], + }); + + const restApi = new RestApi(stack, 'myrestapi'); + restApi.root.addMethod('ANY', undefined, { + authorizer, + authorizationType: AuthorizationType.COGNITO, + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGateway::Authorizer', { + Type: 'COGNITO_USER_POOLS', + RestApiId: stack.resolve(restApi.restApiId), + IdentitySource: 'method.request.header.Authorization', + ProviderARNs: [stack.resolve(userPool.userPoolArn)], + }); + + expect(authorizer.authorizerArn.endsWith(`/authorizers/${authorizer.authorizerId}`)).toBeTruthy(); + }); + + test('cognito authorizer with all parameters specified', () => { + // GIVEN + const stack = new Stack(); + const userPool1 = new cognito.UserPool(stack, 'UserPool1'); + const userPool2 = new cognito.UserPool(stack, 'UserPool2'); + + // WHEN + const authorizer = new CognitoUserPoolsAuthorizer(stack, 'myauthorizer', { + cognitoUserPools: [userPool1, userPool2], + identitySource: 'method.request.header.whoami', + authorizerName: 'myauthorizer', + resultsCacheTtl: Duration.minutes(1), + }); + + const restApi = new RestApi(stack, 'myrestapi'); + restApi.root.addMethod('ANY', undefined, { + authorizer, + authorizationType: AuthorizationType.COGNITO, + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGateway::Authorizer', { + Type: 'COGNITO_USER_POOLS', + Name: 'myauthorizer', + RestApiId: stack.resolve(restApi.restApiId), + IdentitySource: 'method.request.header.whoami', + AuthorizerResultTtlInSeconds: 60, + ProviderARNs: [stack.resolve(userPool1.userPoolArn), stack.resolve(userPool2.userPoolArn)], + }); + + expect(authorizer.authorizerArn.endsWith(`/authorizers/${authorizer.authorizerId}`)).toBeTruthy(); + }); +}); diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.cognito-authorizer.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.cognito-authorizer.expected.json new file mode 100644 index 0000000000000..990619cb495d4 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.cognito-authorizer.expected.json @@ -0,0 +1,191 @@ +{ + "Resources": { + "UserPool6BA7E5F2": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } + ] + }, + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "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 {####}" + } + } + }, + "myauthorizer23CB99DD": { + "Type": "AWS::ApiGateway::Authorizer", + "Properties": { + "RestApiId": { + "Ref": "myrestapi551C8392" + }, + "Type": "COGNITO_USER_POOLS", + "IdentitySource": "method.request.header.Authorization", + "Name": "CognitoUserPoolsAuthorizerIntegmyauthorizer10C804C1", + "ProviderARNs": [ + { + "Fn::GetAtt": [ + "UserPool6BA7E5F2", + "Arn" + ] + } + ] + } + }, + "myrestapi551C8392": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "myrestapi" + } + }, + "myrestapiCloudWatchRoleC48DA1DD": { + "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" + ] + ] + } + ] + } + }, + "myrestapiAccountA49A05BE": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "myrestapiCloudWatchRoleC48DA1DD", + "Arn" + ] + } + }, + "DependsOn": [ + "myrestapi551C8392" + ] + }, + "myrestapiDeployment419B1464b903292b53d7532ca4296973bcb95b1a": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "myrestapi551C8392" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "myrestapiANY94B0497F" + ] + }, + "myrestapiDeploymentStageprodA9250EA4": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "myrestapi551C8392" + }, + "DeploymentId": { + "Ref": "myrestapiDeployment419B1464b903292b53d7532ca4296973bcb95b1a" + }, + "StageName": "prod" + } + }, + "myrestapiANY94B0497F": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Fn::GetAtt": [ + "myrestapi551C8392", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "myrestapi551C8392" + }, + "AuthorizationType": "COGNITO_USER_POOLS", + "AuthorizerId": { + "Ref": "myauthorizer23CB99DD" + }, + "Integration": { + "IntegrationResponses": [ + { + "StatusCode": "200" + } + ], + "PassthroughBehavior": "NEVER", + "RequestTemplates": { + "application/json": "{ \"statusCode\": 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "StatusCode": "200" + } + ] + } + } + }, + "Outputs": { + "myrestapiEndpointE06F9D98": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "myrestapi551C8392" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "myrestapiDeploymentStageprodA9250EA4" + }, + "/" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.cognito-authorizer.ts b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.cognito-authorizer.ts new file mode 100644 index 0000000000000..4830dc83ae29f --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.cognito-authorizer.ts @@ -0,0 +1,43 @@ +import * as cognito from '@aws-cdk/aws-cognito'; +import { App, Stack } from '@aws-cdk/core'; +import { AuthorizationType, CognitoUserPoolsAuthorizer, MockIntegration, PassthroughBehavior, RestApi } from '../../lib'; + +/* + * Stack verification steps: + * * 1. Get the IdToken for the created pool by adding user/app-client and using aws cognito-idp: + * * a. aws cognito-idp create-user-pool-client --user-pool-id --client-name --no-generate-secret + * * b. aws cognito-idp admin-create-user --user-pool-id --username --temporary-password + * * c. aws cognito-idp initiate-auth --client-id --auth-flow USER_PASSWORD_AUTH --auth-parameters USERNAME=,PASSWORD= + * * d. aws cognito-idp respond-to-auth-challenge --client-id --challenge-name --session + * * + * * 2. Verify the stack using above obtained token: + * * a. `curl -s -o /dev/null -w "%{http_code}" ` should return 401 + * * b. `curl -s -o /dev/null -w "%{http_code}" -H 'Authorization: ' ` should return 403 + * * c. `curl -s -o /dev/null -w "%{http_code}" -H 'Authorization: ' ` should return 200 + */ + +const app = new App(); +const stack = new Stack(app, 'CognitoUserPoolsAuthorizerInteg'); + +const userPool = new cognito.UserPool(stack, 'UserPool'); + +const authorizer = new CognitoUserPoolsAuthorizer(stack, 'myauthorizer', { + cognitoUserPools: [userPool], +}); + +const restApi = new RestApi(stack, 'myrestapi'); +restApi.root.addMethod('ANY', new MockIntegration({ + integrationResponses: [ + { statusCode: '200' }, + ], + passthroughBehavior: PassthroughBehavior.NEVER, + requestTemplates: { + 'application/json': '{ "statusCode": 200 }', + }, +}), { + methodResponses: [ + { statusCode: '200' }, + ], + authorizer, + authorizationType: AuthorizationType.COGNITO, +}); From 208a7c1904d2e58a255a32133acec5238d23affc Mon Sep 17 00:00:00 2001 From: AWS CDK Team Date: Wed, 3 Feb 2021 22:16:20 +0000 Subject: [PATCH 33/33] chore(release): 1.88.0 --- CHANGELOG.md | 37 +++++++++++++++++++++++++++++++++++++ version.v1.json | 2 +- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5d66ae0944d6..55ff1bfa44ad1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,43 @@ 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.88.0](https://github.com/aws/aws-cdk/compare/v1.87.1...v1.88.0) (2021-02-03) + + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **appmesh:** the properties virtualRouter and virtualNode of VirtualServiceProps have been replaced with the union-like class VirtualServiceProvider +* **appmesh**: the method `addVirtualService` has been removed from `IMesh` +* **cloudfront:** experimental EdgeFunction stack names have changed from 'edge-lambda-stack-${region}' to 'edge-lambda-stack-${stackid}' to support multiple independent CloudFront distributions with EdgeFunctions. + +### Features + +* **apigateway:** cognito user pool authorizer ([#12786](https://github.com/aws/aws-cdk/issues/12786)) ([ff1e5b3](https://github.com/aws/aws-cdk/commit/ff1e5b3c580119c107fe26c67fe3cc220f9ee7c9)), closes [#5618](https://github.com/aws/aws-cdk/issues/5618) +* **apigateway:** import an existing Resource ([#12785](https://github.com/aws/aws-cdk/issues/12785)) ([8a1a9b8](https://github.com/aws/aws-cdk/commit/8a1a9b82a36e681334fd45be595f6ecdf904ad34)), closes [#4432](https://github.com/aws/aws-cdk/issues/4432) +* **appmesh:** change VirtualService provider to a union-like class ([#11978](https://github.com/aws/aws-cdk/issues/11978)) ([dfc765a](https://github.com/aws/aws-cdk/commit/dfc765af44c755f10be8f6c1c2eae55f62e2aa08)), closes [#9490](https://github.com/aws/aws-cdk/issues/9490) +* **aws-route53:** cross account DNS delegations ([#12680](https://github.com/aws/aws-cdk/issues/12680)) ([126a693](https://github.com/aws/aws-cdk/commit/126a6935cacc1f68b1d1155e484912d4ed6978f2)), closes [#8776](https://github.com/aws/aws-cdk/issues/8776) +* **cloudfront:** add PublicKey and KeyGroup L2 constructs ([#12743](https://github.com/aws/aws-cdk/issues/12743)) ([59cb6d0](https://github.com/aws/aws-cdk/commit/59cb6d032a55515ec5e9903f899de588d18d4cb5)) +* **core:** `stack.exportValue()` can be used to solve "deadly embrace" ([#12778](https://github.com/aws/aws-cdk/issues/12778)) ([3b66088](https://github.com/aws/aws-cdk/commit/3b66088010b6f2315a215e92505d5279680f16d4)), closes [#7602](https://github.com/aws/aws-cdk/issues/7602) [#2036](https://github.com/aws/aws-cdk/issues/2036) +* **ecr:** Public Gallery authorization token ([#12775](https://github.com/aws/aws-cdk/issues/12775)) ([8434294](https://github.com/aws/aws-cdk/commit/84342943ad9f2ea8a83773f00816a0b8117c4d17)) +* **ecs-patterns:** Add PlatformVersion option to ScheduledFargateTask props ([#12676](https://github.com/aws/aws-cdk/issues/12676)) ([3cbf38b](https://github.com/aws/aws-cdk/commit/3cbf38b09a9e66a6c009f833481fb25b8c5fc26c)), closes [#12623](https://github.com/aws/aws-cdk/issues/12623) +* **elbv2:** support for 2020 SSL policy ([#12710](https://github.com/aws/aws-cdk/issues/12710)) ([1dd3d05](https://github.com/aws/aws-cdk/commit/1dd3d0518dc2a70c725f87dd5d4377338389125c)), closes [#12595](https://github.com/aws/aws-cdk/issues/12595) +* **iam:** Permissions Boundaries ([#12777](https://github.com/aws/aws-cdk/issues/12777)) ([415eb86](https://github.com/aws/aws-cdk/commit/415eb861c65829cc53eabbbb8706f83f08c74570)), closes [aws/aws-cdk-rfcs#5](https://github.com/aws/aws-cdk-rfcs/issues/5) [#3242](https://github.com/aws/aws-cdk/issues/3242) +* **lambda:** inline code for Python 3.8 ([#12788](https://github.com/aws/aws-cdk/issues/12788)) ([8d3aaba](https://github.com/aws/aws-cdk/commit/8d3aabaffe436e6a3eebc0a58fe361c5b4b93f08)), closes [#6503](https://github.com/aws/aws-cdk/issues/6503) + + +### Bug Fixes + +* **apigateway:** stack update fails to replace api key ([#12745](https://github.com/aws/aws-cdk/issues/12745)) ([ffe7e42](https://github.com/aws/aws-cdk/commit/ffe7e425e605144a465cea9befa68d4fe19f9d8c)), closes [#12698](https://github.com/aws/aws-cdk/issues/12698) +* **cfn-include:** AWS::CloudFormation resources fail in monocdk ([#12758](https://github.com/aws/aws-cdk/issues/12758)) ([5060782](https://github.com/aws/aws-cdk/commit/5060782b00e17bdf44e225f8f5ef03344be238c7)), closes [#11595](https://github.com/aws/aws-cdk/issues/11595) +* **cli, codepipeline:** renamed bootstrap stack still not supported ([#12771](https://github.com/aws/aws-cdk/issues/12771)) ([40b32bb](https://github.com/aws/aws-cdk/commit/40b32bbda272b6e2f92fd5dd8de7ca5bf405ce52)), closes [#12594](https://github.com/aws/aws-cdk/issues/12594) [#12732](https://github.com/aws/aws-cdk/issues/12732) +* **cloudfront:** use node addr for edgeStackId name ([#12702](https://github.com/aws/aws-cdk/issues/12702)) ([c429bb7](https://github.com/aws/aws-cdk/commit/c429bb7df2406346426dce22d716cabc484ec7e6)), closes [#12323](https://github.com/aws/aws-cdk/issues/12323) +* **codedeploy:** wrong syntax on Windows 'installAgent' flag ([#12736](https://github.com/aws/aws-cdk/issues/12736)) ([238742e](https://github.com/aws/aws-cdk/commit/238742e4323310ce850d8edc70abe4b0e9f53186)), closes [#12734](https://github.com/aws/aws-cdk/issues/12734) +* **codepipeline:** permission denied for Action-level environment variables ([#12761](https://github.com/aws/aws-cdk/issues/12761)) ([99fd074](https://github.com/aws/aws-cdk/commit/99fd074a07ead624f64d3fe64685ba67c798976e)), closes [#12742](https://github.com/aws/aws-cdk/issues/12742) +* **ec2:** ARM-backed bastion hosts try to run x86-based Amazon Linux AMI ([#12280](https://github.com/aws/aws-cdk/issues/12280)) ([1a73d76](https://github.com/aws/aws-cdk/commit/1a73d761ad2363842567a1b6e0488ceb093e70b2)), closes [#12279](https://github.com/aws/aws-cdk/issues/12279) +* **efs:** EFS fails to create when using a VPC with multiple subnets per availability zone ([#12097](https://github.com/aws/aws-cdk/issues/12097)) ([889d673](https://github.com/aws/aws-cdk/commit/889d6734c10174f2661e45057c345cd112a44187)), closes [#10170](https://github.com/aws/aws-cdk/issues/10170) +* **iam:** cannot use the same Role for multiple Config Rules ([#12724](https://github.com/aws/aws-cdk/issues/12724)) ([2f6521a](https://github.com/aws/aws-cdk/commit/2f6521a1d8670b2653f7dee281309351181cf918)), closes [#12714](https://github.com/aws/aws-cdk/issues/12714) +* **lambda:** codeguru profiler not set up for Node runtime ([#12712](https://github.com/aws/aws-cdk/issues/12712)) ([59db763](https://github.com/aws/aws-cdk/commit/59db763e7d05d68fd85b6fd37246d69d4670d7d5)), closes [#12624](https://github.com/aws/aws-cdk/issues/12624) + ## [1.87.1](https://github.com/aws/aws-cdk/compare/v1.87.0...v1.87.1) (2021-01-28) diff --git a/version.v1.json b/version.v1.json index 816b141bbbf4a..f804856ae4ccf 100644 --- a/version.v1.json +++ b/version.v1.json @@ -1,3 +1,3 @@ { - "version": "1.87.1" + "version": "1.88.0" }