From 396acee11f9e7c0669b53a148a83063d6889047d Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 4 Mar 2021 16:00:08 +0200 Subject: [PATCH 01/28] chore: fix auto-approve workflow (#13392) I believe the reason our auto-approve workflow does not work is because it was triggered when the pull request was _created_ and before the `pr/auto-approve` label was applied to it. This adds `types: [ labeled ]` so the workflow is triggered when a PR is labeled. The condition remains the same. --- .github/workflows/auto-approve.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/auto-approve.yml b/.github/workflows/auto-approve.yml index cf2ed7e21ca69..c289f5381995c 100644 --- a/.github/workflows/auto-approve.yml +++ b/.github/workflows/auto-approve.yml @@ -1,7 +1,9 @@ # Approve PRs with "pr/auto-approve". mergify takes care of the actual merge. name: auto-approve -on: pull_request +on: + pull_request: + types: [ labeled, unlabeled, opened, synchronize, reopened, ready_for_review, review_requested ] jobs: auto-approve: From 9331657bf177a25fa4d7535ffb027a106b2bebe3 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 4 Mar 2021 15:34:04 +0100 Subject: [PATCH 02/28] chore(build): yarn-cling does not work under Node 15 (#13391) Some tricks we are using to locate packages on disk don't work anymore under newer Node versions. Switch strategies. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- tools/yarn-cling/lib/index.ts | 46 +++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/tools/yarn-cling/lib/index.ts b/tools/yarn-cling/lib/index.ts index 44a09a09fcb8d..38a766c27d7e5 100644 --- a/tools/yarn-cling/lib/index.ts +++ b/tools/yarn-cling/lib/index.ts @@ -1,6 +1,6 @@ -import * as lockfile from '@yarnpkg/lockfile'; -import { promises as fs } from 'fs'; +import { promises as fs, exists } from 'fs'; import * as path from 'path'; +import * as lockfile from '@yarnpkg/lockfile'; import { hoistDependencies } from './hoisting'; import { PackageJson, PackageLock, PackageLockEntry, PackageLockPackage, YarnLock } from './types'; @@ -42,7 +42,7 @@ export async function generateShrinkwrap(options: ShrinkwrapOptions): Promise, yarnLock: YarnLock, rootDir = await fs.realpath(rootDir); for (const [depName, versionRange] of Object.entries(deps)) { - const depPkgJsonFile = require.resolve(`${depName}/package.json`, { paths: [rootDir] }); + const depDir = await findPackageDir(depName, rootDir); + const depPkgJsonFile = path.join(depDir, 'package.json'); const depPkgJson = await loadPackageJson(depPkgJsonFile); - const depDir = path.dirname(depPkgJsonFile); const yarnKey = `${depName}@${versionRange}`; // Sanity check @@ -150,4 +150,40 @@ export function formatPackageLock(entry: PackageLockEntry) { recurse([...names, depName], depEntry); } } +} + +/** + * Find package directory + * + * Do this by walking upwards in the directory tree until we find + * `/node_modules//package.json`. + * + * ------- + * + * Things that we tried but don't work: + * + * 1. require.resolve(`${depName}/package.json`, { paths: [rootDir] }); + * + * Breaks with ES Modules if `package.json` has not been exported, which is + * being enforced starting Node12. + * + * 2. findPackageJsonUpwardFrom(require.resolve(depName, { paths: [rootDir] })) + * + * Breaks if a built-in NodeJS package name conflicts with an NPM package name + * (in Node15 `string_decoder` is introduced...) + */ +async function findPackageDir(depName: string, rootDir: string) { + let prevDir; + let dir = rootDir; + while (dir !== prevDir) { + const candidateDir = path.join(dir, 'node_modules', depName); + if (await new Promise(ok => exists(path.join(candidateDir, 'package.json'), ok))) { + return candidateDir; + } + + prevDir = dir; + dir = path.dirname(dir); // dirname('/') -> '/', dirname('c:\\') -> 'c:\\' + } + + throw new Error(`Did not find '${depName}' upwards of '${rootDir}'`); } \ No newline at end of file From 319cfcdaaf92e4e6edb8c2388d04dce0971aaf86 Mon Sep 17 00:00:00 2001 From: cyuste Date: Thu, 4 Mar 2021 16:53:44 +0100 Subject: [PATCH 03/28] feat(cloudwatch): EC2 actions (#13281) Fixes #13228 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-cloudwatch-actions/README.md | 15 ++++++ .../aws-cloudwatch-actions/lib/ec2.ts | 47 +++++++++++++++++++ .../aws-cloudwatch-actions/lib/index.ts | 1 + .../aws-cloudwatch-actions/test/ec2.test.ts | 41 ++++++++++++++++ packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts | 28 +++++++++++ .../aws-cloudwatch/test/test.alarm.ts | 23 +++++++++ 6 files changed, 155 insertions(+) create mode 100644 packages/@aws-cdk/aws-cloudwatch-actions/lib/ec2.ts create mode 100644 packages/@aws-cdk/aws-cloudwatch-actions/test/ec2.test.ts diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/README.md b/packages/@aws-cdk/aws-cloudwatch-actions/README.md index 03d84220c5c08..f13861a8c1555 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/README.md +++ b/packages/@aws-cdk/aws-cloudwatch-actions/README.md @@ -11,4 +11,19 @@ This library contains a set of classes which can be used as CloudWatch Alarm actions. +The currently implemented actions are: EC2 Actions, SNS Actions, Autoscaling Actions and Aplication Autoscaling Actions + + +## EC2 Action Example + +```ts +import * as cw from "@aws-cdk/aws-cloudwatch"; +// Alarm must be configured with an EC2 per-instance metric +let alarm: cw.Alarm; +// Attach a reboot when alarm triggers +alarm.addAlarmAction( + new Ec2Action(Ec2InstanceActions.REBOOT) +); +``` + See `@aws-cdk/aws-cloudwatch` for more information. diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/lib/ec2.ts b/packages/@aws-cdk/aws-cloudwatch-actions/lib/ec2.ts new file mode 100644 index 0000000000000..57d5a4fb67501 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch-actions/lib/ec2.ts @@ -0,0 +1,47 @@ +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import { Stack } 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'; + +/** + * Types of EC2 actions available + */ +export enum Ec2InstanceAction { + /** + * Stop the instance + */ + STOP = 'stop', + /** + * Terminatethe instance + */ + TERMINATE = 'terminate', + /** + * Recover the instance + */ + RECOVER = 'recover', + /** + * Reboot the instance + */ + REBOOT = 'reboot' +} + +/** + * Use an EC2 action as an Alarm action + */ +export class Ec2Action implements cloudwatch.IAlarmAction { + private ec2Action: Ec2InstanceAction; + + constructor(instanceAction: Ec2InstanceAction) { + this.ec2Action = instanceAction; + } + + /** + * Returns an alarm action configuration to use an EC2 action as an alarm action + */ + bind(_scope: Construct, _alarm: cloudwatch.IAlarm): cloudwatch.AlarmActionConfig { + return { alarmActionArn: `arn:aws:automate:${Stack.of(_scope).region}:ec2:${this.ec2Action}` }; + } +} + diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/lib/index.ts b/packages/@aws-cdk/aws-cloudwatch-actions/lib/index.ts index 3c446d47491aa..5a384eba01247 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudwatch-actions/lib/index.ts @@ -1,3 +1,4 @@ export * from './appscaling'; export * from './autoscaling'; export * from './sns'; +export * from './ec2'; diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/test/ec2.test.ts b/packages/@aws-cdk/aws-cloudwatch-actions/test/ec2.test.ts new file mode 100644 index 0000000000000..8845588d533b4 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch-actions/test/ec2.test.ts @@ -0,0 +1,41 @@ +import '@aws-cdk/assert/jest'; +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import { Stack } from '@aws-cdk/core'; +import * as actions from '../lib'; + +test('can use instance reboot as alarm action', () => { + // GIVEN + const stack = new Stack(); + const alarm = new cloudwatch.Alarm(stack, 'Alarm', { + metric: new cloudwatch.Metric({ + namespace: 'AWS/EC2', + metricName: 'StatusCheckFailed', + dimensions: { + InstanceId: 'i-03cb889aaaafffeee', + }, + }), + evaluationPeriods: 3, + threshold: 100, + }); + + // WHEN + alarm.addAlarmAction(new actions.Ec2Action(actions.Ec2InstanceAction.REBOOT)); + + // THEN + expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { + AlarmActions: [ + { + 'Fn::Join': [ + '', + [ + 'arn:aws:automate:', + { + Ref: 'AWS::Region', + }, + ':ec2:reboot', + ], + ], + }, + ], + }); +}); diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts index 73c1209733b5f..d8c93f66aa910 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts @@ -1,5 +1,6 @@ import { Lazy, Stack, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; +import { IAlarmAction } from './alarm-action'; import { AlarmBase, IAlarm } from './alarm-base'; import { CfnAlarm, CfnAlarmProps } from './cloudwatch.generated'; import { HorizontalAnnotation } from './graph'; @@ -224,6 +225,33 @@ export class Alarm extends AlarmBase { return this.annotation; } + /** + * Trigger this action if the alarm fires + * + * Typically the ARN of an SNS topic or ARN of an AutoScaling policy. + */ + public addAlarmAction(...actions: IAlarmAction[]) { + if (this.alarmActionArns === undefined) { + this.alarmActionArns = []; + } + + this.alarmActionArns.push(...actions.map(a => + this.validateActionArn(a.bind(this, this).alarmActionArn), + )); + } + + private validateActionArn(actionArn: string): string { + const ec2ActionsRegexp: RegExp = /arn:aws:automate:[a-z|\d|-]+:ec2:[a-z]+/; + if (ec2ActionsRegexp.test(actionArn)) { + // Check per-instance metric + const metricConfig = this.metric.toMetricConfig(); + if (metricConfig.metricStat?.dimensions?.length != 1 || metricConfig.metricStat?.dimensions![0].name != 'InstanceId') { + throw new Error(`EC2 alarm actions requires an EC2 Per-Instance Metric. (${JSON.stringify(metricConfig)} does not have an 'InstanceId' dimension)`); + } + } + return actionArn; + } + private renderMetric(metric: IMetric) { const self = this; return dispatchMetric(metric, { diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts b/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts index c7c0f647c2e58..c6a727c023c16 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts @@ -42,6 +42,20 @@ export = { test.done(); }, + 'non ec2 instance related alarm does not accept EC2 action'(test: Test) { + + const stack = new Stack(); + const alarm = new Alarm(stack, 'Alarm', { + metric: testMetric, + threshold: 1000, + evaluationPeriods: 2, + }); + + test.throws(() => { + alarm.addAlarmAction(new Ec2TestAlarmAction('arn:aws:automate:us-east-1:ec2:reboot')); + }, /EC2 alarm actions requires an EC2 Per-Instance Metric. \(.+ does not have an 'InstanceId' dimension\)/); + test.done(); + }, 'can make simple alarm'(test: Test) { // GIVEN const stack = new Stack(); @@ -253,3 +267,12 @@ class TestAlarmAction implements IAlarmAction { return { alarmActionArn: this.arn }; } } + +class Ec2TestAlarmAction implements IAlarmAction { + constructor(private readonly arn: string) { + } + + public bind(_scope: Construct, _alarm: IAlarm) { + return { alarmActionArn: this.arn }; + } +} From 48963f736d5401d0fb4755b983b26cd738b9a1fe Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Thu, 4 Mar 2021 08:44:32 -0800 Subject: [PATCH 04/28] chore: add new interfaces for Assets (#13356) This is a re-submit of the PR #12700, which had to be reverted because of JSII issue https://github.com/aws/jsii/issues/2256. Since that issue has been fixed in JSII version `1.23.0`, which is what we currently use, re-introduce the changes from that PR. ---- *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 | 17 +++++-- packages/@aws-cdk/aws-s3-assets/lib/asset.ts | 4 +- packages/@aws-cdk/core/lib/fs/options.ts | 51 ++++++++++++++----- 4 files changed, 54 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 26a3a40f35335..3dd422c694176 100644 --- a/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts +++ b/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts @@ -2,7 +2,7 @@ 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, FeatureFlags, IgnoreMode, Stack, Token } from '@aws-cdk/core'; +import { Annotations, AssetStaging, FeatureFlags, FileFingerprintOptions, IgnoreMode, Stack, SymlinkFollowMode, Token } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; @@ -13,7 +13,7 @@ import { Construct as CoreConstruct } from '@aws-cdk/core'; /** * Options for DockerImageAsset */ -export interface DockerImageAssetOptions extends assets.FingerprintOptions { +export interface DockerImageAssetOptions extends assets.FingerprintOptions, FileFingerprintOptions { /** * ECR repository name * @@ -141,8 +141,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, @@ -185,3 +186,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 510834a61c634..aa342337a9df3 100644 --- a/packages/@aws-cdk/aws-s3-assets/lib/asset.ts +++ b/packages/@aws-cdk/aws-s3-assets/lib/asset.ts @@ -12,7 +12,7 @@ import { toSymlinkFollow } from './compat'; // eslint-disable-next-line no-duplicate-imports, import/order import { Construct as CoreConstruct } from '@aws-cdk/core'; -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. @@ -125,7 +125,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 20a22fef7b1b7266395cd7e9e02140e544e562ba Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 4 Mar 2021 18:16:51 +0100 Subject: [PATCH 05/28] chore(build): record preferred NPM publishing order (#13394) Use lerna to generate a topologically sorted list of `.tgz` files. This list will be used during publishing to impose an order on the package publishing actions. Fixes #13221. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- pack.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pack.sh b/pack.sh index 727c6faaf63cd..b85c92f3e68a5 100755 --- a/pack.sh +++ b/pack.sh @@ -63,6 +63,16 @@ for dir in $(find packages -name dist | grep -v node_modules | grep -v run-wrapp rsync -a $dir/ ${distdir}/ done +# Record the dependency order of NPM packages into a file +# (This file will be opportunistically used during publishing) +# +# Manually sort 'aws-cdk' to the end, as the 'cdk init' command has implicit dependencies +# on other packages (that should not appear in 'package.json' and so +# there is no way to tell lerna about these). +for dir in $(lerna ls --toposort -p | grep -v packages/aws-cdk) $PWD/packages/aws-cdk; do + (cd $dir/dist/js && ls >> ${distdir}/js/npm-publish-order.txt) || true +done + # Remove a JSII aggregate POM that may have snuk past rm -rf dist/java/software/amazon/jsii From 664133a35da2bd096a237971ce662f3dd38b297f Mon Sep 17 00:00:00 2001 From: Masaharu Komuro Date: Fri, 5 Mar 2021 02:50:32 +0900 Subject: [PATCH 06/28] fix(ec2): NAT provider's default outbound rules cannot be disabled (#12674) `allowAllTraffic` only applies to inbound traffic, but it should also apply to outbound traffic. Deprecate it and add a new enum-based property, `defaultAllowedTraffic`, which also allows controlling outbound traffic rules. There is no option to allow inbound but not outbound because there is no use case for that. Fix #12673 ---- *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/nat.ts | 57 +++++++- packages/@aws-cdk/aws-ec2/test/vpc.test.ts | 151 ++++++++++++++++++++- 2 files changed, 202 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/nat.ts b/packages/@aws-cdk/aws-ec2/lib/nat.ts index c164362703692..bf65671b92287 100644 --- a/packages/@aws-cdk/aws-ec2/lib/nat.ts +++ b/packages/@aws-cdk/aws-ec2/lib/nat.ts @@ -7,6 +7,26 @@ import { Port } from './port'; import { ISecurityGroup, SecurityGroup } from './security-group'; import { PrivateSubnet, PublicSubnet, RouterType, Vpc } from './vpc'; +/** + * Direction of traffic to allow all by default. + */ +export enum NatTrafficDirection { + /** + * Allow all outbound traffic and disallow all inbound traffic. + */ + OUTBOUND_ONLY = 'OUTBOUND_ONLY', + + /** + * Allow all outbound and inbound traffic. + */ + INBOUND_AND_OUTBOUND = 'INBOUND_AND_OUTBOUND', + + /** + * Disallow all outbound and inbound traffic. + */ + NONE = 'NONE', +} + /** * Pair represents a gateway created by NAT Provider */ @@ -148,7 +168,7 @@ export interface NatInstanceProps { readonly securityGroup?: ISecurityGroup; /** - * Allow all traffic through the NAT instance + * Allow all inbound traffic through the NAT instance * * If you set this to false, you must configure the NAT instance's security * groups in another way, either by passing in a fully configured Security @@ -157,8 +177,24 @@ export interface NatInstanceProps { * Provider to a Vpc. * * @default true + * @deprecated - Use `defaultAllowedTraffic`. */ readonly allowAllTraffic?: boolean; + + /** + * Direction to allow all traffic through the NAT instance by default. + * + * By default, inbound and outbound traffic is allowed. + * + * If you set this to another value than INBOUND_AND_OUTBOUND, you must + * configure the NAT instance's security groups in another way, either by + * passing in a fully configured Security Group using the `securityGroup` + * property, or by configuring it using the `.securityGroup` or + * `.connections` members after passing the NAT Instance Provider to a Vpc. + * + * @default NatTrafficDirection.INBOUND_AND_OUTBOUND + */ + readonly defaultAllowedTraffic?: NatTrafficDirection; } /** @@ -205,18 +241,26 @@ export class NatInstanceProvider extends NatProvider implements IConnectable { constructor(private readonly props: NatInstanceProps) { super(); + + if (props.defaultAllowedTraffic !== undefined && props.allowAllTraffic !== undefined) { + throw new Error('Can not specify both of \'defaultAllowedTraffic\' and \'defaultAllowedTraffic\'; prefer \'defaultAllowedTraffic\''); + } } public configureNat(options: ConfigureNatOptions) { + const defaultDirection = this.props.defaultAllowedTraffic ?? + (this.props.allowAllTraffic ?? true ? NatTrafficDirection.INBOUND_AND_OUTBOUND : NatTrafficDirection.OUTBOUND_ONLY); + // Create the NAT instances. They can share a security group and a Role. const machineImage = this.props.machineImage || new NatInstanceImage(); this._securityGroup = this.props.securityGroup ?? new SecurityGroup(options.vpc, 'NatSecurityGroup', { vpc: options.vpc, description: 'Security Group for NAT instances', + allowAllOutbound: isOutboundAllowed(defaultDirection), }); this._connections = new Connections({ securityGroups: [this._securityGroup] }); - if (this.props.allowAllTraffic ?? true) { + if (isInboundAllowed(defaultDirection)) { this.connections.allowFromAnyIpv4(Port.allTraffic()); } @@ -325,3 +369,12 @@ export class NatInstanceImage extends LookupMachineImage { }); } } + +function isOutboundAllowed(direction: NatTrafficDirection) { + return direction === NatTrafficDirection.INBOUND_AND_OUTBOUND || + direction === NatTrafficDirection.OUTBOUND_ONLY; +} + +function isInboundAllowed(direction: NatTrafficDirection) { + return direction === NatTrafficDirection.INBOUND_AND_OUTBOUND; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/vpc.test.ts b/packages/@aws-cdk/aws-ec2/test/vpc.test.ts index 9f4ceaf1357d6..77de63770cd6a 100644 --- a/packages/@aws-cdk/aws-ec2/test/vpc.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/vpc.test.ts @@ -2,9 +2,30 @@ import { countResources, expect as cdkExpect, haveResource, haveResourceLike, is import { CfnOutput, CfnResource, Fn, Lazy, Stack, Tags } from '@aws-cdk/core'; import { nodeunitShim, Test } from 'nodeunit-shim'; import { - AclCidr, AclTraffic, BastionHostLinux, CfnSubnet, CfnVPC, SubnetFilter, DefaultInstanceTenancy, GenericLinuxImage, - InstanceType, InterfaceVpcEndpoint, InterfaceVpcEndpointService, NatProvider, NetworkAcl, NetworkAclEntry, Peer, Port, PrivateSubnet, - PublicSubnet, RouterType, Subnet, SubnetType, TrafficDirection, Vpc, + AclCidr, + AclTraffic, + BastionHostLinux, + CfnSubnet, + CfnVPC, + SubnetFilter, + DefaultInstanceTenancy, + GenericLinuxImage, + InstanceType, + InterfaceVpcEndpoint, + InterfaceVpcEndpointService, + NatProvider, + NatTrafficDirection, + NetworkAcl, + NetworkAclEntry, + Peer, + Port, + PrivateSubnet, + PublicSubnet, + RouterType, + Subnet, + SubnetType, + TrafficDirection, + Vpc, } from '../lib'; nodeunitShim({ @@ -904,6 +925,22 @@ nodeunitShim({ DestinationCidrBlock: '0.0.0.0/0', InstanceId: { Ref: 'TheVPCPublicSubnet1NatInstanceCC514192' }, })); + cdkExpect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + SecurityGroupEgress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + }, + ], + SecurityGroupIngress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'from 0.0.0.0/0:ALL TRAFFIC', + IpProtocol: '-1', + }, + ], + })); test.done(); }, @@ -929,7 +966,7 @@ nodeunitShim({ test.done(); }, - 'can configure Security Groups of NAT instances'(test: Test) { + 'can configure Security Groups of NAT instances with allowAllTraffic false'(test: Test) { // GIVEN const stack = getTestStack(); @@ -948,6 +985,13 @@ nodeunitShim({ // THEN cdkExpect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + SecurityGroupEgress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + }, + ], SecurityGroupIngress: [ { CidrIp: '1.2.3.4/32', @@ -962,6 +1006,105 @@ nodeunitShim({ test.done(); }, + 'can configure Security Groups of NAT instances with defaultAllowAll INBOUND_AND_OUTBOUND'(test: Test) { + // GIVEN + const stack = getTestStack(); + + // WHEN + const provider = NatProvider.instance({ + instanceType: new InstanceType('q86.mega'), + machineImage: new GenericLinuxImage({ + 'us-east-1': 'ami-1', + }), + defaultAllowedTraffic: NatTrafficDirection.INBOUND_AND_OUTBOUND, + }); + new Vpc(stack, 'TheVPC', { + natGatewayProvider: provider, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + SecurityGroupEgress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + }, + ], + SecurityGroupIngress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'from 0.0.0.0/0:ALL TRAFFIC', + IpProtocol: '-1', + }, + ], + })); + + test.done(); + }, + + 'can configure Security Groups of NAT instances with defaultAllowAll OUTBOUND_ONLY'(test: Test) { + // GIVEN + const stack = getTestStack(); + + // WHEN + const provider = NatProvider.instance({ + instanceType: new InstanceType('q86.mega'), + machineImage: new GenericLinuxImage({ + 'us-east-1': 'ami-1', + }), + defaultAllowedTraffic: NatTrafficDirection.OUTBOUND_ONLY, + }); + new Vpc(stack, 'TheVPC', { + natGatewayProvider: provider, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + SecurityGroupEgress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + }, + ], + })); + + test.done(); + }, + + 'can configure Security Groups of NAT instances with defaultAllowAll NONE'(test: Test) { + // GIVEN + const stack = getTestStack(); + + // WHEN + const provider = NatProvider.instance({ + instanceType: new InstanceType('q86.mega'), + machineImage: new GenericLinuxImage({ + 'us-east-1': 'ami-1', + }), + defaultAllowedTraffic: NatTrafficDirection.NONE, + }); + new Vpc(stack, 'TheVPC', { + natGatewayProvider: provider, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + SecurityGroupEgress: [ + { + CidrIp: '255.255.255.255/32', + Description: 'Disallow all traffic', + FromPort: 252, + IpProtocol: 'icmp', + ToPort: 86, + }, + ], + })); + + test.done(); + }, + }, 'Network ACL association': { From a452bc385640762a043392a717d49de29abcc64e Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Thu, 4 Mar 2021 15:34:38 -0700 Subject: [PATCH 07/28] feat(ecs): allow selection of container and port for SRV service discovery records (#12798) Adds `container` and `containerPort` as optional properties of `CloudMapOptions`. This change allows the user to select which container and container port the `SRV` record points to. Closes #12796 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-ecs/README.md | 43 + .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 65 +- .../aws-ecs/test/ec2/ec2-service.test.ts | 262 ++++++ ...nteg.cloudmap-container-port.expected.json | 808 ++++++++++++++++++ .../test/ec2/integ.cloudmap-container-port.ts | 70 ++ .../test/fargate/fargate-service.test.ts | 46 + 6 files changed, 1288 insertions(+), 6 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/test/ec2/integ.cloudmap-container-port.expected.json create mode 100644 packages/@aws-cdk/aws-ecs/test/ec2/integ.cloudmap-container-port.ts diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 09a5ad13c14cd..a038cf65fbaf9 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -683,6 +683,49 @@ taskDefinition.addContainer('TheContainer', { }); ``` +## CloudMap Service Discovery + +To register your ECS service with a CloudMap Service Registry, you may add the +`cloudMapOptions` property to your service: + +```ts +const service = new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + cloudMapOptions: { + // Create A records - useful for AWSVPC network mode. + dnsRecordType: cloudmap.DnsRecordType.A, + }, +}); +``` + +With `bridge` or `host` network modes, only `SRV` DNS record types are supported. +By default, `SRV` DNS record types will target the default container and default +port. However, you may target a different container and port on the same ECS task: + +```ts +// Add a container to the task definition +const specificContainer = taskDefinition.addContainer(...); + +// Add a port mapping +specificContainer.addPortMappings({ + containerPort: 7600, + protocol: ecs.Protocol.TCP, +}); + +new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + cloudMapOptions: { + // Create SRV records - useful for bridge networking + dnsRecordType: cloudmap.DnsRecordType.SRV, + // Targets port TCP port 7600 `specificContainer` + container: specificContainer, + containerPort: 7600, + }, +}); +``` + ## Capacity Providers Currently, only `FARGATE` and `FARGATE_SPOT` capacity providers are supported. diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 8e2483f98838f..1e1fdde585f1e 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -8,8 +8,8 @@ import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import { Annotations, Duration, IResolvable, IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { LoadBalancerTargetOptions, NetworkMode, TaskDefinition } from '../base/task-definition'; +import { ContainerDefinition, Protocol } from '../container-definition'; import { ICluster, CapacityProviderStrategy } from '../cluster'; -import { Protocol } from '../container-definition'; import { CfnService } from '../ecs.generated'; import { ScalableTaskCount } from './scalable-task-count'; @@ -572,10 +572,12 @@ export abstract class BaseService extends Resource } } - // If the task definition that your service task specifies uses the AWSVPC network mode and a type SRV DNS record is - // used, you must specify a containerName and containerPort combination - const containerName = dnsRecordType === cloudmap.DnsRecordType.SRV ? this.taskDefinition.defaultContainer!.containerName : undefined; - const containerPort = dnsRecordType === cloudmap.DnsRecordType.SRV ? this.taskDefinition.defaultContainer!.containerPort : undefined; + const { containerName, containerPort } = determineContainerNameAndPort({ + taskDefinition: this.taskDefinition, + dnsRecordType: dnsRecordType!, + container: options.container, + containerPort: options.containerPort, + }); const cloudmapService = new cloudmap.Service(this, 'CloudmapService', { namespace: sdNamespace, @@ -799,7 +801,19 @@ export interface CloudMapOptions { * * NOTE: This is used for HealthCheckCustomConfig */ - readonly failureThreshold?: number, + readonly failureThreshold?: number; + + /** + * The container to point to for a SRV record. + * @default - the task definition's default container + */ + readonly container?: ContainerDefinition; + + /** + * The port to point to for a SRV record. + * @default - the default port of the task definition's default container + */ + readonly containerPort?: number; } /** @@ -885,3 +899,42 @@ export enum PropagatedTagSource { */ NONE = 'NONE' } + +/** + * Options for `determineContainerNameAndPort` + */ +interface DetermineContainerNameAndPortOptions { + dnsRecordType: cloudmap.DnsRecordType; + taskDefinition: TaskDefinition; + container?: ContainerDefinition; + containerPort?: number; +} + +/** + * Determine the name of the container and port to target for the service registry. + */ +function determineContainerNameAndPort(options: DetermineContainerNameAndPortOptions) { + // If the record type is SRV, then provide the containerName and containerPort to target. + // We use the name of the default container and the default port of the default container + // unless the user specifies otherwise. + if (options.dnsRecordType === cloudmap.DnsRecordType.SRV) { + // Ensure the user-provided container is from the right task definition. + if (options.container && options.container.taskDefinition != options.taskDefinition) { + throw new Error('Cannot add discovery for a container from another task definition'); + } + + const container = options.container ?? options.taskDefinition.defaultContainer!; + + // Ensure that any port given by the user is mapped. + if (options.containerPort && !container.portMappings.some(mapping => mapping.containerPort === options.containerPort)) { + throw new Error('Cannot add discovery for a container port that has not been mapped'); + } + + return { + containerName: container.containerName, + containerPort: options.containerPort ?? options.taskDefinition.defaultContainer!.containerPort, + }; + } + + return {}; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts index d88c5ecafd1d7..945832a09b869 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts @@ -2190,6 +2190,268 @@ nodeunitShim({ test.done(); }, + + 'user can select any container and port'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + type: cloudmap.NamespaceType.DNS_PRIVATE, + }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'FargateTaskDef', { + networkMode: ecs.NetworkMode.BRIDGE, + }); + + const mainContainer = taskDefinition.addContainer('MainContainer', { + image: ecs.ContainerImage.fromRegistry('hello'), + memoryLimitMiB: 512, + }); + mainContainer.addPortMappings({ containerPort: 8000 }); + + const otherContainer = taskDefinition.addContainer('OtherContainer', { + image: ecs.ContainerImage.fromRegistry('hello'), + memoryLimitMiB: 512, + }); + otherContainer.addPortMappings({ containerPort: 8001 }); + + // WHEN + new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + cloudMapOptions: { + dnsRecordType: cloudmap.DnsRecordType.SRV, + container: otherContainer, + containerPort: 8001, + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::Service', { + ServiceRegistries: [ + { + RegistryArn: { 'Fn::GetAtt': ['ServiceCloudmapService046058A4', 'Arn'] }, + ContainerName: 'OtherContainer', + ContainerPort: 8001, + }, + ], + })); + + test.done(); + }, + + 'By default, the container name is the default'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + type: cloudmap.NamespaceType.DNS_PRIVATE, + }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Task', { + networkMode: ecs.NetworkMode.BRIDGE, + }); + + taskDefinition.addContainer('main', { + image: ecs.ContainerImage.fromRegistry('some'), + memoryLimitMiB: 512, + }).addPortMappings({ containerPort: 1234 }); + + taskDefinition.addContainer('second', { + image: ecs.ContainerImage.fromRegistry('some'), + memoryLimitMiB: 512, + }).addPortMappings({ containerPort: 4321 }); + + // WHEN + new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + cloudMapOptions: {}, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::Service', { + ServiceRegistries: [{ + ContainerName: 'main', + ContainerPort: undefined, + }], + })); + + test.done(); + }, + + 'For SRV, by default, container name is default container and port is the default container port'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + type: cloudmap.NamespaceType.DNS_PRIVATE, + }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Task', { + networkMode: ecs.NetworkMode.BRIDGE, + }); + + taskDefinition.addContainer('main', { + image: ecs.ContainerImage.fromRegistry('some'), + memoryLimitMiB: 512, + }).addPortMappings({ containerPort: 1234 }); + + taskDefinition.addContainer('second', { + image: ecs.ContainerImage.fromRegistry('some'), + memoryLimitMiB: 512, + }).addPortMappings({ containerPort: 4321 }); + + // WHEN + new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + cloudMapOptions: { + dnsRecordType: cloudmap.DnsRecordType.SRV, + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::Service', { + ServiceRegistries: [{ + ContainerName: 'main', + ContainerPort: 1234, + }], + })); + + test.done(); + }, + + 'allows SRV service discovery to select the container and port'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + type: cloudmap.NamespaceType.DNS_PRIVATE, + }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Task', { + networkMode: ecs.NetworkMode.BRIDGE, + }); + + taskDefinition.addContainer('main', { + image: ecs.ContainerImage.fromRegistry('some'), + memoryLimitMiB: 512, + }).addPortMappings({ containerPort: 1234 }); + + const secondContainer = taskDefinition.addContainer('second', { + image: ecs.ContainerImage.fromRegistry('some'), + memoryLimitMiB: 512, + }); + secondContainer.addPortMappings({ containerPort: 4321 }); + + // WHEN + new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + cloudMapOptions: { + dnsRecordType: cloudmap.DnsRecordType.SRV, + container: secondContainer, + containerPort: 4321, + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::Service', { + ServiceRegistries: [{ + ContainerName: 'second', + ContainerPort: 4321, + }], + })); + + test.done(); + }, + + 'throws if SRV and container is not part of task definition'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + type: cloudmap.NamespaceType.DNS_PRIVATE, + }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Task', { + networkMode: ecs.NetworkMode.BRIDGE, + }); + + // The right container + taskDefinition.addContainer('MainContainer', { + image: ecs.ContainerImage.fromRegistry('hello'), + memoryLimitMiB: 512, + }); + + const wrongTaskDefinition = new ecs.Ec2TaskDefinition(stack, 'WrongTaskDef'); + // The wrong container + const wrongContainer = wrongTaskDefinition.addContainer('MainContainer', { + image: ecs.ContainerImage.fromRegistry('hello'), + memoryLimitMiB: 512, + }); + + // WHEN + test.throws(() => { + new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + cloudMapOptions: { + dnsRecordType: cloudmap.DnsRecordType.SRV, + container: wrongContainer, + containerPort: 4321, + }, + }); + }, /another task definition/i); + + test.done(); + }, + + 'throws if SRV and the container port is not mapped'(test: Test) { + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + type: cloudmap.NamespaceType.DNS_PRIVATE, + }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Task', { + networkMode: ecs.NetworkMode.BRIDGE, + }); + + const container = taskDefinition.addContainer('MainContainer', { + image: ecs.ContainerImage.fromRegistry('hello'), + memoryLimitMiB: 512, + }); + + container.addPortMappings({ containerPort: 8000 }); + + test.throws(() => { + new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + cloudMapOptions: { + dnsRecordType: cloudmap.DnsRecordType.SRV, + container: container, + containerPort: 4321, + }, + }); + }, /container port.*not.*mapped/i); + + test.done(); + }, }, 'Metric'(test: Test) { diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.cloudmap-container-port.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.cloudmap-container-port.expected.json new file mode 100644 index 0000000000000..e067e7d75a67d --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.cloudmap-container-port.expected.json @@ -0,0 +1,808 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc" + } + ] + } + }, + "VpcpubSubnet1Subnet410C08CF": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/24", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "pub" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/pubSubnet1" + } + ] + } + }, + "VpcpubSubnet1RouteTableE0483FDA": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/pubSubnet1" + } + ] + } + }, + "VpcpubSubnet1RouteTableAssociation68036D8C": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcpubSubnet1RouteTableE0483FDA" + }, + "SubnetId": { + "Ref": "VpcpubSubnet1Subnet410C08CF" + } + } + }, + "VpcpubSubnet1DefaultRouteF020A9EF": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcpubSubnet1RouteTableE0483FDA" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcpubSubnet2Subnet44A37A0D": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.1.0/24", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "pub" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/pubSubnet2" + } + ] + } + }, + "VpcpubSubnet2RouteTable5A29DF40": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/pubSubnet2" + } + ] + } + }, + "VpcpubSubnet2RouteTableAssociationFB826925": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcpubSubnet2RouteTable5A29DF40" + }, + "SubnetId": { + "Ref": "VpcpubSubnet2Subnet44A37A0D" + } + } + }, + "VpcpubSubnet2DefaultRouteE6D48BA4": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcpubSubnet2RouteTable5A29DF40" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "FargateCluster7CCD5F93": { + "Type": "AWS::ECS::Cluster" + }, + "FargateClustercapacityInstanceSecurityGroupCB3AEDA1": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ/FargateCluster/capacity/InstanceSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "from 0.0.0.0/0:32768-61000", + "FromPort": 32768, + "IpProtocol": "tcp", + "ToPort": 61000 + } + ], + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/FargateCluster/capacity" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "FargateClustercapacityInstanceRoleBE253D2D": { + "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": "aws-ecs-integ/FargateCluster/capacity" + } + ] + } + }, + "FargateClustercapacityInstanceRoleDefaultPolicy90B38927": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecs:DeregisterContainerInstance", + "ecs:RegisterContainerInstance", + "ecs:Submit*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "FargateCluster7CCD5F93", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:Poll", + "ecs:StartTelemetrySession" + ], + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "FargateCluster7CCD5F93", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ecs:DiscoverPollEndpoint", + "ecr:GetAuthorizationToken", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "FargateClustercapacityInstanceRoleDefaultPolicy90B38927", + "Roles": [ + { + "Ref": "FargateClustercapacityInstanceRoleBE253D2D" + } + ] + } + }, + "FargateClustercapacityInstanceProfile8294296C": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "FargateClustercapacityInstanceRoleBE253D2D" + } + ] + } + }, + "FargateClustercapacityLaunchConfig9B95CCB7": { + "Type": "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId": { + "Ref": "SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "InstanceType": "t3.micro", + "IamInstanceProfile": { + "Ref": "FargateClustercapacityInstanceProfile8294296C" + }, + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "FargateClustercapacityInstanceSecurityGroupCB3AEDA1", + "GroupId" + ] + } + ], + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\necho ECS_CLUSTER=", + { + "Ref": "FargateCluster7CCD5F93" + }, + " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save\necho ECS_AWSVPC_BLOCK_IMDS=true >> /etc/ecs/ecs.config" + ] + ] + } + } + }, + "DependsOn": [ + "FargateClustercapacityInstanceRoleDefaultPolicy90B38927", + "FargateClustercapacityInstanceRoleBE253D2D" + ] + }, + "FargateClustercapacityASGE4034F96": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": "1", + "MinSize": "1", + "DesiredCapacity": "1", + "LaunchConfigurationName": { + "Ref": "FargateClustercapacityLaunchConfig9B95CCB7" + }, + "Tags": [ + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "aws-ecs-integ/FargateCluster/capacity" + } + ], + "VPCZoneIdentifier": [ + { + "Ref": "VpcpubSubnet1Subnet410C08CF" + }, + { + "Ref": "VpcpubSubnet2Subnet44A37A0D" + } + ] + }, + "UpdatePolicy": { + "AutoScalingReplacingUpdate": { + "WillReplace": true + }, + "AutoScalingScheduledAction": { + "IgnoreUnmodifiedGroupSizeProperties": true + } + } + }, + "FargateClustercapacityDrainECSHookFunctionServiceRoleA28505D9": { + "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" + ] + ] + } + ], + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/FargateCluster/capacity" + } + ] + } + }, + "FargateClustercapacityDrainECSHookFunctionServiceRoleDefaultPolicy53CD1145": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ec2:DescribeInstances", + "ec2:DescribeInstanceAttribute", + "ec2:DescribeInstanceStatus", + "ec2:DescribeHosts" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "autoscaling:CompleteLifecycleAction", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":autoscaling:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":autoScalingGroup:*:autoScalingGroupName/", + { + "Ref": "FargateClustercapacityASGE4034F96" + } + ] + ] + } + }, + { + "Action": [ + "ecs:DescribeContainerInstances", + "ecs:DescribeTasks" + ], + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "FargateCluster7CCD5F93", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ecs:ListContainerInstances", + "ecs:SubmitContainerStateChange", + "ecs:SubmitTaskStateChange" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "FargateCluster7CCD5F93", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:UpdateContainerInstancesState", + "ecs:ListTasks" + ], + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "FargateCluster7CCD5F93", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "FargateClustercapacityDrainECSHookFunctionServiceRoleDefaultPolicy53CD1145", + "Roles": [ + { + "Ref": "FargateClustercapacityDrainECSHookFunctionServiceRoleA28505D9" + } + ] + } + }, + "FargateClustercapacityDrainECSHookFunction3E60E6D0": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "import boto3, json, os, time\n\necs = boto3.client('ecs')\nautoscaling = boto3.client('autoscaling')\n\n\ndef lambda_handler(event, context):\n print(json.dumps(event))\n cluster = os.environ['CLUSTER']\n snsTopicArn = event['Records'][0]['Sns']['TopicArn']\n lifecycle_event = json.loads(event['Records'][0]['Sns']['Message'])\n instance_id = lifecycle_event.get('EC2InstanceId')\n if not instance_id:\n print('Got event without EC2InstanceId: %s', json.dumps(event))\n return\n\n instance_arn = container_instance_arn(cluster, instance_id)\n print('Instance %s has container instance ARN %s' % (lifecycle_event['EC2InstanceId'], instance_arn))\n\n if not instance_arn:\n return\n\n while has_tasks(cluster, instance_arn):\n time.sleep(10)\n\n try:\n print('Terminating instance %s' % instance_id)\n autoscaling.complete_lifecycle_action(\n LifecycleActionResult='CONTINUE',\n **pick(lifecycle_event, 'LifecycleHookName', 'LifecycleActionToken', 'AutoScalingGroupName'))\n except Exception as e:\n # Lifecycle action may have already completed.\n print(str(e))\n\n\ndef container_instance_arn(cluster, instance_id):\n \"\"\"Turn an instance ID into a container instance ARN.\"\"\"\n arns = ecs.list_container_instances(cluster=cluster, filter='ec2InstanceId==' + instance_id)['containerInstanceArns']\n if not arns:\n return None\n return arns[0]\n\n\ndef has_tasks(cluster, instance_arn):\n \"\"\"Return True if the instance is running tasks for the given cluster.\"\"\"\n instances = ecs.describe_container_instances(cluster=cluster, containerInstances=[instance_arn])['containerInstances']\n if not instances:\n return False\n instance = instances[0]\n\n if instance['status'] == 'ACTIVE':\n # Start draining, then try again later\n set_container_instance_to_draining(cluster, instance_arn)\n return True\n\n tasks = instance['runningTasksCount'] + instance['pendingTasksCount']\n print('Instance %s has %s tasks' % (instance_arn, tasks))\n\n return tasks > 0\n\n\ndef set_container_instance_to_draining(cluster, instance_arn):\n ecs.update_container_instances_state(\n cluster=cluster,\n containerInstances=[instance_arn], status='DRAINING')\n\n\ndef pick(dct, *keys):\n \"\"\"Pick a subset of a dict.\"\"\"\n return {k: v for k, v in dct.items() if k in keys}\n" + }, + "Role": { + "Fn::GetAtt": [ + "FargateClustercapacityDrainECSHookFunctionServiceRoleA28505D9", + "Arn" + ] + }, + "Environment": { + "Variables": { + "CLUSTER": { + "Ref": "FargateCluster7CCD5F93" + } + } + }, + "Handler": "index.lambda_handler", + "Runtime": "python3.6", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/FargateCluster/capacity" + } + ], + "Timeout": 310 + }, + "DependsOn": [ + "FargateClustercapacityDrainECSHookFunctionServiceRoleDefaultPolicy53CD1145", + "FargateClustercapacityDrainECSHookFunctionServiceRoleA28505D9" + ] + }, + "FargateClustercapacityDrainECSHookFunctionAllowInvokeawsecsintegFargateClustercapacityLifecycleHookDrainHookTopic07C1229F3B6FF246": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "FargateClustercapacityDrainECSHookFunction3E60E6D0", + "Arn" + ] + }, + "Principal": "sns.amazonaws.com", + "SourceArn": { + "Ref": "FargateClustercapacityLifecycleHookDrainHookTopic390A0E34" + } + } + }, + "FargateClustercapacityDrainECSHookFunctionTopic7D6C4884": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Protocol": "lambda", + "TopicArn": { + "Ref": "FargateClustercapacityLifecycleHookDrainHookTopic390A0E34" + }, + "Endpoint": { + "Fn::GetAtt": [ + "FargateClustercapacityDrainECSHookFunction3E60E6D0", + "Arn" + ] + } + } + }, + "FargateClustercapacityLifecycleHookDrainHookRoleDD26E39B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "autoscaling.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/FargateCluster/capacity" + } + ] + } + }, + "FargateClustercapacityLifecycleHookDrainHookRoleDefaultPolicyACCDDB70": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "FargateClustercapacityLifecycleHookDrainHookTopic390A0E34" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "FargateClustercapacityLifecycleHookDrainHookRoleDefaultPolicyACCDDB70", + "Roles": [ + { + "Ref": "FargateClustercapacityLifecycleHookDrainHookRoleDD26E39B" + } + ] + } + }, + "FargateClustercapacityLifecycleHookDrainHookTopic390A0E34": { + "Type": "AWS::SNS::Topic", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/FargateCluster/capacity" + } + ] + } + }, + "FargateClustercapacityLifecycleHookDrainHook8AEDE53B": { + "Type": "AWS::AutoScaling::LifecycleHook", + "Properties": { + "AutoScalingGroupName": { + "Ref": "FargateClustercapacityASGE4034F96" + }, + "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING", + "DefaultResult": "CONTINUE", + "HeartbeatTimeout": 300, + "NotificationTargetARN": { + "Ref": "FargateClustercapacityLifecycleHookDrainHookTopic390A0E34" + }, + "RoleARN": { + "Fn::GetAtt": [ + "FargateClustercapacityLifecycleHookDrainHookRoleDD26E39B", + "Arn" + ] + } + }, + "DependsOn": [ + "FargateClustercapacityLifecycleHookDrainHookRoleDefaultPolicyACCDDB70", + "FargateClustercapacityLifecycleHookDrainHookRoleDD26E39B" + ] + }, + "FargateClusterDefaultServiceDiscoveryNamespace04381E1E": { + "Type": "AWS::ServiceDiscovery::PrivateDnsNamespace", + "Properties": { + "Name": "aws-ecs-integ", + "Vpc": { + "Ref": "Vpc8378EB38" + } + } + }, + "TaskDefTaskRole1EDB4A67": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDef54694570": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": "nginx", + "Memory": 512, + "MemoryReservation": 32, + "Name": "nginx", + "PortMappings": [ + { + "ContainerPort": 80, + "HostPort": 0, + "Protocol": "tcp" + } + ] + }, + { + "Environment": [ + { + "Name": "PORT", + "Value": "81" + } + ], + "Essential": true, + "Image": "nathanpeck/name", + "Memory": 512, + "MemoryReservation": 32, + "Name": "name", + "PortMappings": [ + { + "ContainerPort": 81, + "HostPort": 0, + "Protocol": "tcp" + } + ] + } + ], + "Family": "awsecsintegTaskDef6FDFB69A", + "NetworkMode": "bridge", + "RequiresCompatibilities": [ + "EC2" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] + } + } + }, + "ServiceD69D759B": { + "Type": "AWS::ECS::Service", + "Properties": { + "Cluster": { + "Ref": "FargateCluster7CCD5F93" + }, + "DeploymentConfiguration": { + "MaximumPercent": 200, + "MinimumHealthyPercent": 50 + }, + "DesiredCount": 3, + "EnableECSManagedTags": false, + "LaunchType": "EC2", + "SchedulingStrategy": "REPLICA", + "ServiceRegistries": [ + { + "ContainerName": "name", + "ContainerPort": 81, + "RegistryArn": { + "Fn::GetAtt": [ + "ServiceCloudmapService046058A4", + "Arn" + ] + } + } + ], + "TaskDefinition": { + "Ref": "TaskDef54694570" + } + } + }, + "ServiceCloudmapService046058A4": { + "Type": "AWS::ServiceDiscovery::Service", + "Properties": { + "DnsConfig": { + "DnsRecords": [ + { + "TTL": 60, + "Type": "SRV" + } + ], + "NamespaceId": { + "Fn::GetAtt": [ + "FargateClusterDefaultServiceDiscoveryNamespace04381E1E", + "Id" + ] + }, + "RoutingPolicy": "MULTIVALUE" + }, + "HealthCheckCustomConfig": { + "FailureThreshold": 1 + }, + "NamespaceId": { + "Fn::GetAtt": [ + "FargateClusterDefaultServiceDiscoveryNamespace04381E1E", + "Id" + ] + } + } + } + }, + "Parameters": { + "SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.cloudmap-container-port.ts b/packages/@aws-cdk/aws-ecs/test/ec2/integ.cloudmap-container-port.ts new file mode 100644 index 0000000000000..6c1fadbb23666 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.cloudmap-container-port.ts @@ -0,0 +1,70 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cloudmap from '@aws-cdk/aws-servicediscovery'; +import * as cdk from '@aws-cdk/core'; +import * as ecs from '../../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-ecs-integ'); +const vpc = new ec2.Vpc(stack, 'Vpc', { + maxAzs: 2, + subnetConfiguration: [ + { + name: 'pub', + cidrMask: 24, + subnetType: ec2.SubnetType.PUBLIC, + }, + ], +}); +const cluster = new ecs.Cluster(stack, 'FargateCluster', { vpc }); + +const capacity = cluster.addCapacity('capacity', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO), + desiredCapacity: 1, + minCapacity: 1, + maxCapacity: 1, +}); +capacity.connections.allowFromAnyIpv4(ec2.Port.tcpRange(32768, 61000)); + +cluster.addDefaultCloudMapNamespace({ name: 'aws-ecs-integ' }); + +const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', {}); + +// Main container +const mainContainer = taskDefinition.addContainer('nginx', { + image: ecs.ContainerImage.fromRegistry('nginx'), + memoryReservationMiB: 32, + memoryLimitMiB: 512, +}); + +mainContainer.addPortMappings({ + containerPort: 80, + protocol: ecs.Protocol.TCP, +}); + +// Name container with SRV +const nameContainer = taskDefinition.addContainer('name', { + image: ecs.ContainerImage.fromRegistry('nathanpeck/name'), + environment: { + PORT: '81', + }, + memoryReservationMiB: 32, + memoryLimitMiB: 512, +}); + +nameContainer.addPortMappings({ + containerPort: 81, + protocol: ecs.Protocol.TCP, +}); + +new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + desiredCount: 3, + cloudMapOptions: { + container: nameContainer, + containerPort: 81, + dnsRecordType: cloudmap.DnsRecordType.SRV, + }, +}); + +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts index c7aa1fc633a1d..c982920225214 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts @@ -1834,6 +1834,52 @@ nodeunitShim({ test.done(); }, + + 'user can select any container and port'(test: Test) { + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + type: cloudmap.NamespaceType.DNS_PRIVATE, + }); + + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + const mainContainer = taskDefinition.addContainer('MainContainer', { + image: ecs.ContainerImage.fromRegistry('hello'), + memoryLimitMiB: 512, + }); + mainContainer.addPortMappings({ containerPort: 8000 }); + + const otherContainer = taskDefinition.addContainer('OtherContainer', { + image: ecs.ContainerImage.fromRegistry('hello'), + memoryLimitMiB: 512, + }); + otherContainer.addPortMappings({ containerPort: 8001 }); + + new ecs.FargateService(stack, 'Service', { + cluster, + taskDefinition, + cloudMapOptions: { + dnsRecordType: cloudmap.DnsRecordType.SRV, + container: otherContainer, + containerPort: 8001, + }, + }); + + expect(stack).to(haveResourceLike('AWS::ECS::Service', { + ServiceRegistries: [ + { + RegistryArn: { 'Fn::GetAtt': ['ServiceCloudmapService046058A4', 'Arn'] }, + ContainerName: 'OtherContainer', + ContainerPort: 8001, + }, + ], + })); + + test.done(); + }, }, 'Metric'(test: Test) { From 5d57777c10cecaa7b743d91531daccb951b2a91d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Mar 2021 15:40:44 -0800 Subject: [PATCH 08/28] chore(deps): bump actions/setup-node from v2.1.4 to v2.1.5 (#13321) Bumps [actions/setup-node](https://github.com/actions/setup-node) from v2.1.4 to v2.1.5. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v2.1.4...46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Adam Ruka --- .github/workflows/yarn-upgrade.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/yarn-upgrade.yml b/.github/workflows/yarn-upgrade.yml index 17617a75537d3..91daf74640bb8 100644 --- a/.github/workflows/yarn-upgrade.yml +++ b/.github/workflows/yarn-upgrade.yml @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@v2 - name: Set up Node - uses: actions/setup-node@v2.1.4 + uses: actions/setup-node@v2.1.5 with: node-version: 10 From 42f374097248db4d97dc1a3556b04a1c1becd86f Mon Sep 17 00:00:00 2001 From: Robert Djurasaj Date: Fri, 5 Mar 2021 03:44:09 -0700 Subject: [PATCH 09/28] chore(cloudfront): check size of Origin Request headers and prevent forbidden values (#13410) This PR checks the size of Origin Request headers and prevents forbidden values (`Authorization` or `Accept-Encoding`). Closes #13408 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/origin-request-policy.ts | 8 ++++++- .../test/origin-request-policy.test.ts | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-cloudfront/lib/origin-request-policy.ts b/packages/@aws-cdk/aws-cloudfront/lib/origin-request-policy.ts index 3d45cf2bc56fb..17e7894e6e84e 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/origin-request-policy.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/origin-request-policy.ts @@ -121,7 +121,7 @@ export class OriginRequestPolicy extends Resource implements IOriginRequestPolic } /** - * Ddetermines whether any cookies in viewer requests (and if so, which cookies) + * Determines whether any cookies in viewer requests (and if so, which cookies) * are included in requests that CloudFront sends to the origin. */ export class OriginRequestCookieBehavior { @@ -184,6 +184,12 @@ export class OriginRequestHeaderBehavior { if (headers.length === 0) { throw new Error('At least one header to allow must be provided'); } + if (headers.length > 10) { + throw new Error(`Maximum allowed headers in Origin Request Policy is 10; got ${headers.length}.`); + } + if (/Authorization/i.test(headers.join('|')) || /Accept-Encoding/i.test(headers.join('|'))) { + throw new Error('you cannot pass `Authorization` or `Accept-Encoding` as header values; use a CachePolicy to forward these headers instead'); + } return new OriginRequestHeaderBehavior('whitelist', headers); } diff --git a/packages/@aws-cdk/aws-cloudfront/test/origin-request-policy.test.ts b/packages/@aws-cdk/aws-cloudfront/test/origin-request-policy.test.ts index e719225c4845c..b342ac434e48e 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/origin-request-policy.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/origin-request-policy.test.ts @@ -77,6 +77,29 @@ describe('OriginRequestPolicy', () => { expect(() => new OriginRequestPolicy(stack, 'OriginRequestPolicy6', { originRequestPolicyName: 'My_Policy' })).not.toThrow(); }); + test('throws if prohibited headers are being passed', () => { + const errorMessage = /you cannot pass `Authorization` or `Accept-Encoding` as header values/; + expect(() => new OriginRequestPolicy(stack, 'OriginRequestPolicy1', { headerBehavior: OriginRequestHeaderBehavior.allowList('Authorization') })).toThrow(errorMessage); + expect(() => new OriginRequestPolicy(stack, 'OriginRequestPolicy2', { headerBehavior: OriginRequestHeaderBehavior.allowList('Accept-Encoding') })).toThrow(errorMessage); + expect(() => new OriginRequestPolicy(stack, 'OriginRequestPolicy3', { headerBehavior: OriginRequestHeaderBehavior.allowList('authorization') })).toThrow(errorMessage); + expect(() => new OriginRequestPolicy(stack, 'OriginRequestPolicy4', { headerBehavior: OriginRequestHeaderBehavior.allowList('accept-encoding') })).toThrow(errorMessage); + expect(() => new OriginRequestPolicy(stack, 'OriginRequestPolicy5', { headerBehavior: OriginRequestHeaderBehavior.allowList('Foo', 'Authorization', 'Bar') })).toThrow(errorMessage); + expect(() => new OriginRequestPolicy(stack, 'OriginRequestPolicy6', { headerBehavior: OriginRequestHeaderBehavior.allowList('Foo', 'Accept-Encoding', 'Bar') })).toThrow(errorMessage); + + expect(() => new OriginRequestPolicy(stack, 'OriginRequestPolicy7', { headerBehavior: OriginRequestHeaderBehavior.allowList('Foo', 'Bar') })).not.toThrow(); + }); + + test('throws if more than 10 OriginRequestHeaderBehavior headers are being passed', () => { + const errorMessage = /Maximum allowed headers in Origin Request Policy is 10; got (.*?)/; + expect(() => new OriginRequestPolicy(stack, 'OriginRequestPolicy1', { + headerBehavior: OriginRequestHeaderBehavior.allowList('Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'do', 'eiusmod'), + })).toThrow(errorMessage); + + expect(() => new OriginRequestPolicy(stack, 'OriginRequestPolicy2', { + headerBehavior: OriginRequestHeaderBehavior.allowList('Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'do'), + })).not.toThrow(); + }); + test('does not throw if originRequestPolicyName is a token', () => { expect(() => new OriginRequestPolicy(stack, 'CachePolicy', { originRequestPolicyName: Aws.STACK_NAME, From 2672a55c393e5ce7dd9a230d921ec1be1a23e32a Mon Sep 17 00:00:00 2001 From: Luke Thompson Date: Sat, 6 Mar 2021 01:51:43 +1100 Subject: [PATCH 10/28] feat(aws-route53-targets): add global accelerator target to route53 alias targets (#13407) Closes #12839 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-route53-targets/README.md | 13 ++++ .../lib/global-accelerator-target.ts | 41 +++++++++++++ .../@aws-cdk/aws-route53-targets/lib/index.ts | 1 + .../@aws-cdk/aws-route53-targets/package.json | 2 + .../test/global-accelerator-target.test.ts | 59 +++++++++++++++++++ ...obalaccelerator-alias-target.expected.json | 52 ++++++++++++++++ .../integ.globalaccelerator-alias-target.ts | 31 ++++++++++ 7 files changed, 199 insertions(+) create mode 100644 packages/@aws-cdk/aws-route53-targets/lib/global-accelerator-target.ts create mode 100644 packages/@aws-cdk/aws-route53-targets/test/global-accelerator-target.test.ts create mode 100644 packages/@aws-cdk/aws-route53-targets/test/integ.globalaccelerator-alias-target.expected.json create mode 100644 packages/@aws-cdk/aws-route53-targets/test/integ.globalaccelerator-alias-target.ts diff --git a/packages/@aws-cdk/aws-route53-targets/README.md b/packages/@aws-cdk/aws-route53-targets/README.md index 5270482564ee8..ba9f1ea7e4831 100644 --- a/packages/@aws-cdk/aws-route53-targets/README.md +++ b/packages/@aws-cdk/aws-route53-targets/README.md @@ -63,6 +63,19 @@ This library contains Route53 Alias Record targets for: For example, if the Amazon-provided DNS for the load balancer is `ALB-xxxxxxx.us-west-2.elb.amazonaws.com`, CDK will create alias target in Route 53 will be `dualstack.ALB-xxxxxxx.us-west-2.elb.amazonaws.com`. +* GlobalAccelerator + + ```ts + new route53.ARecord(stack, 'AliasRecord', { + zone, + target: route53.RecordTarget.fromAlias(new targets.GlobalAcceleratorTarget(accelerator)), + // or - route53.RecordTarget.fromAlias(new targets.GlobalAcceleratorDomainTarget('xyz.awsglobalaccelerator.com')), + }); + ``` + +**Important:** If you use GlobalAcceleratorDomainTarget, passing a string rather than an instance of IAccelerator, ensure that the string is a valid domain name of an existing Global Accelerator instance. +See [the documentation on DNS addressing](https://docs.aws.amazon.com/global-accelerator/latest/dg/dns-addressing-custom-domains.dns-addressing.html) with Global Accelerator for more info. + * InterfaceVpcEndpoints **Important:** Based on the CFN docs for VPCEndpoints - [see here](attrDnsEntries) - the attributes returned for DnsEntries in CloudFormation is a combination of the hosted zone ID and the DNS name. The entries are ordered as follows: regional public DNS, zonal public DNS, private DNS, and wildcard DNS. This order is not enforced for AWS Marketplace services, and therefore this CDK construct is ONLY guaranteed to work with non-marketplace services. diff --git a/packages/@aws-cdk/aws-route53-targets/lib/global-accelerator-target.ts b/packages/@aws-cdk/aws-route53-targets/lib/global-accelerator-target.ts new file mode 100644 index 0000000000000..d80aaaa140b1b --- /dev/null +++ b/packages/@aws-cdk/aws-route53-targets/lib/global-accelerator-target.ts @@ -0,0 +1,41 @@ +import * as globalaccelerator from '@aws-cdk/aws-globalaccelerator'; +import * as route53 from '@aws-cdk/aws-route53'; + + +/** + * Use a Global Accelerator domain name as an alias record target. + */ +export class GlobalAcceleratorDomainTarget implements route53.IAliasRecordTarget { + /** + * The hosted zone Id if using an alias record in Route53. + * This value never changes. + * Ref: https://docs.aws.amazon.com/general/latest/gr/global_accelerator.html + */ + public static readonly GLOBAL_ACCELERATOR_ZONE_ID = 'Z2BJ6XQ5FK7U4H'; + + /** + * Create an Alias Target for a Global Accelerator domain name. + */ + constructor(private readonly acceleratorDomainName: string) { + } + + bind(_record: route53.IRecordSet): route53.AliasRecordTargetConfig { + return { + hostedZoneId: GlobalAcceleratorTarget.GLOBAL_ACCELERATOR_ZONE_ID, + dnsName: this.acceleratorDomainName, + }; + } +} + +/** + * Use a Global Accelerator instance domain name as an alias record target. + */ +export class GlobalAcceleratorTarget extends GlobalAcceleratorDomainTarget { + + /** + * Create an Alias Target for a Global Accelerator instance. + */ + constructor(accelerator: globalaccelerator.IAccelerator) { + super(accelerator.dnsName); + } +} diff --git a/packages/@aws-cdk/aws-route53-targets/lib/index.ts b/packages/@aws-cdk/aws-route53-targets/lib/index.ts index af574aa599519..5c8b86fb959c1 100644 --- a/packages/@aws-cdk/aws-route53-targets/lib/index.ts +++ b/packages/@aws-cdk/aws-route53-targets/lib/index.ts @@ -6,3 +6,4 @@ export * from './cloudfront-target'; export * from './load-balancer-target'; export * from './interface-vpc-endpoint-target'; export * from './userpool-domain'; +export * from './global-accelerator-target'; diff --git a/packages/@aws-cdk/aws-route53-targets/package.json b/packages/@aws-cdk/aws-route53-targets/package.json index 08ea2cb9e91f4..d98999baaab6a 100644 --- a/packages/@aws-cdk/aws-route53-targets/package.json +++ b/packages/@aws-cdk/aws-route53-targets/package.json @@ -80,6 +80,7 @@ "@aws-cdk/aws-elasticloadbalancing": "0.0.0", "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-globalaccelerator": "0.0.0", "@aws-cdk/aws-route53": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/core": "0.0.0", @@ -96,6 +97,7 @@ "@aws-cdk/aws-elasticloadbalancing": "0.0.0", "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-globalaccelerator": "0.0.0", "@aws-cdk/aws-route53": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-route53-targets/test/global-accelerator-target.test.ts b/packages/@aws-cdk/aws-route53-targets/test/global-accelerator-target.test.ts new file mode 100644 index 0000000000000..07db27b92940e --- /dev/null +++ b/packages/@aws-cdk/aws-route53-targets/test/global-accelerator-target.test.ts @@ -0,0 +1,59 @@ +import '@aws-cdk/assert/jest'; +import * as globalaccelerator from '@aws-cdk/aws-globalaccelerator'; +import * as route53 from '@aws-cdk/aws-route53'; +import { Stack } from '@aws-cdk/core'; +import * as targets from '../lib'; + +test('GlobalAcceleratorTarget exposes a public constant of the zone id', () => { + expect(targets.GlobalAcceleratorTarget.GLOBAL_ACCELERATOR_ZONE_ID).toStrictEqual('Z2BJ6XQ5FK7U4H'); + expect(targets.GlobalAcceleratorDomainTarget.GLOBAL_ACCELERATOR_ZONE_ID).toStrictEqual('Z2BJ6XQ5FK7U4H'); +}); + +test('GlobalAcceleratorTarget creates an alias resource with a string domain name', () => { + // GIVEN + const stack = new Stack(); + const zone = new route53.PublicHostedZone(stack, 'HostedZone', { zoneName: 'test.public' }); + + // WHEN + new route53.ARecord(stack, 'GlobalAcceleratorAlias', { + target: route53.RecordTarget.fromAlias(new targets.GlobalAcceleratorDomainTarget('xyz.awsglobalaccelerator.com')), + recordName: 'test', + zone, + }); + + // THEN + expect(stack).toHaveResource('AWS::Route53::RecordSet', { + AliasTarget: { + DNSName: 'xyz.awsglobalaccelerator.com', + HostedZoneId: 'Z2BJ6XQ5FK7U4H', + }, + }); +}); + +test('GlobalAcceleratorTarget creates an alias resource with a Global Accelerator reference domain name', () => { + // GIVEN + const stack = new Stack(); + const accelerator = new globalaccelerator.Accelerator(stack, 'Accelerator'); + const logicalId = stack.getLogicalId(accelerator.node.defaultChild); + const zone = new route53.PublicHostedZone(stack, 'HostedZone', { zoneName: 'test.public' }); + + // WHEN + new route53.ARecord(stack, 'GlobalAcceleratorAlias', { + target: route53.RecordTarget.fromAlias(new targets.GlobalAcceleratorTarget(accelerator)), + recordName: 'test', + zone, + }); + + // THEN + expect(stack).toHaveResource('AWS::Route53::RecordSet', { + AliasTarget: { + DNSName: { + 'Fn::GetAtt': [ + logicalId, + 'DnsName', + ], + }, + HostedZoneId: 'Z2BJ6XQ5FK7U4H', + }, + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53-targets/test/integ.globalaccelerator-alias-target.expected.json b/packages/@aws-cdk/aws-route53-targets/test/integ.globalaccelerator-alias-target.expected.json new file mode 100644 index 0000000000000..e11d21275bbab --- /dev/null +++ b/packages/@aws-cdk/aws-route53-targets/test/integ.globalaccelerator-alias-target.expected.json @@ -0,0 +1,52 @@ +{ + "Resources": { + "Accelerator8EB0B6B1": { + "Type": "AWS::GlobalAccelerator::Accelerator", + "Properties": { + "Name": "aws-cdk-globalaccelerator-integ", + "Enabled": true + } + }, + "HostedZoneDB99F866": { + "Type": "AWS::Route53::HostedZone", + "Properties": { + "Name": "test.public." + } + }, + "LocalGlobalAcceleratorAlias18B4A87A": { + "Type": "AWS::Route53::RecordSet", + "Properties": { + "Name": "test-local.test.public.", + "Type": "A", + "AliasTarget": { + "DNSName": { + "Fn::GetAtt": [ + "Accelerator8EB0B6B1", + "DnsName" + ] + }, + "HostedZoneId": "Z2BJ6XQ5FK7U4H" + }, + "Comment": "Alias to the locally created Global Accelerator", + "HostedZoneId": { + "Ref": "HostedZoneDB99F866" + } + } + }, + "ExistingGlobalAcceleratorAlias7ACF888C": { + "Type": "AWS::Route53::RecordSet", + "Properties": { + "Name": "test-existing.test.public.", + "Type": "A", + "AliasTarget": { + "DNSName": "someexisting.awsglobalaccelerator.com", + "HostedZoneId": "Z2BJ6XQ5FK7U4H" + }, + "Comment": "Alias to the an existing Global Accelerator", + "HostedZoneId": { + "Ref": "HostedZoneDB99F866" + } + } + } + } + } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53-targets/test/integ.globalaccelerator-alias-target.ts b/packages/@aws-cdk/aws-route53-targets/test/integ.globalaccelerator-alias-target.ts new file mode 100644 index 0000000000000..560e828accaa5 --- /dev/null +++ b/packages/@aws-cdk/aws-route53-targets/test/integ.globalaccelerator-alias-target.ts @@ -0,0 +1,31 @@ +#!/usr/bin/env node +import * as globalaccelerator from '@aws-cdk/aws-globalaccelerator'; +import * as route53 from '@aws-cdk/aws-route53'; +import * as cdk from '@aws-cdk/core'; +import * as targets from '../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-cdk-globalaccelerator-integ'); + +let accelerator = new globalaccelerator.Accelerator(stack, 'Accelerator', { + acceleratorName: `${stack.stackName}`, + enabled: true, +}); + +const zone = new route53.PublicHostedZone(stack, 'HostedZone', { zoneName: 'test.public' }); + +new route53.ARecord(stack, 'LocalGlobalAcceleratorAlias', { + comment: 'Alias to the locally created Global Accelerator', + target: route53.RecordTarget.fromAlias(new targets.GlobalAcceleratorTarget(accelerator)), + recordName: 'test-local', + zone, +}); + +new route53.ARecord(stack, 'ExistingGlobalAcceleratorAlias', { + comment: 'Alias to the an existing Global Accelerator', + target: route53.RecordTarget.fromAlias(new targets.GlobalAcceleratorDomainTarget('someexisting.awsglobalaccelerator.com')), + recordName: 'test-existing', + zone, +}); + +app.synth(); From 8e612ca4d9cf3c6a480c949f2a40fc07a49e7a1b Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Fri, 5 Mar 2021 15:00:01 +0000 Subject: [PATCH 11/28] chore: linter action should run on edits and label adjusts (#13398) Currently, the linter only runs when the PR is opened. This naturally means that when the title, description or labels (that it complains about) is fixed, it does not re-run and confirm that it passes. Adjust the action so it also runs when the PR labels, title or descriptions are modified. --- .github/workflows/pr-linter.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-linter.yml b/.github/workflows/pr-linter.yml index bb8c6dd66943b..5702b254d4a0b 100644 --- a/.github/workflows/pr-linter.yml +++ b/.github/workflows/pr-linter.yml @@ -2,7 +2,15 @@ # https://github.com/actions/toolkit/blob/master/packages/github/src/context.ts name: PR Linter -on: pull_request +on: + pull_request: + types: + - labeled + - unlabeled + - edited + - opened + - synchronize + - reopened jobs: validate-pr: From fe1c8393e0840fb273c4a5f325cb3cebc784bf4b Mon Sep 17 00:00:00 2001 From: Ayush Goyal Date: Fri, 5 Mar 2021 21:35:55 +0530 Subject: [PATCH 12/28] feat(apigatewayv2): websocket api (#13031) feat(apigatewayv2): add support for WebSocket APIs BREAKING CHANGE: `HttpApiMapping` (and related interfaces for `Attributed` and `Props`) has been renamed to `ApiMapping` * **apigatewayv2:** `CommonStageOptions` has been renamed to `StageOptions` * **apigatewayv2:** `HttpStage.fromStageName` has been removed in favour of `HttpStage.fromHttpStageAttributes` * **apigatewayv2:** `DefaultDomainMappingOptions` has been removed in favour of `DomainMappingOptions` * **apigatewayv2:** `HttpApiProps.defaultDomainMapping` has been changed from `DefaultDomainMappingOptions` to `DomainMappingOptions` * **apigatewayv2:** `HttpApi.defaultStage` has been changed from `HttpStage` to `IStage` * **apigatewayv2:** `IHttpApi.defaultStage` has been removed closes #2872 Some notes: 1. Only Lambda Integration is currently supported 2. No support for `IntegrationResponse` and `RouteResponse`. 3. The `$default` stageName does not seem to work for WebSocket APIs. Therefore modified the API for defaultStage in the API. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-apigatewayv2-integrations/README.md | 31 + .../lib/index.ts | 1 + .../lib/websocket/index.ts | 1 + .../lib/websocket/lambda.ts | 44 ++ .../test/websocket/integ.lambda.expected.json | 534 ++++++++++++++++++ .../test/websocket/integ.lambda.ts | 54 ++ .../test/websocket/lambda.test.ts | 35 ++ packages/@aws-cdk/aws-apigatewayv2/README.md | 48 +- .../lib/common/api-mapping.ts | 114 +++- .../aws-apigatewayv2/lib/common/api.ts | 71 +++ .../aws-apigatewayv2/lib/common/base.ts | 111 ++++ .../aws-apigatewayv2/lib/common/index.ts | 1 + .../lib/common/integration.ts | 2 +- .../aws-apigatewayv2/lib/common/stage.ts | 99 +++- .../aws-apigatewayv2/lib/http/api-mapping.ts | 109 ---- .../@aws-cdk/aws-apigatewayv2/lib/http/api.ts | 130 +---- .../aws-apigatewayv2/lib/http/index.ts | 1 - .../aws-apigatewayv2/lib/http/stage.ts | 154 ++--- .../@aws-cdk/aws-apigatewayv2/lib/index.ts | 3 +- .../lib/private/integration-cache.ts | 29 + .../aws-apigatewayv2/lib/websocket/api.ts | 130 +++++ .../aws-apigatewayv2/lib/websocket/index.ts | 4 + .../lib/websocket/integration.ts | 110 ++++ .../aws-apigatewayv2/lib/websocket/route.ts | 84 +++ .../aws-apigatewayv2/lib/websocket/stage.ts | 96 ++++ .../@aws-cdk/aws-apigatewayv2/package.json | 13 +- .../test/{http => common}/api-mapping.test.ts | 62 +- .../aws-apigatewayv2/test/http/api.test.ts | 18 +- .../aws-apigatewayv2/test/http/stage.test.ts | 5 +- .../test/websocket/api.test.ts | 92 +++ .../test/websocket/route.test.ts | 54 ++ .../test/websocket/stage.test.ts | 44 ++ 32 files changed, 1904 insertions(+), 380 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/index.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/lambda.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json create mode 100644 packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/lambda.test.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/common/api.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/common/base.ts delete mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/http/api-mapping.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/private/integration-cache.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/websocket/index.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/websocket/integration.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts rename packages/@aws-cdk/aws-apigatewayv2/test/{http => common}/api-mapping.test.ts (76%) create mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts create mode 100644 packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md b/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md index 6dd9de9e4e475..cce77fd6398e6 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md @@ -21,6 +21,8 @@ - [Lambda Integration](#lambda) - [HTTP Proxy Integration](#http-proxy) - [Private Integration](#private-integration) +- [WebSocket APIs](#websocket-apis) + - [Lambda WebSocket Integration](#lambda-websocket-integration) ## HTTP APIs @@ -146,3 +148,32 @@ const httpEndpoint = new HttpApi(stack, 'HttpProxyPrivateApi', { }), }); ``` + +## WebSocket APIs + +WebSocket integrations connect a route to backend resources. The following integrations are supported in the CDK. + +### Lambda WebSocket Integration + +Lambda integrations enable integrating a WebSocket API route with a Lambda function. When a client connects/disconnects +or sends message specific to a route, the API Gateway service forwards the request to the Lambda function + +The API Gateway service will invoke the lambda function with an event payload of a specific format. + +The following code configures a `sendmessage` route with a Lambda integration + +```ts +const webSocketApi = new WebSocketApi(stack, 'mywsapi'); +new WebSocketStage(stack, 'mystage', { + webSocketApi, + stageName: 'dev', + autoDeploy: true, +}); + +const messageHandler = new lambda.Function(stack, 'MessageHandler', {...}); +webSocketApi.addRoute('sendmessage', { + integration: new LambdaWebSocketIntegration({ + handler: connectHandler, + }), +}); +``` diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/index.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/index.ts index c202386ae710e..fd16aff655ff2 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/index.ts @@ -1 +1,2 @@ export * from './http'; +export * from './websocket'; diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/index.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/index.ts new file mode 100644 index 0000000000000..04a64da0c7540 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/index.ts @@ -0,0 +1 @@ +export * from './lambda'; diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/lambda.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/lambda.ts new file mode 100644 index 0000000000000..85e199a71c3d7 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/lambda.ts @@ -0,0 +1,44 @@ +import { + IWebSocketRouteIntegration, + WebSocketIntegrationType, + WebSocketRouteIntegrationBindOptions, + WebSocketRouteIntegrationConfig, +} from '@aws-cdk/aws-apigatewayv2'; +import { ServicePrincipal } from '@aws-cdk/aws-iam'; +import { IFunction } from '@aws-cdk/aws-lambda'; +import { Names, Stack } from '@aws-cdk/core'; + +/** + * Lambda WebSocket Integration props + */ +export interface LambdaWebSocketIntegrationProps { + /** + * The handler for this integration. + */ + readonly handler: IFunction +} + +/** + * Lambda WebSocket Integration + */ +export class LambdaWebSocketIntegration implements IWebSocketRouteIntegration { + constructor(private props: LambdaWebSocketIntegrationProps) {} + + bind(options: WebSocketRouteIntegrationBindOptions): WebSocketRouteIntegrationConfig { + const route = options.route; + this.props.handler.addPermission(`${Names.nodeUniqueId(route.node)}-Permission`, { + scope: options.scope, + principal: new ServicePrincipal('apigateway.amazonaws.com'), + sourceArn: Stack.of(route).formatArn({ + service: 'execute-api', + resource: route.webSocketApi.apiId, + resourceName: `*/*${route.routeKey}`, + }), + }); + + return { + type: WebSocketIntegrationType.AWS_PROXY, + uri: this.props.handler.functionArn, + }; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json new file mode 100644 index 0000000000000..48bf164ada435 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json @@ -0,0 +1,534 @@ +{ + "Resources": { + "ConnectHandlerServiceRole7E4A9B1F": { + "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" + ] + ] + } + ] + } + }, + "ConnectHandler2FFD52D8": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: \"connected\" }; };" + }, + "Role": { + "Fn::GetAtt": [ + "ConnectHandlerServiceRole7E4A9B1F", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "ConnectHandlerServiceRole7E4A9B1F" + ] + }, + "DisconnectHandlerServiceRoleE54F14F9": { + "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" + ] + ] + } + ] + } + }, + "DisconnectHandlerCB7ED6F7": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: \"disconnected\" }; };" + }, + "Role": { + "Fn::GetAtt": [ + "DisconnectHandlerServiceRoleE54F14F9", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "DisconnectHandlerServiceRoleE54F14F9" + ] + }, + "DefaultHandlerServiceRoleDF00569C": { + "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" + ] + ] + } + ] + } + }, + "DefaultHandler604DF7AC": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: \"default\" }; };" + }, + "Role": { + "Fn::GetAtt": [ + "DefaultHandlerServiceRoleDF00569C", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "DefaultHandlerServiceRoleDF00569C" + ] + }, + "MessageHandlerServiceRoleDF05266A": { + "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" + ] + ] + } + ] + } + }, + "MessageHandlerDFBBCD6B": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: \"received\" }; };" + }, + "Role": { + "Fn::GetAtt": [ + "MessageHandlerServiceRoleDF05266A", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "MessageHandlerServiceRoleDF05266A" + ] + }, + "mywsapi32E6CE11": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "mywsapi", + "ProtocolType": "WEBSOCKET", + "RouteSelectionExpression": "$request.body.action" + } + }, + "mywsapiconnectRouteWebSocketApiIntegmywsapiconnectRoute456CB290Permission2D0BC294": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "ConnectHandler2FFD52D8", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "mywsapi32E6CE11" + }, + "/*/*$connect" + ] + ] + } + } + }, + "mywsapiconnectRouteWebSocketIntegration50b017444a02be00a0b575d123314581176017EE": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "ConnectHandler2FFD52D8", + "Arn" + ] + } + } + }, + "mywsapiconnectRoute45A0ED6A": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "RouteKey": "$connect", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mywsapiconnectRouteWebSocketIntegration50b017444a02be00a0b575d123314581176017EE" + } + ] + ] + } + } + }, + "mywsapidisconnectRouteWebSocketApiIntegmywsapidisconnectRoute26B84CF3PermissionB3F6D0A8": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "DisconnectHandlerCB7ED6F7", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "mywsapi32E6CE11" + }, + "/*/*$disconnect" + ] + ] + } + } + }, + "mywsapidisconnectRouteWebSocketIntegrationcd3bacb451e82549501e141cc094d7ba1F7F68BC": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "DisconnectHandlerCB7ED6F7", + "Arn" + ] + } + } + }, + "mywsapidisconnectRoute421A8CB9": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "RouteKey": "$disconnect", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mywsapidisconnectRouteWebSocketIntegrationcd3bacb451e82549501e141cc094d7ba1F7F68BC" + } + ] + ] + } + } + }, + "mywsapidefaultRouteWebSocketApiIntegmywsapidefaultRouteA13D926BPermission58B64FCE": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "DefaultHandler604DF7AC", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "mywsapi32E6CE11" + }, + "/*/*$default" + ] + ] + } + } + }, + "mywsapidefaultRouteWebSocketIntegration640ac0772c157aa8b9a56aa99adbd9d7A2B7F2FA": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "DefaultHandler604DF7AC", + "Arn" + ] + } + } + }, + "mywsapidefaultRouteE9382DF8": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "RouteKey": "$default", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mywsapidefaultRouteWebSocketIntegration640ac0772c157aa8b9a56aa99adbd9d7A2B7F2FA" + } + ] + ] + } + } + }, + "mywsapisendmessageRouteWebSocketApiIntegmywsapisendmessageRoute8A775F3CPermission660FB575": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MessageHandlerDFBBCD6B", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "mywsapi32E6CE11" + }, + "/*/*sendmessage" + ] + ] + } + } + }, + "mywsapisendmessageRouteWebSocketIntegrationcf58a195e318f43f52c4d9ac6d6d2430786B6471": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "MessageHandlerDFBBCD6B", + "Arn" + ] + } + } + }, + "mywsapisendmessageRouteAE873328": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "RouteKey": "sendmessage", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mywsapisendmessageRouteWebSocketIntegrationcf58a195e318f43f52c4d9ac6d6d2430786B6471" + } + ] + ] + } + } + }, + "mystage114C35EC": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "StageName": "dev", + "AutoDeploy": true + } + } + }, + "Outputs": { + "ApiEndpoint": { + "Value": { + "Fn::Join": [ + "", + [ + "wss://", + { + "Ref": "mywsapi32E6CE11" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/dev" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts new file mode 100644 index 0000000000000..01e25f906b0f8 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts @@ -0,0 +1,54 @@ +import { WebSocketApi, WebSocketStage } from '@aws-cdk/aws-apigatewayv2'; +import * as lambda from '@aws-cdk/aws-lambda'; +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { LambdaWebSocketIntegration } from '../../lib'; + +/* + * Stack verification steps: + * 1. Connect: 'wscat -c '. Should connect successfully and print event data containing connectionId in cloudwatch + * 2. SendMessage: '> {"action": "sendmessage", "data": "some-data"}'. Should send the message successfully and print the data in cloudwatch + * 3. Default: '> {"data": "some-data"}'. Should send the message successfully and print the data in cloudwatch + * 4. Disconnect: disconnect from the wscat. Should print event data containing connectionId in cloudwatch + */ + +const app = new App(); +const stack = new Stack(app, 'WebSocketApiInteg'); + +const connectHandler = new lambda.Function(stack, 'ConnectHandler', { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: new lambda.InlineCode('exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: "connected" }; };'), +}); + +const disconnetHandler = new lambda.Function(stack, 'DisconnectHandler', { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: new lambda.InlineCode('exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: "disconnected" }; };'), +}); + +const defaultHandler = new lambda.Function(stack, 'DefaultHandler', { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: new lambda.InlineCode('exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: "default" }; };'), +}); + +const messageHandler = new lambda.Function(stack, 'MessageHandler', { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: new lambda.InlineCode('exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: "received" }; };'), +}); + +const webSocketApi = new WebSocketApi(stack, 'mywsapi', { + connectRouteOptions: { integration: new LambdaWebSocketIntegration({ handler: connectHandler }) }, + disconnectRouteOptions: { integration: new LambdaWebSocketIntegration({ handler: disconnetHandler }) }, + defaultRouteOptions: { integration: new LambdaWebSocketIntegration({ handler: defaultHandler }) }, +}); +const stage = new WebSocketStage(stack, 'mystage', { + webSocketApi, + stageName: 'dev', + autoDeploy: true, +}); + +webSocketApi.addRoute('sendmessage', { integration: new LambdaWebSocketIntegration({ handler: messageHandler }) }); + +new CfnOutput(stack, 'ApiEndpoint', { value: stage.url }); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/lambda.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/lambda.test.ts new file mode 100644 index 0000000000000..5f431ca28fc49 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/lambda.test.ts @@ -0,0 +1,35 @@ +import '@aws-cdk/assert/jest'; +import { WebSocketApi } from '@aws-cdk/aws-apigatewayv2'; +import { Code, Function, Runtime } from '@aws-cdk/aws-lambda'; +import { Stack } from '@aws-cdk/core'; +import { LambdaWebSocketIntegration } from '../../lib'; + + +describe('LambdaWebSocketIntegration', () => { + test('default', () => { + // GIVEN + const stack = new Stack(); + const fooFn = fooFunction(stack, 'Fn'); + + // WHEN + new WebSocketApi(stack, 'Api', { + connectRouteOptions: { + integration: new LambdaWebSocketIntegration({ handler: fooFn }), + }, + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'AWS_PROXY', + IntegrationUri: stack.resolve(fooFn.functionArn), + }); + }); +}); + +function fooFunction(stack: Stack, id: string) { + return new Function(stack, id, { + code: Code.fromInline('foo'), + runtime: Runtime.NODEJS_12_X, + handler: 'index.handler', + }); +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/README.md b/packages/@aws-cdk/aws-apigatewayv2/README.md index 4da900f271e8f..d8278a800a00f 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2/README.md @@ -7,7 +7,7 @@ Features | Stability -------------------------------------------|-------------------------------------------------------- CFN Resources | ![Stable](https://img.shields.io/badge/stable-success.svg?style=for-the-badge) Higher level constructs for HTTP APIs | ![Experimental](https://img.shields.io/badge/experimental-important.svg?style=for-the-badge) -Higher level constructs for Websocket APIs | ![Not Implemented](https://img.shields.io/badge/not--implemented-black.svg?style=for-the-badge) +Higher level constructs for Websocket APIs | ![Experimental](https://img.shields.io/badge/experimental-important.svg?style=for-the-badge) > **CFN Resources:** All classes with the `Cfn` prefix in this module ([CFN Resources]) are always > stable and safe to use. @@ -38,6 +38,7 @@ Higher level constructs for Websocket APIs | ![Not Implemented](https://img.shie - [Metrics](#metrics) - [VPC Link](#vpc-link) - [Private Integration](#private-integration) +- [WebSocket API](#websocket-api) ## Introduction @@ -230,7 +231,7 @@ API](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-acces These authorizers can be found in the [APIGatewayV2-Authorizers](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-apigatewayv2-authorizers-readme.html) constructs library. -## Metrics +### Metrics The API Gateway v2 service sends metrics around the performance of HTTP APIs to Amazon CloudWatch. These metrics can be referred to using the metric APIs available on the `HttpApi` construct. @@ -277,3 +278,46 @@ Amazon ECS container-based applications. Using private integrations, resources clients outside of the VPC. These integrations can be found in the [APIGatewayV2-Integrations](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-apigatewayv2-integrations-readme.html) constructs library. + +## WebSocket API + +A WebSocket API in API Gateway is a collection of WebSocket routes that are integrated with backend HTTP endpoints, +Lambda functions, or other AWS services. You can use API Gateway features to help you with all aspects of the API +lifecycle, from creation through monitoring your production APIs. [Read more](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-overview.html) + +WebSocket APIs have two fundamental concepts - Routes and Integrations. + +WebSocket APIs direct JSON messages to backend integrations based on configured routes. (Non-JSON messages are directed +to the configured `$default` route.) + +Integrations define how the WebSocket API behaves when a client reaches a specific Route. Learn more at +[Configuring integrations](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-integration-requests.html). + +Integrations are available in the `aws-apigatewayv2-integrations` module and more information is available in that module. + +To add the default WebSocket routes supported by API Gateway (`$connect`, `$disconnect` and `$default`), configure them as part of api props: + +```ts +const webSocketApi = new WebSocketApi(stack, 'mywsapi', { + connectRouteOptions: { integration: new LambdaWebSocketIntegration({ handler: connectHandler }) }, + disconnectRouteOptions: { integration: new LambdaWebSocketIntegration({ handler: disconnetHandler }) }, + defaultRouteOptions: { integration: new LambdaWebSocketIntegration({ handler: defaultHandler }) }, +}); + +new WebSocketStage(stack, 'mystage', { + webSocketApi, + stageName: 'dev', + autoDeploy: true, +}); +``` + +To add any other route: + +```ts +const webSocketApi = new WebSocketApi(stack, 'mywsapi'); +webSocketApi.addRoute('sendmessage', { + integration: new LambdaWebSocketIntegration({ + handler: messageHandler, + }), +}); +``` diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts index d843b51f8b315..adbe3fe3efc2c 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts @@ -1,4 +1,10 @@ -import { IResource } from '@aws-cdk/core'; +import { IResource, Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnApiMapping, CfnApiMappingProps } from '../apigatewayv2.generated'; +import { HttpApi } from '../http/api'; +import { IApi } from './api'; +import { IDomainName } from './domain-name'; +import { IStage } from './stage'; /** * Represents an ApiGatewayV2 ApiMapping resource @@ -11,3 +17,109 @@ export interface IApiMapping extends IResource { */ readonly apiMappingId: string; } + +/** + * Properties used to create the ApiMapping resource + */ +export interface ApiMappingProps { + /** + * Api mapping key. The path where this stage should be mapped to on the domain + * @default - undefined for the root path mapping. + */ + readonly apiMappingKey?: string; + + /** + * The Api to which this mapping is applied + */ + readonly api: IApi; + + /** + * custom domain name of the mapping target + */ + readonly domainName: IDomainName; + + /** + * stage for the ApiMapping resource + * required for WebSocket API + * defaults to default stage of an HTTP API + * + * @default - Default stage of the passed API for HTTP API, required for WebSocket API + */ + readonly stage?: IStage; +} + +/** + * The attributes used to import existing ApiMapping + */ +export interface ApiMappingAttributes { + /** + * The API mapping ID + */ + readonly apiMappingId: string; +} + +/** + * Create a new API mapping for API Gateway API endpoint. + * @resource AWS::ApiGatewayV2::ApiMapping + */ +export class ApiMapping extends Resource implements IApiMapping { + /** + * import from API ID + */ + public static fromApiMappingAttributes(scope: Construct, id: string, attrs: ApiMappingAttributes): IApiMapping { + class Import extends Resource implements IApiMapping { + public readonly apiMappingId = attrs.apiMappingId; + } + return new Import(scope, id); + } + /** + * ID of the API Mapping + */ + public readonly apiMappingId: string; + + /** + * API Mapping key + */ + public readonly mappingKey?: string; + + constructor(scope: Construct, id: string, props: ApiMappingProps) { + super(scope, id); + + let stage = props.stage; + if (!stage) { + if (props.api instanceof HttpApi) { + if (props.api.defaultStage) { + stage = props.api.defaultStage; + } else { + throw new Error('stage is required if default stage is not available'); + } + } else { + throw new Error('stage is required for WebSocket API'); + } + } + + const paramRe = '^[a-zA-Z0-9]*[-_.+!,$]?[a-zA-Z0-9]*$'; + if (props.apiMappingKey && !new RegExp(paramRe).test(props.apiMappingKey)) { + throw new Error('An ApiMapping key may contain only letters, numbers and one of $-_.+!*\'(),'); + } + + if (props.apiMappingKey === '') { + throw new Error('empty string for api mapping key not allowed'); + } + + const apiMappingProps: CfnApiMappingProps = { + apiId: props.api.apiId, + domainName: props.domainName.name, + stage: stage.stageName, + apiMappingKey: props.apiMappingKey, + }; + + const resource = new CfnApiMapping(this, 'Resource', apiMappingProps); + + // ensure the dependency on the provided stage + this.node.addDependency(stage); + + this.apiMappingId = resource.ref; + this.mappingKey = props.apiMappingKey; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api.ts new file mode 100644 index 0000000000000..c632e6309083d --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api.ts @@ -0,0 +1,71 @@ +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import { IResource } from '@aws-cdk/core'; + +/** + * Represents a API Gateway HTTP/WebSocket API + */ +export interface IApi extends IResource { + /** + * The identifier of this API Gateway API. + * @attribute + */ + readonly apiId: string; + + /** + * The default endpoint for an API + * @attribute + */ + readonly apiEndpoint: string; + + /** + * Return the given named metric for this Api Gateway + * + * @default - average over 5 minutes + */ + metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for the number of client-side errors captured in a given period. + * + * @default - sum over 5 minutes + */ + metricClientError(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for the number of server-side errors captured in a given period. + * + * @default - sum over 5 minutes + */ + metricServerError(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for the amount of data processed in bytes. + * + * @default - sum over 5 minutes + */ + metricDataProcessed(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for the total number API requests in a given period. + * + * @default - SampleCount over 5 minutes + */ + metricCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for the time between when API Gateway relays a request to the backend + * and when it receives a response from the backend. + * + * @default - no statistic + */ + metricIntegrationLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * The time between when API Gateway receives a request from a client + * and when it returns a response to the client. + * The latency includes the integration latency and other API Gateway overhead. + * + * @default - no statistic + */ + metricLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric; +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/base.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/base.ts new file mode 100644 index 0000000000000..542fcfb16f8f4 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/base.ts @@ -0,0 +1,111 @@ +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import { Resource } from '@aws-cdk/core'; +import { IntegrationCache } from '../private/integration-cache'; +import { IApi } from './api'; +import { ApiMapping } from './api-mapping'; +import { DomainMappingOptions, IStage } from './stage'; + +/** + * Base class representing an API + * @internal + */ +export abstract class ApiBase extends Resource implements IApi { + abstract readonly apiId: string; + abstract readonly apiEndpoint: string; + /** + * @internal + */ + protected _integrationCache: IntegrationCache = new IntegrationCache(); + + public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace: 'AWS/ApiGateway', + metricName, + dimensions: { ApiId: this.apiId }, + ...props, + }).attachTo(this); + } + + public metricClientError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('4XXError', { statistic: 'Sum', ...props }); + } + + public metricServerError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('5XXError', { statistic: 'Sum', ...props }); + } + + public metricDataProcessed(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('DataProcessed', { statistic: 'Sum', ...props }); + } + + public metricCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('Count', { statistic: 'SampleCount', ...props }); + } + + public metricIntegrationLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('IntegrationLatency', props); + } + + public metricLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('Latency', props); + } +} + + +/** + * Base class representing a Stage + * @internal + */ +export abstract class StageBase extends Resource implements IStage { + public abstract readonly stageName: string; + protected abstract readonly baseApi: IApi; + + /** + * The URL to this stage. + */ + abstract get url(): string; + + /** + * @internal + */ + protected _addDomainMapping(domainMapping: DomainMappingOptions) { + new ApiMapping(this, `${domainMapping.domainName}${domainMapping.mappingKey}`, { + api: this.baseApi, + domainName: domainMapping.domainName, + stage: this, + apiMappingKey: domainMapping.mappingKey, + }); + // ensure the dependency + this.node.addDependency(domainMapping.domainName); + } + + public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.baseApi.metric(metricName, props).with({ + dimensions: { ApiId: this.baseApi.apiId, Stage: this.stageName }, + }).attachTo(this); + } + + public metricClientError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('4XXError', { statistic: 'Sum', ...props }); + } + + public metricServerError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('5XXError', { statistic: 'Sum', ...props }); + } + + public metricDataProcessed(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('DataProcessed', { statistic: 'Sum', ...props }); + } + + public metricCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('Count', { statistic: 'SampleCount', ...props }); + } + + public metricIntegrationLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('IntegrationLatency', props); + } + + public metricLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('Latency', props); + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts index eeb237a4e7f84..b0a0f1c0265eb 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts @@ -1,3 +1,4 @@ +export * from './api'; export * from './integration'; export * from './route'; export * from './stage'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/integration.ts index 7255607639468..83e200aadb007 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/integration.ts @@ -9,4 +9,4 @@ export interface IIntegration extends IResource { * @attribute */ readonly integrationId: string; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts index b608a7a34ad97..40b7832418633 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts @@ -1,4 +1,6 @@ +import { Metric, MetricOptions } from '@aws-cdk/aws-cloudwatch'; import { IResource } from '@aws-cdk/core'; +import { IDomainName } from './domain-name'; /** * Represents a Stage. @@ -9,22 +11,107 @@ export interface IStage extends IResource { * @attribute */ readonly stageName: string; + + /** + * The URL to this stage. + */ + readonly url: string; + + /** + * Return the given named metric for this HTTP Api Gateway Stage + * + * @default - average over 5 minutes + */ + metric(metricName: string, props?: MetricOptions): Metric + + /** + * Metric for the number of client-side errors captured in a given period. + * + * @default - sum over 5 minutes + */ + metricClientError(props?: MetricOptions): Metric + + /** + * Metric for the number of server-side errors captured in a given period. + * + * @default - sum over 5 minutes + */ + metricServerError(props?: MetricOptions): Metric + + /** + * Metric for the amount of data processed in bytes. + * + * @default - sum over 5 minutes + */ + metricDataProcessed(props?: MetricOptions): Metric + + /** + * Metric for the total number API requests in a given period. + * + * @default - SampleCount over 5 minutes + */ + metricCount(props?: MetricOptions): Metric + + /** + * Metric for the time between when API Gateway relays a request to the backend + * and when it receives a response from the backend. + * + * @default - no statistic + */ + metricIntegrationLatency(props?: MetricOptions): Metric + + /** + * The time between when API Gateway receives a request from a client + * and when it returns a response to the client. + * The latency includes the integration latency and other API Gateway overhead. + * + * @default - no statistic + */ + metricLatency(props?: MetricOptions): Metric } /** - * Options required to create a new stage. - * Options that are common between HTTP and Websocket APIs. + * Options for DomainMapping */ -export interface CommonStageOptions { +export interface DomainMappingOptions { /** - * The name of the stage. See `StageName` class for more details. - * @default '$default' the default stage of the API. This stage will have the URL at the root of the API endpoint. + * The domain name for the mapping + * */ - readonly stageName?: string; + readonly domainName: IDomainName; + /** + * The API mapping key. Leave it undefined for the root path mapping. + * @default - empty key for the root path mapping + */ + readonly mappingKey?: string; +} + +/** + * Options required to create a new stage. + * Options that are common between HTTP and Websocket APIs. + */ +export interface StageOptions { /** * Whether updates to an API automatically trigger a new deployment. * @default false */ readonly autoDeploy?: boolean; + + /** + * The options for custom domain and api mapping + * + * @default - no custom domain and api mapping configuration + */ + readonly domainMapping?: DomainMappingOptions; +} + +/** + * The attributes used to import existing Stage + */ +export interface StageAttributes { + /** + * The name of the stage + */ + readonly stageName: string; } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api-mapping.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api-mapping.ts deleted file mode 100644 index ee9323240833d..0000000000000 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api-mapping.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { Resource } from '@aws-cdk/core'; -import { Construct } from 'constructs'; -import { CfnApiMapping, CfnApiMappingProps } from '../apigatewayv2.generated'; -import { IApiMapping, IDomainName } from '../common'; -import { IHttpApi } from '../http/api'; -import { IHttpStage } from './stage'; - -/** - * Properties used to create the HttpApiMapping resource - */ -export interface HttpApiMappingProps { - /** - * Api mapping key. The path where this stage should be mapped to on the domain - * @default - undefined for the root path mapping. - */ - readonly apiMappingKey?: string; - - /** - * The HttpApi to which this mapping is applied - */ - readonly api: IHttpApi; - - /** - * custom domain name of the mapping target - */ - readonly domainName: IDomainName; - - /** - * stage for the HttpApiMapping resource - * - * @default - the $default stage - */ - readonly stage?: IHttpStage; -} - -/** - * The attributes used to import existing HttpApiMapping - */ -export interface HttpApiMappingAttributes { - /** - * The API mapping ID - */ - readonly apiMappingId: string; -} - -/** - * Create a new API mapping for API Gateway HTTP API endpoint. - * @resource AWS::ApiGatewayV2::ApiMapping - */ -export class HttpApiMapping extends Resource implements IApiMapping { - /** - * import from API ID - */ - public static fromHttpApiMappingAttributes(scope: Construct, id: string, attrs: HttpApiMappingAttributes): IApiMapping { - class Import extends Resource implements IApiMapping { - public readonly apiMappingId = attrs.apiMappingId; - } - return new Import(scope, id); - } - /** - * ID of the API Mapping - */ - public readonly apiMappingId: string; - - /** - * API Mapping key - */ - public readonly mappingKey?: string; - - constructor(scope: Construct, id: string, props: HttpApiMappingProps) { - super(scope, id); - - if ((!props.stage?.stageName) && !props.api.defaultStage) { - throw new Error('stage is required if default stage is not available'); - } - - const paramRe = '^[a-zA-Z0-9]*[-_.+!,$]?[a-zA-Z0-9]*$'; - if (props.apiMappingKey && !new RegExp(paramRe).test(props.apiMappingKey)) { - throw new Error('An ApiMapping key may contain only letters, numbers and one of $-_.+!*\'(),'); - } - - if (props.apiMappingKey === '') { - throw new Error('empty string for api mapping key not allowed'); - } - - const apiMappingProps: CfnApiMappingProps = { - apiId: props.api.httpApiId, - domainName: props.domainName.name, - stage: props.stage?.stageName ?? props.api.defaultStage!.stageName, - apiMappingKey: props.apiMappingKey, - }; - - const resource = new CfnApiMapping(this, 'Resource', apiMappingProps); - - // ensure the dependency on the provided stage - if (props.stage) { - this.node.addDependency(props.stage); - } - - // if stage not specified, we ensure the default stage is ready before we create the api mapping - if (!props.stage?.stageName && props.api.defaultStage) { - this.node.addDependency(props.api.defaultStage!); - } - - this.apiMappingId = resource.ref; - this.mappingKey = props.apiMappingKey; - } - -} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts index 52e5f1fccbe07..b74c00e5824fb 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts @@ -1,9 +1,9 @@ -import * as crypto from 'crypto'; -import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; -import { Duration, IResource, Resource, Stack } from '@aws-cdk/core'; +import { Duration } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnApi, CfnApiProps } from '../apigatewayv2.generated'; -import { DefaultDomainMappingOptions } from '../http/stage'; +import { IApi } from '../common/api'; +import { ApiBase } from '../common/base'; +import { DomainMappingOptions, IStage } from '../common/stage'; import { IHttpRouteAuthorizer } from './authorizer'; import { IHttpRouteIntegration, HttpIntegration, HttpRouteIntegrationConfig } from './integration'; import { BatchHttpRouteOptions, HttpMethod, HttpRoute, HttpRouteKey } from './route'; @@ -13,76 +13,14 @@ import { VpcLink, VpcLinkProps } from './vpc-link'; /** * Represents an HTTP API */ -export interface IHttpApi extends IResource { +export interface IHttpApi extends IApi { /** * The identifier of this API Gateway HTTP API. * @attribute + * @deprecated - use apiId instead */ readonly httpApiId: string; - /** - * The default endpoint for an API - * @attribute - */ - readonly apiEndpoint: string; - - /** - * The default stage - */ - readonly defaultStage?: HttpStage; - - /** - * Return the given named metric for this HTTP Api Gateway - * - * @default - average over 5 minutes - */ - metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric; - - /** - * Metric for the number of client-side errors captured in a given period. - * - * @default - sum over 5 minutes - */ - metricClientError(props?: cloudwatch.MetricOptions): cloudwatch.Metric; - - /** - * Metric for the number of server-side errors captured in a given period. - * - * @default - sum over 5 minutes - */ - metricServerError(props?: cloudwatch.MetricOptions): cloudwatch.Metric; - - /** - * Metric for the amount of data processed in bytes. - * - * @default - sum over 5 minutes - */ - metricDataProcessed(props?: cloudwatch.MetricOptions): cloudwatch.Metric; - - /** - * Metric for the total number API requests in a given period. - * - * @default - SampleCount over 5 minutes - */ - metricCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric; - - /** - * Metric for the time between when API Gateway relays a request to the backend - * and when it receives a response from the backend. - * - * @default - no statistic - */ - metricIntegrationLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric; - - /** - * The time between when API Gateway receives a request from a client - * and when it returns a response to the client. - * The latency includes the integration latency and other API Gateway overhead. - * - * @default - no statistic - */ - metricLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric; - /** * Add a new VpcLink */ @@ -135,7 +73,7 @@ export interface HttpApiProps { * * @default - no default domain mapping configured. meaningless if `createDefaultStage` is `false`. */ - readonly defaultDomainMapping?: DefaultDomainMappingOptions; + readonly defaultDomainMapping?: DomainMappingOptions; /** * Specifies whether clients can invoke your API using the default endpoint. @@ -218,45 +156,12 @@ export interface AddRoutesOptions extends BatchHttpRouteOptions { readonly authorizationScopes?: string[]; } -abstract class HttpApiBase extends Resource implements IHttpApi { // note that this is not exported +abstract class HttpApiBase extends ApiBase implements IHttpApi { // note that this is not exported + public abstract readonly apiId: string; public abstract readonly httpApiId: string; public abstract readonly apiEndpoint: string; private vpcLinks: Record = {}; - private httpIntegrations: Record = {}; - - public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return new cloudwatch.Metric({ - namespace: 'AWS/ApiGateway', - metricName, - dimensions: { ApiId: this.httpApiId }, - ...props, - }).attachTo(this); - } - - public metricClientError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('4XXError', { statistic: 'Sum', ...props }); - } - - public metricServerError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('5XXError', { statistic: 'Sum', ...props }); - } - - public metricDataProcessed(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('DataProcessed', { statistic: 'Sum', ...props }); - } - - public metricCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('Count', { statistic: 'SampleCount', ...props }); - } - - public metricIntegrationLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('IntegrationLatency', props); - } - - public metricLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('Latency', props); - } public addVpcLink(options: VpcLinkProps): VpcLink { const { vpcId } = options.vpc; @@ -275,11 +180,9 @@ abstract class HttpApiBase extends Resource implements IHttpApi { // note that t * @internal */ public _addIntegration(scope: Construct, config: HttpRouteIntegrationConfig): HttpIntegration { - const stringifiedConfig = JSON.stringify(Stack.of(scope).resolve(config)); - const configHash = crypto.createHash('md5').update(stringifiedConfig).digest('hex'); - - if (configHash in this.httpIntegrations) { - return this.httpIntegrations[configHash]; + const { configHash, integration: existingIntegration } = this._integrationCache.getIntegration(scope, config); + if (existingIntegration) { + return existingIntegration as HttpIntegration; } const integration = new HttpIntegration(scope, `HttpIntegration-${configHash}`, { @@ -291,7 +194,7 @@ abstract class HttpApiBase extends Resource implements IHttpApi { // note that t connectionType: config.connectionType, payloadFormatVersion: config.payloadFormatVersion, }); - this.httpIntegrations[configHash] = integration; + this._integrationCache.saveIntegration(scope, config, integration); return integration; } @@ -322,6 +225,7 @@ export class HttpApi extends HttpApiBase { */ public static fromHttpApiAttributes(scope: Construct, id: string, attrs: HttpApiAttributes): IHttpApi { class Import extends HttpApiBase { + public readonly apiId = attrs.httpApiId; public readonly httpApiId = attrs.httpApiId; private readonly _apiEndpoint = attrs.apiEndpoint; @@ -339,6 +243,7 @@ export class HttpApi extends HttpApiBase { * A human friendly name for this HTTP API. Note that this is different from `httpApiId`. */ public readonly httpApiName?: string; + public readonly apiId: string; public readonly httpApiId: string; /** @@ -347,9 +252,9 @@ export class HttpApi extends HttpApiBase { public readonly disableExecuteApiEndpoint?: boolean; /** - * default stage of the api resource + * The default stage of this API */ - public readonly defaultStage: HttpStage | undefined; + public readonly defaultStage: IStage | undefined; private readonly _apiEndpoint: string; @@ -392,6 +297,7 @@ export class HttpApi extends HttpApiBase { }; const resource = new CfnApi(this, 'Resource', apiProps); + this.apiId = resource.ref; this.httpApiId = resource.ref; this._apiEndpoint = resource.attrApiEndpoint; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts index efd60f9f24d7c..81ddfec695bc3 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts @@ -2,6 +2,5 @@ export * from './api'; export * from './route'; export * from './integration'; export * from './stage'; -export * from './api-mapping'; export * from './vpc-link'; export * from './authorizer'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts index fbe54345e25e3..d6a5f96320e3b 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts @@ -1,11 +1,10 @@ -import { Metric, MetricOptions } from '@aws-cdk/aws-cloudwatch'; -import { Resource, Stack } from '@aws-cdk/core'; +import { Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnStage } from '../apigatewayv2.generated'; -import { CommonStageOptions, IDomainName, IStage } from '../common'; +import { StageOptions, IStage, StageAttributes } from '../common'; +import { IApi } from '../common/api'; +import { StageBase } from '../common/base'; import { IHttpApi } from './api'; -import { HttpApiMapping } from './api-mapping'; - const DEFAULT_STAGE_NAME = '$default'; @@ -13,18 +12,21 @@ const DEFAULT_STAGE_NAME = '$default'; * Represents the HttpStage */ export interface IHttpStage extends IStage { + /** + * The API this stage is associated to. + */ + readonly api: IHttpApi; } /** - * Options to create a new stage for an HTTP API. + * The options to create a new Stage for an HTTP API */ -export interface HttpStageOptions extends CommonStageOptions { +export interface HttpStageOptions extends StageOptions { /** - * The options for custom domain and api mapping - * - * @default - no custom domain and api mapping configuration + * The name of the stage. See `StageName` class for more details. + * @default '$default' the default stage of the API. This stage will have the URL at the root of the API endpoint. */ - readonly domainMapping?: DomainMappingOptions; + readonly stageName?: string; } /** @@ -38,51 +40,39 @@ export interface HttpStageProps extends HttpStageOptions { } /** - * Options for defaultDomainMapping + * The attributes used to import existing HttpStage */ -export interface DefaultDomainMappingOptions { - /** - * The domain name for the mapping - * - */ - readonly domainName: IDomainName; - +export interface HttpStageAttributes extends StageAttributes { /** - * The API mapping key. Leave it undefined for the root path mapping. - * @default - empty key for the root path mapping + * The API to which this stage is associated */ - readonly mappingKey?: string; -} - -/** - * Options for DomainMapping - */ -export interface DomainMappingOptions extends DefaultDomainMappingOptions { - /** - * The API Stage - * - * @default - the $default stage - */ - readonly stage?: IStage; + readonly api: IHttpApi; } /** * Represents a stage where an instance of the API is deployed. * @resource AWS::ApiGatewayV2::Stage */ -export class HttpStage extends Resource implements IStage { +export class HttpStage extends StageBase implements IHttpStage { /** * Import an existing stage into this CDK app. */ - public static fromStageName(scope: Construct, id: string, stageName: string): IStage { - class Import extends Resource implements IStage { - public readonly stageName = stageName; + public static fromHttpStageAttributes(scope: Construct, id: string, attrs: HttpStageAttributes): IHttpStage { + class Import extends StageBase implements IHttpStage { + protected readonly baseApi = attrs.api; + public readonly stageName = attrs.stageName; + public readonly api = attrs.api; + + get url(): string { + throw new Error('url is not available for imported stages.'); + } } return new Import(scope, id); } + protected readonly baseApi: IApi; public readonly stageName: string; - private httpApi: IHttpApi; + public readonly api: IHttpApi; constructor(scope: Construct, id: string, props: HttpStageProps) { super(scope, id, { @@ -90,25 +80,18 @@ export class HttpStage extends Resource implements IStage { }); new CfnStage(this, 'Resource', { - apiId: props.httpApi.httpApiId, + apiId: props.httpApi.apiId, stageName: this.physicalName, autoDeploy: props.autoDeploy, }); this.stageName = this.physicalName; - this.httpApi = props.httpApi; + this.baseApi = props.httpApi; + this.api = props.httpApi; if (props.domainMapping) { - new HttpApiMapping(this, `${props.domainMapping.domainName}${props.domainMapping.mappingKey}`, { - api: props.httpApi, - domainName: props.domainMapping.domainName, - stage: this, - apiMappingKey: props.domainMapping.mappingKey, - }); - // ensure the dependency - this.node.addDependency(props.domainMapping.domainName); + this._addDomainMapping(props.domainMapping); } - } /** @@ -117,75 +100,6 @@ export class HttpStage extends Resource implements IStage { public get url(): string { const s = Stack.of(this); const urlPath = this.stageName === DEFAULT_STAGE_NAME ? '' : this.stageName; - return `https://${this.httpApi.httpApiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath}`; - } - - /** - * Return the given named metric for this HTTP Api Gateway Stage - * - * @default - average over 5 minutes - */ - public metric(metricName: string, props?: MetricOptions): Metric { - var api = this.httpApi; - return api.metric(metricName, props).with({ - dimensions: { ApiId: this.httpApi.httpApiId, Stage: this.stageName }, - }).attachTo(this); - } - - /** - * Metric for the number of client-side errors captured in a given period. - * - * @default - sum over 5 minutes - */ - public metricClientError(props?: MetricOptions): Metric { - return this.metric('4XXError', { statistic: 'Sum', ...props }); - } - - /** - * Metric for the number of server-side errors captured in a given period. - * - * @default - sum over 5 minutes - */ - public metricServerError(props?: MetricOptions): Metric { - return this.metric('5XXError', { statistic: 'Sum', ...props }); - } - - /** - * Metric for the amount of data processed in bytes. - * - * @default - sum over 5 minutes - */ - public metricDataProcessed(props?: MetricOptions): Metric { - return this.metric('DataProcessed', { statistic: 'Sum', ...props }); - } - - /** - * Metric for the total number API requests in a given period. - * - * @default - SampleCount over 5 minutes - */ - public metricCount(props?: MetricOptions): Metric { - return this.metric('Count', { statistic: 'SampleCount', ...props }); - } - - /** - * Metric for the time between when API Gateway relays a request to the backend - * and when it receives a response from the backend. - * - * @default - no statistic - */ - public metricIntegrationLatency(props?: MetricOptions): Metric { - return this.metric('IntegrationLatency', props); - } - - /** - * The time between when API Gateway receives a request from a client - * and when it returns a response to the client. - * The latency includes the integration latency and other API Gateway overhead. - * - * @default - no statistic - */ - public metricLatency(props?: MetricOptions): Metric { - return this.metric('Latency', props); + return `https://${this.api.apiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath}`; } } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts index 31ea86b4a91c2..12dd8113f8b4c 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts @@ -1,3 +1,4 @@ export * from './apigatewayv2.generated'; export * from './common'; -export * from './http'; \ No newline at end of file +export * from './http'; +export * from './websocket'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/private/integration-cache.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/private/integration-cache.ts new file mode 100644 index 0000000000000..2401d28e20d2d --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/private/integration-cache.ts @@ -0,0 +1,29 @@ +import * as crypto from 'crypto'; +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { IIntegration } from '../common/integration'; +import { HttpRouteIntegrationConfig } from '../http'; +import { WebSocketRouteIntegrationConfig } from '../websocket'; + +type IntegrationConfig = HttpRouteIntegrationConfig | WebSocketRouteIntegrationConfig; + +export class IntegrationCache { + private integrations: Record = {}; + + getIntegration(scope: Construct, config: IntegrationConfig) { + const configHash = this.integrationConfigHash(scope, config); + const integration = this.integrations[configHash]; + return { configHash, integration }; + } + + saveIntegration(scope: Construct, config: IntegrationConfig, integration: IIntegration) { + const configHash = this.integrationConfigHash(scope, config); + this.integrations[configHash] = integration; + } + + private integrationConfigHash(scope: Construct, config: IntegrationConfig): string { + const stringifiedConfig = JSON.stringify(Stack.of(scope).resolve(config)); + const configHash = crypto.createHash('md5').update(stringifiedConfig).digest('hex'); + return configHash; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts new file mode 100644 index 0000000000000..f2f2653c94ee6 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts @@ -0,0 +1,130 @@ +import { Construct } from 'constructs'; +import { CfnApi } from '../apigatewayv2.generated'; +import { IApi } from '../common/api'; +import { ApiBase } from '../common/base'; +import { WebSocketRouteIntegrationConfig, WebSocketIntegration } from './integration'; +import { WebSocketRoute, WebSocketRouteOptions } from './route'; + +/** + * Represents a WebSocket API + */ +export interface IWebSocketApi extends IApi { + /** + * Add a websocket integration + * @internal + */ + _addIntegration(scope: Construct, config: WebSocketRouteIntegrationConfig): WebSocketIntegration +} + +/** + * Props for WebSocket API + */ +export interface WebSocketApiProps { + /** + * Name for the WebSocket API resoruce + * @default - id of the WebSocketApi construct. + */ + readonly apiName?: string; + + /** + * The description of the API. + * @default - none + */ + readonly description?: string; + + /** + * The route selection expression for the API + * @default '$request.body.action' + */ + readonly routeSelectionExpression?: string; + + /** + * Options to configure a '$connect' route + * + * @default - no '$connect' route configured + */ + readonly connectRouteOptions?: WebSocketRouteOptions; + + /** + * Options to configure a '$disconnect' route + * + * @default - no '$disconnect' route configured + */ + readonly disconnectRouteOptions?: WebSocketRouteOptions; + + /** + * Options to configure a '$default' route + * + * @default - no '$default' route configured + */ + readonly defaultRouteOptions?: WebSocketRouteOptions; +} + +/** + * Create a new API Gateway WebSocket API endpoint. + * @resource AWS::ApiGatewayV2::Api + */ +export class WebSocketApi extends ApiBase implements IWebSocketApi { + public readonly apiId: string; + public readonly apiEndpoint: string; + + /** + * A human friendly name for this WebSocket API. Note that this is different from `webSocketApiId`. + */ + public readonly webSocketApiName?: string; + + constructor(scope: Construct, id: string, props?: WebSocketApiProps) { + super(scope, id); + + this.webSocketApiName = props?.apiName ?? id; + + const resource = new CfnApi(this, 'Resource', { + name: this.webSocketApiName, + protocolType: 'WEBSOCKET', + description: props?.description, + routeSelectionExpression: props?.routeSelectionExpression ?? '$request.body.action', + }); + this.apiId = resource.ref; + this.apiEndpoint = resource.attrApiEndpoint; + + if (props?.connectRouteOptions) { + this.addRoute('$connect', props.connectRouteOptions); + } + if (props?.disconnectRouteOptions) { + this.addRoute('$disconnect', props.disconnectRouteOptions); + } + if (props?.defaultRouteOptions) { + this.addRoute('$default', props.defaultRouteOptions); + } + } + + /** + * @internal + */ + public _addIntegration(scope: Construct, config: WebSocketRouteIntegrationConfig): WebSocketIntegration { + const { configHash, integration: existingIntegration } = this._integrationCache.getIntegration(scope, config); + if (existingIntegration) { + return existingIntegration as WebSocketIntegration; + } + + const integration = new WebSocketIntegration(scope, `WebSocketIntegration-${configHash}`, { + webSocketApi: this, + integrationType: config.type, + integrationUri: config.uri, + }); + this._integrationCache.saveIntegration(scope, config, integration); + + return integration; + } + + /** + * Add a new route + */ + public addRoute(routeKey: string, options: WebSocketRouteOptions) { + return new WebSocketRoute(this, `${routeKey}-Route`, { + webSocketApi: this, + routeKey, + ...options, + }); + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/index.ts new file mode 100644 index 0000000000000..b0ce6a8a91419 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/index.ts @@ -0,0 +1,4 @@ +export * from './api'; +export * from './route'; +export * from './stage'; +export * from './integration'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/integration.ts new file mode 100644 index 0000000000000..e75bd00b63d95 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/integration.ts @@ -0,0 +1,110 @@ +import { Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnIntegration } from '../apigatewayv2.generated'; +import { IIntegration } from '../common'; +import { IWebSocketApi } from './api'; +import { IWebSocketRoute } from './route'; + +// 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'; + +/** + * Represents an Integration for an WebSocket API. + */ +export interface IWebSocketIntegration extends IIntegration { + /** The WebSocket API associated with this integration */ + readonly webSocketApi: IWebSocketApi; +} + +/** + * WebSocket Integration Types + */ +export enum WebSocketIntegrationType { + /** + * AWS Proxy Integration Type + */ + AWS_PROXY = 'AWS_PROXY' +} + +/** + * The integration properties + */ +export interface WebSocketIntegrationProps { + /** + * The WebSocket API to which this integration should be bound. + */ + readonly webSocketApi: IWebSocketApi; + + /** + * Integration type + */ + readonly integrationType: WebSocketIntegrationType; + + /** + * Integration URI. + */ + readonly integrationUri: string; +} + +/** + * The integration for an API route. + * @resource AWS::ApiGatewayV2::Integration + */ +export class WebSocketIntegration extends Resource implements IWebSocketIntegration { + public readonly integrationId: string; + public readonly webSocketApi: IWebSocketApi; + + constructor(scope: Construct, id: string, props: WebSocketIntegrationProps) { + super(scope, id); + const integ = new CfnIntegration(this, 'Resource', { + apiId: props.webSocketApi.apiId, + integrationType: props.integrationType, + integrationUri: props.integrationUri, + }); + this.integrationId = integ.ref; + this.webSocketApi = props.webSocketApi; + } +} + +/** + * Options to the WebSocketRouteIntegration during its bind operation. + */ +export interface WebSocketRouteIntegrationBindOptions { + /** + * The route to which this is being bound. + */ + readonly route: IWebSocketRoute; + + /** + * The current scope in which the bind is occurring. + * If the `WebSocketRouteIntegration` being bound creates additional constructs, + * this will be used as their parent scope. + */ + readonly scope: CoreConstruct; +} + +/** + * The interface that various route integration classes will inherit. + */ +export interface IWebSocketRouteIntegration { + /** + * Bind this integration to the route. + */ + bind(options: WebSocketRouteIntegrationBindOptions): WebSocketRouteIntegrationConfig; +} + +/** + * Config returned back as a result of the bind. + */ +export interface WebSocketRouteIntegrationConfig { + /** + * Integration type. + */ + readonly type: WebSocketIntegrationType; + + /** + * Integration URI + */ + readonly uri: string; +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts new file mode 100644 index 0000000000000..0588889a603bc --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts @@ -0,0 +1,84 @@ +import { Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnRoute } from '../apigatewayv2.generated'; +import { IRoute } from '../common'; +import { IWebSocketApi } from './api'; +import { IWebSocketRouteIntegration } from './integration'; + +/** + * Represents a Route for an WebSocket API. + */ +export interface IWebSocketRoute extends IRoute { + /** + * The WebSocket API associated with this route. + */ + readonly webSocketApi: IWebSocketApi; + + /** + * The key to this route. + * @attribute + */ + readonly routeKey: string; +} + +/** + * Options used to add route to the API + */ +export interface WebSocketRouteOptions { + /** + * The integration to be configured on this route. + */ + readonly integration: IWebSocketRouteIntegration; +} + + +/** + * Properties to initialize a new Route + */ +export interface WebSocketRouteProps extends WebSocketRouteOptions { + /** + * the API the route is associated with + */ + readonly webSocketApi: IWebSocketApi; + + /** + * The key to this route. + */ + readonly routeKey: string; +} + +/** + * Route class that creates the Route for API Gateway WebSocket API + * @resource AWS::ApiGatewayV2::Route + */ +export class WebSocketRoute extends Resource implements IWebSocketRoute { + public readonly routeId: string; + public readonly webSocketApi: IWebSocketApi; + public readonly routeKey: string; + + /** + * Integration response ID + */ + public readonly integrationResponseId?: string; + + constructor(scope: Construct, id: string, props: WebSocketRouteProps) { + super(scope, id); + + this.webSocketApi = props.webSocketApi; + this.routeKey = props.routeKey; + + const config = props.integration.bind({ + route: this, + scope: this, + }); + + const integration = props.webSocketApi._addIntegration(this, config); + + const route = new CfnRoute(this, 'Resource', { + apiId: props.webSocketApi.apiId, + routeKey: props.routeKey, + target: `integrations/${integration.integrationId}`, + }); + this.routeId = route.ref; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts new file mode 100644 index 0000000000000..a50353a79ca2d --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts @@ -0,0 +1,96 @@ +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnStage } from '../apigatewayv2.generated'; +import { StageOptions, IApi, IStage, StageAttributes } from '../common'; +import { StageBase } from '../common/base'; +import { IWebSocketApi } from './api'; + +/** + * Represents the WebSocketStage + */ +export interface IWebSocketStage extends IStage { + /** + * The API this stage is associated to. + */ + readonly api: IWebSocketApi; +} + +/** + * Properties to initialize an instance of `WebSocketStage`. + */ +export interface WebSocketStageProps extends StageOptions { + /** + * The WebSocket API to which this stage is associated. + */ + readonly webSocketApi: IWebSocketApi; + + /** + * The name of the stage. + */ + readonly stageName: string; +} + +/** + * The attributes used to import existing WebSocketStage + */ +export interface WebSocketStageAttributes extends StageAttributes { + /** + * The API to which this stage is associated + */ + readonly api: IWebSocketApi; +} + +/** + * Represents a stage where an instance of the API is deployed. + * @resource AWS::ApiGatewayV2::Stage + */ +export class WebSocketStage extends StageBase implements IWebSocketStage { + /** + * Import an existing stage into this CDK app. + */ + public static fromWebSocketStageAttributes(scope: Construct, id: string, attrs: WebSocketStageAttributes): IWebSocketStage { + class Import extends StageBase implements IWebSocketStage { + public readonly baseApi = attrs.api; + public readonly stageName = attrs.stageName; + public readonly api = attrs.api; + + get url(): string { + throw new Error('url is not available for imported stages.'); + } + } + return new Import(scope, id); + } + + protected readonly baseApi: IApi; + public readonly stageName: string; + public readonly api: IWebSocketApi; + + constructor(scope: Construct, id: string, props: WebSocketStageProps) { + super(scope, id, { + physicalName: props.stageName, + }); + + this.baseApi = props.webSocketApi; + this.api = props.webSocketApi; + this.stageName = this.physicalName; + + new CfnStage(this, 'Resource', { + apiId: props.webSocketApi.apiId, + stageName: this.physicalName, + autoDeploy: props.autoDeploy, + }); + + if (props.domainMapping) { + this._addDomainMapping(props.domainMapping); + } + } + + /** + * The URL to this stage. + */ + public get url(): string { + const s = Stack.of(this); + const urlPath = this.stageName; + return `wss://${this.api.apiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath}`; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/package.json b/packages/@aws-cdk/aws-apigatewayv2/package.json index 19abb0ca10b3f..b1dd874b85f9e 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2/package.json @@ -103,13 +103,16 @@ }, "awslint": { "exclude": [ + "props-physical-name:@aws-cdk/aws-apigatewayv2.ApiMappingProps", "from-method:@aws-cdk/aws-apigatewayv2.HttpIntegration", "from-method:@aws-cdk/aws-apigatewayv2.HttpRoute", - "from-method:@aws-cdk/aws-apigatewayv2.HttpStage", - "props-physical-name-type:@aws-cdk/aws-apigatewayv2.HttpStageProps.stageName", - "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpApiMappingProps", "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpIntegrationProps", - "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpRouteProps" + "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpRouteProps", + "from-method:@aws-cdk/aws-apigatewayv2.WebSocketApi", + "from-method:@aws-cdk/aws-apigatewayv2.WebSocketIntegration", + "from-method:@aws-cdk/aws-apigatewayv2.WebSocketRoute", + "props-physical-name:@aws-cdk/aws-apigatewayv2.WebSocketIntegrationProps", + "props-physical-name:@aws-cdk/aws-apigatewayv2.WebSocketRouteProps" ] }, "stability": "experimental", @@ -121,7 +124,7 @@ }, { "name": "Higher level constructs for Websocket APIs", - "stability": "Not Implemented" + "stability": "Experimental" } ], "awscdkio": { diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/api-mapping.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/common/api-mapping.test.ts similarity index 76% rename from packages/@aws-cdk/aws-apigatewayv2/test/http/api-mapping.test.ts rename to packages/@aws-cdk/aws-apigatewayv2/test/common/api-mapping.test.ts index fe113727c3f50..b917f19513a57 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/api-mapping.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/common/api-mapping.test.ts @@ -1,7 +1,7 @@ import '@aws-cdk/assert/jest'; import { Certificate } from '@aws-cdk/aws-certificatemanager'; import { Stack } from '@aws-cdk/core'; -import { DomainName, HttpApi, HttpApiMapping } from '../../lib'; +import { DomainName, HttpApi, ApiMapping, WebSocketApi } from '../../lib'; const domainName = 'example.com'; const certArn = 'arn:aws:acm:us-east-1:111111111111:certificate'; @@ -17,7 +17,7 @@ describe('ApiMapping', () => { certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), }); - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, }); @@ -47,7 +47,7 @@ describe('ApiMapping', () => { certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), }); - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, stage: beta, @@ -75,7 +75,7 @@ describe('ApiMapping', () => { }); expect(() => { - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, apiMappingKey: '', @@ -94,7 +94,7 @@ describe('ApiMapping', () => { }); expect(() => { - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, apiMappingKey: '/', @@ -113,7 +113,7 @@ describe('ApiMapping', () => { }); expect(() => { - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, apiMappingKey: '/foo', @@ -132,7 +132,7 @@ describe('ApiMapping', () => { }); expect(() => { - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, apiMappingKey: 'foo/bar', @@ -151,7 +151,7 @@ describe('ApiMapping', () => { }); expect(() => { - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, apiMappingKey: 'foo/', @@ -170,7 +170,7 @@ describe('ApiMapping', () => { }); expect(() => { - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, apiMappingKey: '^foo', @@ -189,7 +189,7 @@ describe('ApiMapping', () => { }); expect(() => { - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, apiMappingKey: 'foo.*$', @@ -207,15 +207,53 @@ describe('ApiMapping', () => { certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), }); - const mapping = new HttpApiMapping(stack, 'Mapping', { + const mapping = new ApiMapping(stack, 'Mapping', { api, domainName: dn, }); - const imported = HttpApiMapping.fromHttpApiMappingAttributes(stack, 'ImportedMapping', { + const imported = ApiMapping.fromApiMappingAttributes(stack, 'ImportedMapping', { apiMappingId: mapping.apiMappingId, } ); expect(imported.apiMappingId).toEqual(mapping.apiMappingId); }); + + test('stage validation - throws if defaultStage not available for HttpApi', () => { + // GIVEN + const stack = new Stack(); + const api = new HttpApi(stack, 'Api', { + createDefaultStage: false, + }); + const dn = new DomainName(stack, 'DomainName', { + domainName, + certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), + }); + + // WHEN + expect(() => { + new ApiMapping(stack, 'Mapping', { + api, + domainName: dn, + }); + }).toThrow(/stage is required if default stage is not available/); + }); + + test('stage validation - throws if stage not provided for WebSocketApi', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'api'); + const dn = new DomainName(stack, 'DomainName', { + domainName, + certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), + }); + + // WHEN + expect(() => { + new ApiMapping(stack, 'Mapping', { + api, + domainName: dn, + }); + }).toThrow(/stage is required for WebSocket API/); + }); }); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts index 01252be7d84f1..a8c5f418f7782 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts @@ -19,7 +19,7 @@ describe('HttpApi', () => { }); expect(stack).toHaveResource('AWS::ApiGatewayV2::Stage', { - ApiId: stack.resolve(api.httpApiId), + ApiId: stack.resolve(api.apiId), StageName: '$default', AutoDeploy: true, }); @@ -34,7 +34,7 @@ describe('HttpApi', () => { const stack = new Stack(); const imported = HttpApi.fromHttpApiAttributes(stack, 'imported', { httpApiId: 'http-1234', apiEndpoint: 'api-endpoint' }); - expect(imported.httpApiId).toEqual('http-1234'); + expect(imported.apiId).toEqual('http-1234'); expect(imported.apiEndpoint).toEqual('api-endpoint'); }); @@ -55,12 +55,12 @@ describe('HttpApi', () => { }); expect(stack).toHaveResourceLike('AWS::ApiGatewayV2::Route', { - ApiId: stack.resolve(httpApi.httpApiId), + ApiId: stack.resolve(httpApi.apiId), RouteKey: '$default', }); expect(stack).toHaveResourceLike('AWS::ApiGatewayV2::Integration', { - ApiId: stack.resolve(httpApi.httpApiId), + ApiId: stack.resolve(httpApi.apiId), }); }); @@ -75,12 +75,12 @@ describe('HttpApi', () => { }); expect(stack).toHaveResourceLike('AWS::ApiGatewayV2::Route', { - ApiId: stack.resolve(httpApi.httpApiId), + ApiId: stack.resolve(httpApi.apiId), RouteKey: 'GET /pets', }); expect(stack).toHaveResourceLike('AWS::ApiGatewayV2::Route', { - ApiId: stack.resolve(httpApi.httpApiId), + ApiId: stack.resolve(httpApi.apiId), RouteKey: 'PATCH /pets', }); }); @@ -95,7 +95,7 @@ describe('HttpApi', () => { }); expect(stack).toHaveResourceLike('AWS::ApiGatewayV2::Route', { - ApiId: stack.resolve(httpApi.httpApiId), + ApiId: stack.resolve(httpApi.apiId), RouteKey: 'ANY /pets', }); }); @@ -149,7 +149,7 @@ describe('HttpApi', () => { }); const metricName = '4xxError'; const statistic = 'Sum'; - const apiId = api.httpApiId; + const apiId = api.apiId; // WHEN const countMetric = api.metric(metricName, { statistic }); @@ -168,7 +168,7 @@ describe('HttpApi', () => { createDefaultStage: false, }); const color = '#00ff00'; - const apiId = api.httpApiId; + const apiId = api.apiId; // WHEN const metrics = new Array(); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts index 6c4359b5439c9..ec1e28542d598 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts @@ -31,7 +31,10 @@ describe('HttpStage', () => { httpApi: api, }); - const imported = HttpStage.fromStageName(stack, 'Import', stage.stageName ); + const imported = HttpStage.fromHttpStageAttributes(stack, 'Import', { + stageName: stage.stageName, + api, + }); expect(imported.stageName).toEqual(stage.stageName); }); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts new file mode 100644 index 0000000000000..fcc65d4e18207 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts @@ -0,0 +1,92 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { + IWebSocketRouteIntegration, WebSocketApi, WebSocketIntegrationType, + WebSocketRouteIntegrationBindOptions, WebSocketRouteIntegrationConfig, +} from '../../lib'; + +describe('WebSocketApi', () => { + test('default', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new WebSocketApi(stack, 'api'); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Api', { + Name: 'api', + ProtocolType: 'WEBSOCKET', + }); + + expect(stack).not.toHaveResource('AWS::ApiGatewayV2::Stage'); + expect(stack).not.toHaveResource('AWS::ApiGatewayV2::Route'); + expect(stack).not.toHaveResource('AWS::ApiGatewayV2::Integration'); + }); + + test('addRoute: adds a route with passed key', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'api'); + + // WHEN + api.addRoute('myroute', { integration: new DummyIntegration() }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + ApiId: stack.resolve(api.apiId), + RouteKey: 'myroute', + }); + }); + + test('connectRouteOptions: adds a $connect route', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'api', { + connectRouteOptions: { integration: new DummyIntegration() }, + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + ApiId: stack.resolve(api.apiId), + RouteKey: '$connect', + }); + }); + + test('disconnectRouteOptions: adds a $disconnect route', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'api', { + disconnectRouteOptions: { integration: new DummyIntegration() }, + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + ApiId: stack.resolve(api.apiId), + RouteKey: '$disconnect', + }); + }); + + test('defaultRouteOptions: adds a $default route', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'api', { + defaultRouteOptions: { integration: new DummyIntegration() }, + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + ApiId: stack.resolve(api.apiId), + RouteKey: '$default', + }); + }); +}); + +class DummyIntegration implements IWebSocketRouteIntegration { + bind(_options: WebSocketRouteIntegrationBindOptions): WebSocketRouteIntegrationConfig { + return { + type: WebSocketIntegrationType.AWS_PROXY, + uri: 'some-uri', + }; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts new file mode 100644 index 0000000000000..04e8e5fc7efac --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts @@ -0,0 +1,54 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { + IWebSocketRouteIntegration, WebSocketApi, WebSocketIntegrationType, + WebSocketRoute, WebSocketRouteIntegrationBindOptions, WebSocketRouteIntegrationConfig, +} from '../../lib'; + +describe('WebSocketRoute', () => { + test('default', () => { + // GIVEN + const stack = new Stack(); + const webSocketApi = new WebSocketApi(stack, 'Api'); + + // WHEN + new WebSocketRoute(stack, 'Route', { + webSocketApi, + integration: new DummyIntegration(), + routeKey: 'message', + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + ApiId: stack.resolve(webSocketApi.apiId), + RouteKey: 'message', + Target: { + 'Fn::Join': [ + '', + [ + 'integrations/', + { + Ref: 'RouteWebSocketIntegrationb7742333c7ab20d7b2b178df59bb17f20338431E', + }, + ], + ], + }, + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + ApiId: stack.resolve(webSocketApi.apiId), + IntegrationType: 'AWS_PROXY', + IntegrationUri: 'some-uri', + }); + }); +}); + + +class DummyIntegration implements IWebSocketRouteIntegration { + bind(_options: WebSocketRouteIntegrationBindOptions): WebSocketRouteIntegrationConfig { + return { + type: WebSocketIntegrationType.AWS_PROXY, + uri: 'some-uri', + }; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts new file mode 100644 index 0000000000000..5ebdf0c61a980 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts @@ -0,0 +1,44 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { WebSocketApi, WebSocketStage } from '../../lib'; + +describe('WebSocketStage', () => { + test('default', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'Api'); + + // WHEN + const defaultStage = new WebSocketStage(stack, 'Stage', { + webSocketApi: api, + stageName: 'dev', + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Stage', { + ApiId: stack.resolve(api.apiId), + StageName: 'dev', + }); + expect(defaultStage.url.endsWith('/dev')).toBe(true); + }); + + test('import', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'Api'); + + // WHEN + const stage = new WebSocketStage(stack, 'Stage', { + webSocketApi: api, + stageName: 'dev', + }); + + const imported = WebSocketStage.fromWebSocketStageAttributes(stack, 'Import', { + stageName: stage.stageName, + api, + }); + + // THEN + expect(imported.stageName).toEqual(stage.stageName); + }); +}); From 80b10f8ccd2fae1b62eef26df736b94e68077130 Mon Sep 17 00:00:00 2001 From: Ayush Goyal Date: Fri, 5 Mar 2021 22:25:58 +0530 Subject: [PATCH 13/28] chore(apigatewayv2): remove usage of deprecated httpApiId (#13419) remove usage of deprecated httpApiId ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-apigatewayv2-integrations/lib/http/lambda.ts | 2 +- packages/@aws-cdk/aws-apigatewayv2/lib/http/authorizer.ts | 2 +- .../@aws-cdk/aws-apigatewayv2/lib/http/integration.ts | 2 +- packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts | 2 +- .../aws-apigatewayv2/test/http/authorizer.test.ts | 2 +- .../@aws-cdk/aws-apigatewayv2/test/http/route.test.ts | 8 ++++---- .../@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts | 6 +++--- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts index a962b268d7165..220d3dca57210 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts @@ -41,7 +41,7 @@ export class LambdaProxyIntegration implements IHttpRouteIntegration { principal: new ServicePrincipal('apigateway.amazonaws.com'), sourceArn: Stack.of(route).formatArn({ service: 'execute-api', - resource: route.httpApi.httpApiId, + resource: route.httpApi.apiId, resourceName: `*/*${route.path ?? ''}`, // empty string in the case of the catch-all route $default }), }); diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/authorizer.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/authorizer.ts index aadfb630ba276..f63c2e3a96583 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/authorizer.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/authorizer.ts @@ -112,7 +112,7 @@ export class HttpAuthorizer extends Resource implements IHttpAuthorizer { const resource = new CfnAuthorizer(this, 'Resource', { name: props.authorizerName ?? id, - apiId: props.httpApi.httpApiId, + apiId: props.httpApi.apiId, authorizerType: props.type, identitySource: props.identitySource, jwtConfiguration: undefinedIfNoKeys({ diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts index e609c9396c08f..836b831550fb7 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts @@ -134,7 +134,7 @@ export class HttpIntegration extends Resource implements IHttpIntegration { constructor(scope: Construct, id: string, props: HttpIntegrationProps) { super(scope, id); const integ = new CfnIntegration(this, 'Resource', { - apiId: props.httpApi.httpApiId, + apiId: props.httpApi.apiId, integrationType: props.integrationType, integrationUri: props.integrationUri, integrationMethod: props.method, diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts index 4510d13ed6f2b..416e9bed973a3 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts @@ -157,7 +157,7 @@ export class HttpRoute extends Resource implements IHttpRoute { } const routeProps: CfnRouteProps = { - apiId: props.httpApi.httpApiId, + apiId: props.httpApi.apiId, routeKey: props.routeKey.key, target: `integrations/${integration.integrationId}`, authorizerId: authBindResult?.authorizerId, diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/authorizer.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/authorizer.test.ts index bee2f7d05be1b..afb98fe733f19 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/authorizer.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/authorizer.test.ts @@ -18,7 +18,7 @@ describe('HttpAuthorizer', () => { }); expect(stack).toHaveResource('AWS::ApiGatewayV2::Authorizer', { - ApiId: stack.resolve(httpApi.httpApiId), + ApiId: stack.resolve(httpApi.apiId), Name: 'HttpAuthorizer', AuthorizerType: 'JWT', IdentitySource: ['identitysource.1', 'identitysource.2'], diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts index a5ce1b7b64b64..41af3121dc893 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts @@ -17,7 +17,7 @@ describe('HttpRoute', () => { }); expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { - ApiId: stack.resolve(httpApi.httpApiId), + ApiId: stack.resolve(httpApi.apiId), RouteKey: 'GET /books', Target: { 'Fn::Join': [ @@ -33,7 +33,7 @@ describe('HttpRoute', () => { }); expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { - ApiId: stack.resolve(httpApi.httpApiId), + ApiId: stack.resolve(httpApi.apiId), }); }); @@ -48,7 +48,7 @@ describe('HttpRoute', () => { }); expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { - ApiId: stack.resolve(httpApi.httpApiId), + ApiId: stack.resolve(httpApi.apiId), IntegrationType: 'HTTP_PROXY', PayloadFormatVersion: '2.0', IntegrationUri: 'some-uri', @@ -209,7 +209,7 @@ describe('HttpRoute', () => { }); expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { - ApiId: stack.resolve(httpApi.httpApiId), + ApiId: stack.resolve(httpApi.apiId), IntegrationType: 'HTTP_PROXY', PayloadFormatVersion: '2.0', IntegrationUri: 'some-uri', diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts index ec1e28542d598..ff70ea026acb6 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts @@ -16,7 +16,7 @@ describe('HttpStage', () => { }); expect(stack).toHaveResource('AWS::ApiGatewayV2::Stage', { - ApiId: stack.resolve(api.httpApiId), + ApiId: stack.resolve(api.apiId), StageName: '$default', }); }); @@ -69,7 +69,7 @@ describe('HttpStage', () => { }); const metricName = '4xxError'; const statistic = 'Sum'; - const apiId = api.httpApiId; + const apiId = api.apiId; // WHEN const countMetric = stage.metric(metricName, { statistic }); @@ -94,7 +94,7 @@ describe('HttpStage', () => { httpApi: api, }); const color = '#00ff00'; - const apiId = api.httpApiId; + const apiId = api.apiId; // WHEN const metrics = new Array(); From 278fba5df4a3d785e49bdb57ccf88fd34bacacbb Mon Sep 17 00:00:00 2001 From: Casey Lee Date: Fri, 5 Mar 2021 13:30:42 -0800 Subject: [PATCH 14/28] fix(ecr): Generate valid CloudFormation for imageScanOnPush (#13420) fix #13418: Update ECR construct to generate valid CloudFormation when enabling `imageScanOnPush` ---- *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/lib/repository.ts | 2 +- packages/@aws-cdk/aws-ecr/test/integ.imagescan.expected.json | 4 ++-- packages/@aws-cdk/aws-ecr/test/test.repository.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-ecr/lib/repository.ts b/packages/@aws-cdk/aws-ecr/lib/repository.ts index 3787903dedc3e..20f110c206428 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository.ts @@ -450,7 +450,7 @@ export class Repository extends RepositoryBase { repositoryPolicyText: Lazy.any({ produce: () => this.policyDocument }), lifecyclePolicy: Lazy.any({ produce: () => this.renderLifecyclePolicy() }), imageScanningConfiguration: !props.imageScanOnPush ? undefined : { - scanOnPush: true, + ScanOnPush: true, }, }); diff --git a/packages/@aws-cdk/aws-ecr/test/integ.imagescan.expected.json b/packages/@aws-cdk/aws-ecr/test/integ.imagescan.expected.json index 5367d722f62c9..5fbca07aa35d4 100644 --- a/packages/@aws-cdk/aws-ecr/test/integ.imagescan.expected.json +++ b/packages/@aws-cdk/aws-ecr/test/integ.imagescan.expected.json @@ -4,7 +4,7 @@ "Type": "AWS::ECR::Repository", "Properties": { "ImageScanningConfiguration": { - "scanOnPush": true + "ScanOnPush": true } }, "UpdateReplacePolicy": "Delete", @@ -87,4 +87,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecr/test/test.repository.ts b/packages/@aws-cdk/aws-ecr/test/test.repository.ts index 20c53f1a3032a..8c8094be287e9 100644 --- a/packages/@aws-cdk/aws-ecr/test/test.repository.ts +++ b/packages/@aws-cdk/aws-ecr/test/test.repository.ts @@ -38,7 +38,7 @@ export = { // THEN expect(stack).to(haveResource('AWS::ECR::Repository', { ImageScanningConfiguration: { - scanOnPush: true, + ScanOnPush: true, }, })); test.done(); From c7c424fec42f1f14ab8bdc3011f5bdb602918aa3 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Fri, 5 Mar 2021 23:04:19 +0100 Subject: [PATCH 15/28] fix(dynamodb): replicas not created on table replacement (#13300) Process `Update` events resulting from table replacements. Include the table name in the physical resource id to receive a `Delete` event when the table is replaced. This allows to clean "old" replicas. Use a managed policy instead of an inline policy for the custom resource. An update of the description property of a managed policy requires a replacement. If we use the table name in the description it forces a managed policy replacement when the table name changes. This way we preserve permissions to delete old replicas in case of a table replacement: a new managed policy with permissions for the new table is created during the update phase and the old managed policy with permissions for the old table is removed only during the update clean up phase. The logical ID of the `SourceTableAttachedPolicy` needs to be updated because CF doesn't allow to change a resource type. Closes #12332 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-dynamodb/lib/replica-handler/index.ts | 43 +++++----- packages/@aws-cdk/aws-dynamodb/lib/table.ts | 17 ++-- ....global-replicas-provisioned.expected.json | 80 ++++++++++++------- .../test/integ.global.expected.json | 80 ++++++++++++------- .../test/replica-provider.test.ts | 45 ++++++++++- 5 files changed, 180 insertions(+), 85 deletions(-) diff --git a/packages/@aws-cdk/aws-dynamodb/lib/replica-handler/index.ts b/packages/@aws-cdk/aws-dynamodb/lib/replica-handler/index.ts index 814bad346ece2..1554dcc84004d 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/replica-handler/index.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/replica-handler/index.ts @@ -5,27 +5,34 @@ import { DynamoDB } from 'aws-sdk'; // eslint-disable-line import/no-extraneous- export async function onEventHandler(event: OnEventRequest): Promise { console.log('Event: %j', event); - /** - * Process only Create and Delete requests. We shouldn't receive any - * update request and in case we do there is nothing to update. - */ + const dynamodb = new DynamoDB(); + + let updateTableAction: 'Create' | 'Update' | 'Delete'; if (event.RequestType === 'Create' || event.RequestType === 'Delete') { - const dynamodb = new DynamoDB(); - - const data = await dynamodb.updateTable({ - TableName: event.ResourceProperties.TableName, - ReplicaUpdates: [ - { - [event.RequestType]: { - RegionName: event.ResourceProperties.Region, - }, - }, - ], - }).promise(); - console.log('Update table: %j', data); + updateTableAction = event.RequestType; + } else { // Update + // This can only be a table replacement so we create a replica + // in the new table. The replica for the "old" table will be + // deleted when CF issues a Delete event on the old physical + // resource id. + updateTableAction = 'Create'; } - return { PhysicalResourceId: event.ResourceProperties.Region }; + const data = await dynamodb.updateTable({ + TableName: event.ResourceProperties.TableName, + ReplicaUpdates: [ + { + [updateTableAction]: { + RegionName: event.ResourceProperties.Region, + }, + }, + ], + }).promise(); + console.log('Update table: %j', data); + + return event.RequestType === 'Create' || event.RequestType === 'Update' + ? { PhysicalResourceId: `${event.ResourceProperties.TableName}-${event.ResourceProperties.Region}` } + : {}; } export async function isCompleteHandler(event: IsCompleteRequest): Promise { diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index 1b12eef42de1f..8f36894fc5df8 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -1670,12 +1670,19 @@ interface ScalableAttributePair { */ class SourceTableAttachedPolicy extends CoreConstruct implements iam.IGrantable { public readonly grantPrincipal: iam.IPrincipal; - public readonly policy: iam.IPolicy; + public readonly policy: iam.IManagedPolicy; public constructor(sourceTable: Table, role: iam.IRole) { - super(sourceTable, `SourceTableAttachedPolicy-${Names.nodeUniqueId(role.node)}`); - - const policy = new iam.Policy(this, 'Resource', { roles: [role] }); + super(sourceTable, `SourceTableAttachedManagedPolicy-${Names.nodeUniqueId(role.node)}`); + + const policy = new iam.ManagedPolicy(this, 'Resource', { + // A CF update of the description property of a managed policy requires + // a replacement. Use the table name in the description to force a managed + // policy replacement when the table name changes. This way we preserve permissions + // to delete old replicas in case of a table replacement. + description: `DynamoDB replication managed policy for table ${sourceTable.tableName}`, + roles: [role], + }); this.policy = policy; this.grantPrincipal = new SourceTableAttachedPrincipal(role, policy); } @@ -1686,7 +1693,7 @@ class SourceTableAttachedPolicy extends CoreConstruct implements iam.IGrantable * `SourceTableAttachedPolicy` class so it can act as an `IGrantable`. */ class SourceTableAttachedPrincipal extends iam.PrincipalBase { - public constructor(private readonly role: iam.IRole, private readonly policy: iam.Policy) { + public constructor(private readonly role: iam.IRole, private readonly policy: iam.ManagedPolicy) { super(); } diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.global-replicas-provisioned.expected.json b/packages/@aws-cdk/aws-dynamodb/test/integ.global-replicas-provisioned.expected.json index 89a9c3807fc21..b4ea44f2709c4 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/integ.global-replicas-provisioned.expected.json +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.global-replicas-provisioned.expected.json @@ -26,8 +26,8 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, - "TableSourceTableAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRoleD9856B77945CD5DF": { - "Type": "AWS::IAM::Policy", + "TableSourceTableAttachedManagedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRoleD9856B771F8F2CCB": { + "Type": "AWS::IAM::ManagedPolicy", "Properties": { "PolicyDocument": { "Statement": [ @@ -93,7 +93,18 @@ ], "Version": "2012-10-17" }, - "PolicyName": "leAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRoleD9856B77945CD5DF", + "Description": { + "Fn::Join": [ + "", + [ + "DynamoDB replication managed policy for table ", + { + "Ref": "TableCD117FA1" + } + ] + ] + }, + "Path": "/", "Roles": [ { "Fn::GetAtt": [ @@ -104,8 +115,8 @@ ] } }, - "TableSourceTableAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRoleBE2B1C1AE3D3CF6D": { - "Type": "AWS::IAM::Policy", + "TableSourceTableAttachedManagedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRoleBE2B1C1A5DC546D2": { + "Type": "AWS::IAM::ManagedPolicy", "Properties": { "PolicyDocument": { "Statement": [ @@ -127,7 +138,18 @@ ], "Version": "2012-10-17" }, - "PolicyName": "ttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRoleBE2B1C1AE3D3CF6D", + "Description": { + "Fn::Join": [ + "", + [ + "DynamoDB replication managed policy for table ", + { + "Ref": "TableCD117FA1" + } + ] + ] + }, + "Path": "/", "Roles": [ { "Fn::GetAtt": [ @@ -153,8 +175,8 @@ "Region": "us-east-2" }, "DependsOn": [ - "TableSourceTableAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRoleBE2B1C1AE3D3CF6D", - "TableSourceTableAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRoleD9856B77945CD5DF", + "TableSourceTableAttachedManagedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRoleBE2B1C1A5DC546D2", + "TableSourceTableAttachedManagedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRoleD9856B771F8F2CCB", "TableWriteScalingTargetE5669214", "TableWriteScalingTargetTrackingD78DCCD8" ], @@ -178,8 +200,8 @@ }, "DependsOn": [ "TableReplicauseast28A15C236", - "TableSourceTableAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRoleBE2B1C1AE3D3CF6D", - "TableSourceTableAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRoleD9856B77945CD5DF", + "TableSourceTableAttachedManagedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRoleBE2B1C1A5DC546D2", + "TableSourceTableAttachedManagedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRoleD9856B771F8F2CCB", "TableWriteScalingTargetE5669214", "TableWriteScalingTargetTrackingD78DCCD8" ], @@ -256,7 +278,7 @@ }, "/", { - "Ref": "AssetParameterse31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fdS3BucketEDAACFE7" + "Ref": "AssetParametersd56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aadS3Bucket806FEB2C" }, "/", { @@ -266,7 +288,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fdS3VersionKey6FF3D50F" + "Ref": "AssetParametersd56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aadS3VersionKey81C7BC5B" } ] } @@ -279,7 +301,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fdS3VersionKey6FF3D50F" + "Ref": "AssetParametersd56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aadS3VersionKey81C7BC5B" } ] } @@ -289,11 +311,11 @@ ] }, "Parameters": { - "referencetoawscdkdynamodbglobalreplicasprovisionedAssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3Bucket50997EC4Ref": { - "Ref": "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3Bucket1C6779E0" + "referencetoawscdkdynamodbglobalreplicasprovisionedAssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776S3BucketD1258B42Ref": { + "Ref": "AssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776S3BucketDEBF01E6" }, - "referencetoawscdkdynamodbglobalreplicasprovisionedAssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3VersionKey0F47C425Ref": { - "Ref": "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3VersionKey5C1D9275" + "referencetoawscdkdynamodbglobalreplicasprovisionedAssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776S3VersionKey0F5C355ERef": { + "Ref": "AssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776S3VersionKey42EBA2AE" }, "referencetoawscdkdynamodbglobalreplicasprovisionedAssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3Bucket6C51C355Ref": { "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1" @@ -334,17 +356,17 @@ } }, "Parameters": { - "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3Bucket1C6779E0": { + "AssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776S3BucketDEBF01E6": { "Type": "String", - "Description": "S3 bucket for asset \"f13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714\"" + "Description": "S3 bucket for asset \"dd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776\"" }, - "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3VersionKey5C1D9275": { + "AssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776S3VersionKey42EBA2AE": { "Type": "String", - "Description": "S3 key for asset version \"f13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714\"" + "Description": "S3 key for asset version \"dd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776\"" }, - "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714ArtifactHash477AAEA7": { + "AssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776ArtifactHash692B4CCE": { "Type": "String", - "Description": "Artifact hash for asset \"f13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714\"" + "Description": "Artifact hash for asset \"dd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776\"" }, "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1": { "Type": "String", @@ -358,17 +380,17 @@ "Type": "String", "Description": "Artifact hash for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" }, - "AssetParameterse31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fdS3BucketEDAACFE7": { + "AssetParametersd56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aadS3Bucket806FEB2C": { "Type": "String", - "Description": "S3 bucket for asset \"e31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fd\"" + "Description": "S3 bucket for asset \"d56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aad\"" }, - "AssetParameterse31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fdS3VersionKey6FF3D50F": { + "AssetParametersd56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aadS3VersionKey81C7BC5B": { "Type": "String", - "Description": "S3 key for asset version \"e31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fd\"" + "Description": "S3 key for asset version \"d56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aad\"" }, - "AssetParameterse31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fdArtifactHash898696F1": { + "AssetParametersd56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aadArtifactHashD0230F6F": { "Type": "String", - "Description": "Artifact hash for asset \"e31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fd\"" + "Description": "Artifact hash for asset \"d56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aad\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json b/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json index a66dd3d965ed9..3896ac3a355b2 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json @@ -41,8 +41,8 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, - "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4AA4E210EA": { - "Type": "AWS::IAM::Policy", + "TableSourceTableAttachedManagedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4A23250B4C": { + "Type": "AWS::IAM::ManagedPolicy", "Properties": { "PolicyDocument": { "Statement": [ @@ -119,7 +119,18 @@ ], "Version": "2012-10-17" }, - "PolicyName": "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4AA4E210EA", + "Description": { + "Fn::Join": [ + "", + [ + "DynamoDB replication managed policy for table ", + { + "Ref": "TableCD117FA1" + } + ] + ] + }, + "Path": "/", "Roles": [ { "Fn::GetAtt": [ @@ -130,8 +141,8 @@ ] } }, - "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole397161288F61AAFA": { - "Type": "AWS::IAM::Policy", + "TableSourceTableAttachedManagedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole3971612857304880": { + "Type": "AWS::IAM::ManagedPolicy", "Properties": { "PolicyDocument": { "Statement": [ @@ -164,7 +175,18 @@ ], "Version": "2012-10-17" }, - "PolicyName": "leSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole397161288F61AAFA", + "Description": { + "Fn::Join": [ + "", + [ + "DynamoDB replication managed policy for table ", + { + "Ref": "TableCD117FA1" + } + ] + ] + }, + "Path": "/", "Roles": [ { "Fn::GetAtt": [ @@ -190,8 +212,8 @@ "Region": "eu-west-2" }, "DependsOn": [ - "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole397161288F61AAFA", - "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4AA4E210EA" + "TableSourceTableAttachedManagedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole3971612857304880", + "TableSourceTableAttachedManagedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4A23250B4C" ], "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -212,8 +234,8 @@ }, "DependsOn": [ "TableReplicaeuwest290D3CD3A", - "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole397161288F61AAFA", - "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4AA4E210EA" + "TableSourceTableAttachedManagedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole3971612857304880", + "TableSourceTableAttachedManagedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4A23250B4C" ], "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -231,7 +253,7 @@ }, "/", { - "Ref": "AssetParametersf8cfc24954f0c95960d9a93888c01bf5e95802f26bfa5dc6fde5c913a1429ceaS3Bucket434BDB62" + "Ref": "AssetParametersa789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4S3Bucket8BB0CECD" }, "/", { @@ -241,7 +263,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersf8cfc24954f0c95960d9a93888c01bf5e95802f26bfa5dc6fde5c913a1429ceaS3VersionKey01638790" + "Ref": "AssetParametersa789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4S3VersionKeyC531296D" } ] } @@ -254,7 +276,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersf8cfc24954f0c95960d9a93888c01bf5e95802f26bfa5dc6fde5c913a1429ceaS3VersionKey01638790" + "Ref": "AssetParametersa789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4S3VersionKeyC531296D" } ] } @@ -264,11 +286,11 @@ ] }, "Parameters": { - "referencetocdkdynamodbglobal20191121AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3Bucket71E24D5BRef": { - "Ref": "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3Bucket1C6779E0" + "referencetocdkdynamodbglobal20191121AssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776S3Bucket06999F76Ref": { + "Ref": "AssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776S3BucketDEBF01E6" }, - "referencetocdkdynamodbglobal20191121AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3VersionKeyD88E8BACRef": { - "Ref": "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3VersionKey5C1D9275" + "referencetocdkdynamodbglobal20191121AssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776S3VersionKey3D988AD7Ref": { + "Ref": "AssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776S3VersionKey42EBA2AE" }, "referencetocdkdynamodbglobal20191121AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketC7F3A147Ref": { "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1" @@ -283,17 +305,17 @@ } }, "Parameters": { - "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3Bucket1C6779E0": { + "AssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776S3BucketDEBF01E6": { "Type": "String", - "Description": "S3 bucket for asset \"f13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714\"" + "Description": "S3 bucket for asset \"dd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776\"" }, - "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3VersionKey5C1D9275": { + "AssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776S3VersionKey42EBA2AE": { "Type": "String", - "Description": "S3 key for asset version \"f13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714\"" + "Description": "S3 key for asset version \"dd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776\"" }, - "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714ArtifactHash477AAEA7": { + "AssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776ArtifactHash692B4CCE": { "Type": "String", - "Description": "Artifact hash for asset \"f13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714\"" + "Description": "Artifact hash for asset \"dd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776\"" }, "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1": { "Type": "String", @@ -307,17 +329,17 @@ "Type": "String", "Description": "Artifact hash for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" }, - "AssetParametersf8cfc24954f0c95960d9a93888c01bf5e95802f26bfa5dc6fde5c913a1429ceaS3Bucket434BDB62": { + "AssetParametersa789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4S3Bucket8BB0CECD": { "Type": "String", - "Description": "S3 bucket for asset \"f8cfc24954f0c95960d9a93888c01bf5e95802f26bfa5dc6fde5c913a1429cea\"" + "Description": "S3 bucket for asset \"a789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4\"" }, - "AssetParametersf8cfc24954f0c95960d9a93888c01bf5e95802f26bfa5dc6fde5c913a1429ceaS3VersionKey01638790": { + "AssetParametersa789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4S3VersionKeyC531296D": { "Type": "String", - "Description": "S3 key for asset version \"f8cfc24954f0c95960d9a93888c01bf5e95802f26bfa5dc6fde5c913a1429cea\"" + "Description": "S3 key for asset version \"a789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4\"" }, - "AssetParametersf8cfc24954f0c95960d9a93888c01bf5e95802f26bfa5dc6fde5c913a1429ceaArtifactHashD0E61C22": { + "AssetParametersa789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4ArtifactHash9D92B407": { "Type": "String", - "Description": "Artifact hash for asset \"f8cfc24954f0c95960d9a93888c01bf5e95802f26bfa5dc6fde5c913a1429cea\"" + "Description": "Artifact hash for asset \"a789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-dynamodb/test/replica-provider.test.ts b/packages/@aws-cdk/aws-dynamodb/test/replica-provider.test.ts index 3a1d97bd4b345..4b5acef3d15cb 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/replica-provider.test.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/replica-provider.test.ts @@ -54,25 +54,62 @@ test('on event', async () => { }); expect(data).toEqual({ - PhysicalResourceId: 'eu-west-2', + PhysicalResourceId: 'my-table-eu-west-2', }); }); -test('on event does not call updateTable for Update requests', async () => { +test('on event calls updateTable with Create for Update requests with table replacement', async () => { const updateTableMock = sinon.fake.resolves({}); AWS.mock('DynamoDB', 'updateTable', updateTableMock); const data = await onEventHandler({ ...createEvent, + OldResourceProperties: { + TableName: 'my-old-table', + }, RequestType: 'Update', }); - sinon.assert.notCalled(updateTableMock); + sinon.assert.calledWith(updateTableMock, { + TableName: 'my-table', + ReplicaUpdates: [ + { + Create: { + RegionName: 'eu-west-2', + }, + }, + ], + }); expect(data).toEqual({ - PhysicalResourceId: 'eu-west-2', + PhysicalResourceId: 'my-table-eu-west-2', + }); +}); + +test('on event calls updateTable with Delete', async () => { + const updateTableMock = sinon.fake.resolves({}); + + AWS.mock('DynamoDB', 'updateTable', updateTableMock); + + const data = await onEventHandler({ + ...createEvent, + RequestType: 'Delete', + }); + + sinon.assert.calledWith(updateTableMock, { + TableName: 'my-table', + ReplicaUpdates: [ + { + Delete: { + RegionName: 'eu-west-2', + }, + }, + ], }); + + // Physical resource id never changed on Delete + expect(data).toEqual({}); }); test('is complete for create returns false without replicas', async () => { From 22b9b3d474dcc5f8b3d610b43cb648d0ec0d710f Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Fri, 5 Mar 2021 14:48:44 -0800 Subject: [PATCH 16/28] revert: "chore: add new interfaces for Assets (#13356)" (#13426) This reverts commit 48963f73 ---- *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 | 17 ++----- packages/@aws-cdk/aws-s3-assets/lib/asset.ts | 4 +- packages/@aws-cdk/core/lib/fs/options.ts | 51 +++++-------------- 4 files changed, 19 insertions(+), 54 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 3dd422c694176..26a3a40f35335 100644 --- a/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts +++ b/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts @@ -2,7 +2,7 @@ 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, FeatureFlags, FileFingerprintOptions, IgnoreMode, Stack, SymlinkFollowMode, Token } from '@aws-cdk/core'; +import { Annotations, FeatureFlags, IgnoreMode, Stack, Token } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; @@ -13,7 +13,7 @@ import { Construct as CoreConstruct } from '@aws-cdk/core'; /** * Options for DockerImageAsset */ -export interface DockerImageAssetOptions extends assets.FingerprintOptions, FileFingerprintOptions { +export interface DockerImageAssetOptions extends assets.FingerprintOptions { /** * ECR repository name * @@ -141,9 +141,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, @@ -186,13 +185,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 aa342337a9df3..510834a61c634 100644 --- a/packages/@aws-cdk/aws-s3-assets/lib/asset.ts +++ b/packages/@aws-cdk/aws-s3-assets/lib/asset.ts @@ -12,7 +12,7 @@ import { toSymlinkFollow } from './compat'; // eslint-disable-next-line no-duplicate-imports, import/order import { Construct as CoreConstruct } from '@aws-cdk/core'; -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. @@ -125,7 +125,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 623675d2f8fb2786f23beb87994e687e8a7c6612 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Fri, 5 Mar 2021 17:20:53 -0800 Subject: [PATCH 17/28] fix(cfn-include): allow dynamic mappings to be used in Fn::FindInMap (#13428) The template parsing logic in cloudformation-include always searched for the Mapping in the template based on the first argument passed to Fn::FindInMap. However, that doesn't work if that first argument is a dynamic expression, like `{ Ref: Param }`. Check for that case explicitly, and don't search for the Mapping if the first argument to Fn::FindInMap is a dynamic expression. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../find-in-map-with-dynamic-mapping.json | 30 +++++++++++++++++++ .../test/valid-templates.test.ts | 8 +++++ packages/@aws-cdk/core/lib/cfn-parse.ts | 16 +++++++--- 3 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/find-in-map-with-dynamic-mapping.json diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/find-in-map-with-dynamic-mapping.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/find-in-map-with-dynamic-mapping.json new file mode 100644 index 0000000000000..aedf3250272fc --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/find-in-map-with-dynamic-mapping.json @@ -0,0 +1,30 @@ +{ + "Parameters": { + "Stage": { + "Type": "String", + "AllowedValues": ["beta"], + "Default": "beta" + } + }, + "Mappings": { + "beta": { + "region": { + "key1": "name" + } + } + }, + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": { + "Fn::FindInMap": [ + { "Ref": "Stage" }, + "region", + "key1" + ] + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts index 873add618b9d0..34b403a2e7a99 100644 --- a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts @@ -746,6 +746,14 @@ describe('CDK Include', () => { }).toThrow(/Mapping with name 'NonExistentMapping' was not found in the template/); }); + test('can ingest a template that uses Fn::FindInMap with the first argument being a dynamic reference', () => { + includeTestTemplate(stack, 'find-in-map-with-dynamic-mapping.json'); + + expect(stack).toMatchTemplate( + loadTestFileToJsObject('find-in-map-with-dynamic-mapping.json'), + ); + }); + test('handles renaming Mapping references', () => { const cfnTemplate = includeTestTemplate(stack, 'only-mapping-and-bucket.json'); const someMapping = cfnTemplate.getMapping('SomeMapping'); diff --git a/packages/@aws-cdk/core/lib/cfn-parse.ts b/packages/@aws-cdk/core/lib/cfn-parse.ts index 1a5c245b61c4e..93376ae19d365 100644 --- a/packages/@aws-cdk/core/lib/cfn-parse.ts +++ b/packages/@aws-cdk/core/lib/cfn-parse.ts @@ -566,11 +566,19 @@ export class CfnParser { case 'Fn::FindInMap': { const value = this.parseValue(object[key]); // the first argument to FindInMap is the mapping name - const mapping = this.finder.findMapping(value[0]); - if (!mapping) { - throw new Error(`Mapping used in FindInMap expression with name '${value[0]}' was not found in the template`); + let mappingName: string; + if (Token.isUnresolved(value[0])) { + // the first argument can be a dynamic expression like Ref: Param; + // if it is, we can't find the mapping in advance + mappingName = value[0]; + } else { + const mapping = this.finder.findMapping(value[0]); + if (!mapping) { + throw new Error(`Mapping used in FindInMap expression with name '${value[0]}' was not found in the template`); + } + mappingName = mapping.logicalId; } - return Fn._findInMap(mapping.logicalId, value[1], value[2]); + return Fn._findInMap(mappingName, value[1], value[2]); } case 'Fn::Select': { const value = this.parseValue(object[key]); From 8f080ff42282f054b78b97eb605a44bdc1301708 Mon Sep 17 00:00:00 2001 From: AWS CDK Team Date: Sat, 6 Mar 2021 01:28:30 +0000 Subject: [PATCH 18/28] chore(release): 1.92.0 --- CHANGELOG.md | 84 +++++++++++++++++++++++++++++++++++++++++++++++++ version.v1.json | 2 +- 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dd8c2c1991b1..d521b06255057 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,90 @@ 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.92.0](https://github.com/aws/aws-cdk/compare/v1.91.0...v1.92.0) (2021-03-06) + + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **apigatewayv2:** `HttpApiMapping` (and related interfaces for `Attributed` and `Props`) has been renamed to `ApiMapping` +* **apigatewayv2:** `CommonStageOptions` has been renamed to `StageOptions` +* **apigatewayv2:** `HttpStage.fromStageName` has been removed in favour of `HttpStage.fromHttpStageAttributes` +* **apigatewayv2:** `DefaultDomainMappingOptions` has been removed in favour of `DomainMappingOptions` +* **apigatewayv2:** `HttpApiProps.defaultDomainMapping` has been changed from `DefaultDomainMappingOptions` to `DomainMappingOptions` +* **apigatewayv2:** `HttpApi.defaultStage` has been changed from `HttpStage` to `IStage` +* **apigatewayv2:** `IHttpApi.defaultStage` has been removed +* **ecs-patterns:** ** the desiredCount property stored on the above constructs will be optional, allowing them to be undefined. This is enabled through the `@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount` feature flag. We would recommend all aws-cdk users to set the `REMOVE_DEFAULT_DESIRED_COUNT` flag to true for all of their existing applications. + +Fixes: https://github.com/aws/aws-cdk/issues/12990 +* **aws-appsync:** RdsDataSource now takes a ServerlessCluster instead of a DatabaseCluster +* **aws-appsync:** graphqlapi.addRdsDataSource now takes databaseName as its fourth argument + +### Features + +* **apigateway:** integrate with aws services in a different region ([#13251](https://github.com/aws/aws-cdk/issues/13251)) ([d942699](https://github.com/aws/aws-cdk/commit/d9426996c07ff909993594ed91cfcf2b5761414b)), closes [#7009](https://github.com/aws/aws-cdk/issues/7009) +* **apigatewayv2:** websocket api ([#13031](https://github.com/aws/aws-cdk/issues/13031)) ([fe1c839](https://github.com/aws/aws-cdk/commit/fe1c8393e0840fb273c4a5f325cb3cebc784bf4b)), closes [#2872](https://github.com/aws/aws-cdk/issues/2872) +* **aws-appsync:** add databaseName to rdsDataSource ([#12575](https://github.com/aws/aws-cdk/issues/12575)) ([f92b65e](https://github.com/aws/aws-cdk/commit/f92b65e2a158f918d8f05132ed12a4bb85228997)), closes [#12572](https://github.com/aws/aws-cdk/issues/12572) +* **aws-events:** Event Bus target ([#12926](https://github.com/aws/aws-cdk/issues/12926)) ([ea91aa3](https://github.com/aws/aws-cdk/commit/ea91aa31db9e2f31c734ad6d7e1f64d5d432dfd4)), closes [#9473](https://github.com/aws/aws-cdk/issues/9473) +* **aws-route53-targets:** add global accelerator target to route53 alias targets ([#13407](https://github.com/aws/aws-cdk/issues/13407)) ([2672a55](https://github.com/aws/aws-cdk/commit/2672a55c393e5ce7dd9a230d921ec1be1a23e32a)), closes [#12839](https://github.com/aws/aws-cdk/issues/12839) +* **aws-s3:** adds s3 bucket AWS FSBP option ([#12804](https://github.com/aws/aws-cdk/issues/12804)) ([b9cdd52](https://github.com/aws/aws-cdk/commit/b9cdd52274eca55940c65b830939132d0e074365)), closes [#10969](https://github.com/aws/aws-cdk/issues/10969) +* **cfnspec:** cloudformation spec v28.0.0 ([#13101](https://github.com/aws/aws-cdk/issues/13101)) ([13c9859](https://github.com/aws/aws-cdk/commit/13c9859cc62b3d472ba1be84b12d478f61f02ec9)) +* **cfnspec:** cloudformation spec v29.0.0 ([#13249](https://github.com/aws/aws-cdk/issues/13249)) ([6318e26](https://github.com/aws/aws-cdk/commit/6318e2632297783bc8b5b2609bba096dd83a1113)) +* **cfnspec:** cloudformation spec v30.0.0 ([#13365](https://github.com/aws/aws-cdk/issues/13365)) ([ae0185d](https://github.com/aws/aws-cdk/commit/ae0185dd089e3bb7c5639ebc1bce3f95e126f71c)) +* **cli:** Configurable --change-set-name CLI flag ([#13024](https://github.com/aws/aws-cdk/issues/13024)) ([18184df](https://github.com/aws/aws-cdk/commit/18184df05f5b8478ef9cae1285e45e61a0833822)), closes [#11075](https://github.com/aws/aws-cdk/issues/11075) [/github.com/aws/aws-cdk/pull/12683#issuecomment-778465771](https://github.com/aws//github.com/aws/aws-cdk/pull/12683/issues/issuecomment-778465771) +* **cloudwatch:** EC2 actions ([#13281](https://github.com/aws/aws-cdk/issues/13281)) ([319cfcd](https://github.com/aws/aws-cdk/commit/319cfcdaaf92e4e6edb8c2388d04dce0971aaf86)), closes [#13228](https://github.com/aws/aws-cdk/issues/13228) +* **cognito:** user pools - sign in with apple ([#13160](https://github.com/aws/aws-cdk/issues/13160)) ([b965589](https://github.com/aws/aws-cdk/commit/b965589358f4c281aea36404276f08128e6ff3db)) +* **core:** `description` parameter in the CustomResourceProvider ([#13275](https://github.com/aws/aws-cdk/issues/13275)) ([78831cf](https://github.com/aws/aws-cdk/commit/78831cf9dec0407e7d827711183ac47be070f480)), closes [#13277](https://github.com/aws/aws-cdk/issues/13277) [#13276](https://github.com/aws/aws-cdk/issues/13276) +* **core:** customize bundling output packaging ([#13152](https://github.com/aws/aws-cdk/issues/13152)) ([6eca979](https://github.com/aws/aws-cdk/commit/6eca979f65542f3e44461588d8220e8c0bf76a6e)) +* **ec2:** Add VPC endpoint for RDS ([#12497](https://github.com/aws/aws-cdk/issues/12497)) ([fc87574](https://github.com/aws/aws-cdk/commit/fc8757437c37a0947cced720ff363b8858850f72)), closes [#12402](https://github.com/aws/aws-cdk/issues/12402) +* **ecs:** add port mappings to containers with props ([#13262](https://github.com/aws/aws-cdk/issues/13262)) ([f511639](https://github.com/aws/aws-cdk/commit/f511639bba156f6edd15896a4dd8e27b07671ea1)), closes [#13261](https://github.com/aws/aws-cdk/issues/13261) +* **ecs:** allow selection of container and port for SRV service discovery records ([#12798](https://github.com/aws/aws-cdk/issues/12798)) ([a452bc3](https://github.com/aws/aws-cdk/commit/a452bc385640762a043392a717d49de29abcc64e)), closes [#12796](https://github.com/aws/aws-cdk/issues/12796) +* **ecs-patterns:** Add support for assignPublicIp for QueueProcessingFargateService ([#13122](https://github.com/aws/aws-cdk/issues/13122)) ([3fb4600](https://github.com/aws/aws-cdk/commit/3fb46001a7345cbefa6df70893999bcb304ed40d)), closes [#12815](https://github.com/aws/aws-cdk/issues/12815) +* **ecs-patterns:** remove default desiredCount to align with cfn behaviour (under feature flag) ([#13130](https://github.com/aws/aws-cdk/issues/13130)) ([a9caa45](https://github.com/aws/aws-cdk/commit/a9caa455b708e08f1cf2d366ac32892d4faa59b4)) +* **elasticloadbalancingv2:** Add support for application cookies ([#13142](https://github.com/aws/aws-cdk/issues/13142)) ([23385dd](https://github.com/aws/aws-cdk/commit/23385ddeb0decd227a0104d7b0aff06939acaad9)) +* **elbv2:** allow control of ingress rules on redirect listener ([#12768](https://github.com/aws/aws-cdk/issues/12768)) ([b7b441f](https://github.com/aws/aws-cdk/commit/b7b441f74a07d26fd8de23df84e7ab4663c89c0c)), closes [#12766](https://github.com/aws/aws-cdk/issues/12766) +* **events:** archive events ([#12060](https://github.com/aws/aws-cdk/issues/12060)) ([465cd9c](https://github.com/aws/aws-cdk/commit/465cd9c434acff74070ca6d33891e1481e253128)), closes [#11531](https://github.com/aws/aws-cdk/issues/11531) +* **events:** dead letter queue for Lambda Targets ([#11617](https://github.com/aws/aws-cdk/issues/11617)) ([1bb3650](https://github.com/aws/aws-cdk/commit/1bb3650c5dd2087b05793a5e903cdfb80fc5c1ad)), closes [#11612](https://github.com/aws/aws-cdk/issues/11612) +* **lambda:** code signing config ([#12656](https://github.com/aws/aws-cdk/issues/12656)) ([778ea27](https://github.com/aws/aws-cdk/commit/778ea2759a8a4504dc232eb6b1d77a38f8ee7aef)), closes [#12216](https://github.com/aws/aws-cdk/issues/12216) +* **lambda:** Code.fromDockerBuild ([#13318](https://github.com/aws/aws-cdk/issues/13318)) ([ad01099](https://github.com/aws/aws-cdk/commit/ad01099d5b8f835c3b87d7d20fd2dc1a5df2fd6f)), closes [#13273](https://github.com/aws/aws-cdk/issues/13273) +* **lambda:** Code.fromDockerBuildAsset ([#12258](https://github.com/aws/aws-cdk/issues/12258)) ([09afed5](https://github.com/aws/aws-cdk/commit/09afed5ca2b39919c1c84d200370d490110cd0d1)), closes [#11914](https://github.com/aws/aws-cdk/issues/11914) +* **neptune:** high level constructs for db clusters and instances ([#12763](https://github.com/aws/aws-cdk/issues/12763)) ([c366837](https://github.com/aws/aws-cdk/commit/c36683701d88eb0c53fdd2add66b10c47c05f56b)), closes [aws#12762](https://github.com/aws/aws/issues/12762) +* **stepfunctions-tasks:** add EKS call to SFN-tasks ([#12779](https://github.com/aws/aws-cdk/issues/12779)) ([296a10d](https://github.com/aws/aws-cdk/commit/296a10d76a9f6fc2a374d1a6461c460bcc3eeb79)) +* **synthetics:** Update CloudWatch Synthetics NodeJS runtimes ([#12907](https://github.com/aws/aws-cdk/issues/12907)) ([6aac3b6](https://github.com/aws/aws-cdk/commit/6aac3b6a9bb1586ee16e7a85ca657b544d0f8304)), closes [#12906](https://github.com/aws/aws-cdk/issues/12906) + + +### Bug Fixes + +* **appsync:** revert to allow resolver creation from data source ([#12973](https://github.com/aws/aws-cdk/issues/12973)) ([d35f032](https://github.com/aws/aws-cdk/commit/d35f03226d6d7fb5be246b4d3584ee9205b0ef2d)), closes [#12635](https://github.com/aws/aws-cdk/issues/12635) [#11522](https://github.com/aws/aws-cdk/issues/11522) +* **aws-appsync:** use serverlessCluster on rdsDataSource ([#13206](https://github.com/aws/aws-cdk/issues/13206)) ([45cf387](https://github.com/aws/aws-cdk/commit/45cf3873fb48d4043e7a22284d36695ea6bde6ef)), closes [#12567](https://github.com/aws/aws-cdk/issues/12567) +* **cfn-diff:** handle Fn::If inside policies and statements ([#12975](https://github.com/aws/aws-cdk/issues/12975)) ([daf4e47](https://github.com/aws/aws-cdk/commit/daf4e47a790ab99639e471f6792f22e3e4f8ee73)), closes [#12887](https://github.com/aws/aws-cdk/issues/12887) +* **cfn-include:** allow dynamic mappings to be used in Fn::FindInMap ([#13428](https://github.com/aws/aws-cdk/issues/13428)) ([623675d](https://github.com/aws/aws-cdk/commit/623675d2f8fb2786f23beb87994e687e8a7c6612)) +* **cloudfront:** cannot add two EdgeFunctions with same aliases ([#13324](https://github.com/aws/aws-cdk/issues/13324)) ([1f35351](https://github.com/aws/aws-cdk/commit/1f3535145d22b2b13ebbcbfe31a3bfd73519352d)), closes [#13237](https://github.com/aws/aws-cdk/issues/13237) +* **cloudwatch:** MathExpression period of <5 minutes is not respected ([#13078](https://github.com/aws/aws-cdk/issues/13078)) ([d9ee914](https://github.com/aws/aws-cdk/commit/d9ee91432918aa113f728abdd61295096ed1512f)), closes [#9156](https://github.com/aws/aws-cdk/issues/9156) +* **cloudwatch:** metric `label` not rendered into Alarms ([#13070](https://github.com/aws/aws-cdk/issues/13070)) ([cbcc712](https://github.com/aws/aws-cdk/commit/cbcc712e0c4c44c83c7f4d1e8a544bccfa26bb56)) +* **codebuild:** allow FILE_PATH webhook filter for BitBucket ([#13186](https://github.com/aws/aws-cdk/issues/13186)) ([cbed348](https://github.com/aws/aws-cdk/commit/cbed3488f03bdfba16f3950bda653535c8999db1)), closes [#13175](https://github.com/aws/aws-cdk/issues/13175) +* **core:** custom resource provider NODEJS_12 now looks like Lambda's NODEJS_12_X, add Node 14 ([#13301](https://github.com/aws/aws-cdk/issues/13301)) ([3413b2f](https://github.com/aws/aws-cdk/commit/3413b2f887596d11dfb53c0e99c2a1788095a2ad)) +* **core:** ENOTDIR invalid cwd on "cdk deploy" ([#13145](https://github.com/aws/aws-cdk/issues/13145)) ([cd7a3ed](https://github.com/aws/aws-cdk/commit/cd7a3ed333570a3b26446e1e3a054ca886cd3906)), closes [#12258](https://github.com/aws/aws-cdk/issues/12258) [#13076](https://github.com/aws/aws-cdk/issues/13076) [#13131](https://github.com/aws/aws-cdk/issues/13131) +* **custom-resources:** unable to use a resource attributes as dictionary keys in AwsCustomResource ([#13074](https://github.com/aws/aws-cdk/issues/13074)) ([3cb3104](https://github.com/aws/aws-cdk/commit/3cb31043a42b035f6dcd2a318836d4bfc4973151)), closes [#13063](https://github.com/aws/aws-cdk/issues/13063) +* **dynamodb:** replicas not created on table replacement ([#13300](https://github.com/aws/aws-cdk/issues/13300)) ([c7c424f](https://github.com/aws/aws-cdk/commit/c7c424fec42f1f14ab8bdc3011f5bdb602918aa3)), closes [#12332](https://github.com/aws/aws-cdk/issues/12332) +* **ec2:** NAT provider's default outbound rules cannot be disabled ([#12674](https://github.com/aws/aws-cdk/issues/12674)) ([664133a](https://github.com/aws/aws-cdk/commit/664133a35da2bd096a237971ce662f3dd38b297f)), closes [#12673](https://github.com/aws/aws-cdk/issues/12673) +* **ec2:** readme grammar ([#13180](https://github.com/aws/aws-cdk/issues/13180)) ([fe4f056](https://github.com/aws/aws-cdk/commit/fe4f05678c06d634d3fe9e1b608e444a57f67b9c)) +* **ec2:** Throw error on empty InitFile content ([#13009](https://github.com/aws/aws-cdk/issues/13009)) ([#13119](https://github.com/aws/aws-cdk/issues/13119)) ([81a78a3](https://github.com/aws/aws-cdk/commit/81a78a31408276ebb020e45b15ddca7a2c57ae50)) +* **ecr:** Allow referencing an EcrImage by digest instead of tag ([#13299](https://github.com/aws/aws-cdk/issues/13299)) ([266a621](https://github.com/aws/aws-cdk/commit/266a621abfc34c62ff1e26de9cb8cf0687588f89)), closes [#5082](https://github.com/aws/aws-cdk/issues/5082) +* **ecr:** Generate valid CloudFormation for imageScanOnPush ([#13420](https://github.com/aws/aws-cdk/issues/13420)) ([278fba5](https://github.com/aws/aws-cdk/commit/278fba5df4a3d785e49bdb57ccf88fd34bacacbb)), closes [#13418](https://github.com/aws/aws-cdk/issues/13418) +* **ecs:** services essential container exceptions thrown too soon ([#13240](https://github.com/aws/aws-cdk/issues/13240)) ([c174f6c](https://github.com/aws/aws-cdk/commit/c174f6c2f4dd909e07be34b66bd6b3a92d5e8484)), closes [#13239](https://github.com/aws/aws-cdk/issues/13239) +* **eks:** `KubectlProvider` creates un-necessary security group ([#13178](https://github.com/aws/aws-cdk/issues/13178)) ([c5e8b6d](https://github.com/aws/aws-cdk/commit/c5e8b6df1e5f0359d51d025edcc68508ab5daef1)) +* UserPool, Volume, ElasticSearch, FSx are now RETAIN by default ([#12920](https://github.com/aws/aws-cdk/issues/12920)) ([5a54741](https://github.com/aws/aws-cdk/commit/5a54741a414d3f8b7913163f4785759b984b41d8)), closes [#12563](https://github.com/aws/aws-cdk/issues/12563) +* **eks:** Deployment fails for the first deployment in an account ([#13103](https://github.com/aws/aws-cdk/issues/13103)) ([e042879](https://github.com/aws/aws-cdk/commit/e042879851f8ddd558d20941019c9a6692a1c2bf)), closes [/github.com/aws/aws-cdk/issues/9027#issuecomment-780482124](https://github.com/aws//github.com/aws/aws-cdk/issues/9027/issues/issuecomment-780482124) +* incorrect peerDependency on "constructs" ([#13255](https://github.com/aws/aws-cdk/issues/13255)) ([17244af](https://github.com/aws/aws-cdk/commit/17244af0d181a28b908fa161250c5a3285521c53)) +* **elasticloadbalancingv2:** should allow more than 2 certificates ([#13332](https://github.com/aws/aws-cdk/issues/13332)) ([d3155e9](https://github.com/aws/aws-cdk/commit/d3155e97fd9331a4732396941ce4ad20613fe81c)), closes [#13150](https://github.com/aws/aws-cdk/issues/13150) +* **events:** cannot trigger multiple Lambdas from the same Rule ([#13260](https://github.com/aws/aws-cdk/issues/13260)) ([c8c1762](https://github.com/aws/aws-cdk/commit/c8c1762c213aad1062c3a0bc48b22b05c3a0a185)), closes [#13231](https://github.com/aws/aws-cdk/issues/13231) +* **events:** imported ECS Task Definition cannot be used as target ([#13293](https://github.com/aws/aws-cdk/issues/13293)) ([6f7cebd](https://github.com/aws/aws-cdk/commit/6f7cebdf61073cc1fb358fcac5f5b2156389cb81)), closes [#12811](https://github.com/aws/aws-cdk/issues/12811) +* **lambda-nodejs:** 'must use "outdir"' error with spaces in paths ([#13268](https://github.com/aws/aws-cdk/issues/13268)) ([09723f5](https://github.com/aws/aws-cdk/commit/09723f58ed3034fc2cb46316e6d798cb8f2bf96e)), closes [#13210](https://github.com/aws/aws-cdk/issues/13210) +* **lambda-nodejs:** invalid sample in documentation ([#12404](https://github.com/aws/aws-cdk/issues/12404)) ([520c263](https://github.com/aws/aws-cdk/commit/520c263ca3c6b0ea7d9c09c23e509a3373ee2b8a)) +* **lambda-nodejs:** paths with spaces break esbuild ([#13312](https://github.com/aws/aws-cdk/issues/13312)) ([f983fbb](https://github.com/aws/aws-cdk/commit/f983fbb474ecd6727b0c5a35333718cc55d78bf1)), closes [#13311](https://github.com/aws/aws-cdk/issues/13311) +* **lambda-python:** asset hash is non-deterministic ([#12984](https://github.com/aws/aws-cdk/issues/12984)) ([37debc0](https://github.com/aws/aws-cdk/commit/37debc0513c5174ca3d918fce94a138d5d34b586)), closes [#12770](https://github.com/aws/aws-cdk/issues/12770) [#12684](https://github.com/aws/aws-cdk/issues/12684) +* **stepfunctions:** `SageMakeUpdateEndpoint` adds insufficient permissions ([#13170](https://github.com/aws/aws-cdk/issues/13170)) ([6126e49](https://github.com/aws/aws-cdk/commit/6126e499e5ca22b5f751af4f4f05d74f696829f1)), closes [#11594](https://github.com/aws/aws-cdk/issues/11594) + ## [1.91.0](https://github.com/aws/aws-cdk/compare/v1.90.1...v1.91.0) (2021-02-23) diff --git a/version.v1.json b/version.v1.json index aa566c6d404ff..c2a1515792517 100644 --- a/version.v1.json +++ b/version.v1.json @@ -1,3 +1,3 @@ { - "version": "1.91.0" + "version": "1.92.0" } From bf65f62ea3ff52de6dbcb7791a95ec48d9d3a3ff Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Fri, 5 Mar 2021 17:33:05 -0800 Subject: [PATCH 19/28] Re-word Changelog for the ECS change --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d521b06255057..1bcd9cb9c47a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. See [standa ## [1.92.0](https://github.com/aws/aws-cdk/compare/v1.91.0...v1.92.0) (2021-03-06) +* **ecs-patterns**: the `desiredCount` property stored on the above constructs will be optional, allowing them to be undefined. This is enabled through the `@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount` feature flag. We would recommend all CDK users to set the `@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount` flag to `true` for all of their existing applications. ### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES @@ -14,9 +15,6 @@ All notable changes to this project will be documented in this file. See [standa * **apigatewayv2:** `HttpApiProps.defaultDomainMapping` has been changed from `DefaultDomainMappingOptions` to `DomainMappingOptions` * **apigatewayv2:** `HttpApi.defaultStage` has been changed from `HttpStage` to `IStage` * **apigatewayv2:** `IHttpApi.defaultStage` has been removed -* **ecs-patterns:** ** the desiredCount property stored on the above constructs will be optional, allowing them to be undefined. This is enabled through the `@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount` feature flag. We would recommend all aws-cdk users to set the `REMOVE_DEFAULT_DESIRED_COUNT` flag to true for all of their existing applications. - -Fixes: https://github.com/aws/aws-cdk/issues/12990 * **aws-appsync:** RdsDataSource now takes a ServerlessCluster instead of a DatabaseCluster * **aws-appsync:** graphqlapi.addRdsDataSource now takes databaseName as its fourth argument From b17538f31ddcfdf3b21c4d810f584e5e1487e444 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Fri, 5 Mar 2021 17:37:21 -0800 Subject: [PATCH 20/28] Correct Changelog entry for EKS --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bcd9cb9c47a1..5b873247ca7ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,7 +73,7 @@ All notable changes to this project will be documented in this file. See [standa * **ecs:** services essential container exceptions thrown too soon ([#13240](https://github.com/aws/aws-cdk/issues/13240)) ([c174f6c](https://github.com/aws/aws-cdk/commit/c174f6c2f4dd909e07be34b66bd6b3a92d5e8484)), closes [#13239](https://github.com/aws/aws-cdk/issues/13239) * **eks:** `KubectlProvider` creates un-necessary security group ([#13178](https://github.com/aws/aws-cdk/issues/13178)) ([c5e8b6d](https://github.com/aws/aws-cdk/commit/c5e8b6df1e5f0359d51d025edcc68508ab5daef1)) * UserPool, Volume, ElasticSearch, FSx are now RETAIN by default ([#12920](https://github.com/aws/aws-cdk/issues/12920)) ([5a54741](https://github.com/aws/aws-cdk/commit/5a54741a414d3f8b7913163f4785759b984b41d8)), closes [#12563](https://github.com/aws/aws-cdk/issues/12563) -* **eks:** Deployment fails for the first deployment in an account ([#13103](https://github.com/aws/aws-cdk/issues/13103)) ([e042879](https://github.com/aws/aws-cdk/commit/e042879851f8ddd558d20941019c9a6692a1c2bf)), closes [/github.com/aws/aws-cdk/issues/9027#issuecomment-780482124](https://github.com/aws//github.com/aws/aws-cdk/issues/9027/issues/issuecomment-780482124) +* **eks:** Deployment fails for the first deployment in an account ([#13103](https://github.com/aws/aws-cdk/issues/13103)) ([e042879](https://github.com/aws/aws-cdk/commit/e042879851f8ddd558d20941019c9a6692a1c2bf)), closes [#9027](https://github.com/aws/aws-cdk/issues/9027) * incorrect peerDependency on "constructs" ([#13255](https://github.com/aws/aws-cdk/issues/13255)) ([17244af](https://github.com/aws/aws-cdk/commit/17244af0d181a28b908fa161250c5a3285521c53)) * **elasticloadbalancingv2:** should allow more than 2 certificates ([#13332](https://github.com/aws/aws-cdk/issues/13332)) ([d3155e9](https://github.com/aws/aws-cdk/commit/d3155e97fd9331a4732396941ce4ad20613fe81c)), closes [#13150](https://github.com/aws/aws-cdk/issues/13150) * **events:** cannot trigger multiple Lambdas from the same Rule ([#13260](https://github.com/aws/aws-cdk/issues/13260)) ([c8c1762](https://github.com/aws/aws-cdk/commit/c8c1762c213aad1062c3a0bc48b22b05c3a0a185)), closes [#13231](https://github.com/aws/aws-cdk/issues/13231) From 12f636072a07c81d7f2a6ec2236281a5da0d07e2 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Fri, 5 Mar 2021 17:40:19 -0800 Subject: [PATCH 21/28] Update Changelog for CLI ChangeSet change --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b873247ca7ca..fad4611dfad6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ All notable changes to this project will be documented in this file. See [standa * **cfnspec:** cloudformation spec v28.0.0 ([#13101](https://github.com/aws/aws-cdk/issues/13101)) ([13c9859](https://github.com/aws/aws-cdk/commit/13c9859cc62b3d472ba1be84b12d478f61f02ec9)) * **cfnspec:** cloudformation spec v29.0.0 ([#13249](https://github.com/aws/aws-cdk/issues/13249)) ([6318e26](https://github.com/aws/aws-cdk/commit/6318e2632297783bc8b5b2609bba096dd83a1113)) * **cfnspec:** cloudformation spec v30.0.0 ([#13365](https://github.com/aws/aws-cdk/issues/13365)) ([ae0185d](https://github.com/aws/aws-cdk/commit/ae0185dd089e3bb7c5639ebc1bce3f95e126f71c)) -* **cli:** Configurable --change-set-name CLI flag ([#13024](https://github.com/aws/aws-cdk/issues/13024)) ([18184df](https://github.com/aws/aws-cdk/commit/18184df05f5b8478ef9cae1285e45e61a0833822)), closes [#11075](https://github.com/aws/aws-cdk/issues/11075) [/github.com/aws/aws-cdk/pull/12683#issuecomment-778465771](https://github.com/aws//github.com/aws/aws-cdk/pull/12683/issues/issuecomment-778465771) +* **cli:** Configurable --change-set-name CLI flag ([#13024](https://github.com/aws/aws-cdk/issues/13024)) ([18184df](https://github.com/aws/aws-cdk/commit/18184df05f5b8478ef9cae1285e45e61a0833822)), closes [#11075](https://github.com/aws/aws-cdk/issues/11075) [#12683](https://github.com/aws/aws-cdk/pull/12683) * **cloudwatch:** EC2 actions ([#13281](https://github.com/aws/aws-cdk/issues/13281)) ([319cfcd](https://github.com/aws/aws-cdk/commit/319cfcdaaf92e4e6edb8c2388d04dce0971aaf86)), closes [#13228](https://github.com/aws/aws-cdk/issues/13228) * **cognito:** user pools - sign in with apple ([#13160](https://github.com/aws/aws-cdk/issues/13160)) ([b965589](https://github.com/aws/aws-cdk/commit/b965589358f4c281aea36404276f08128e6ff3db)) * **core:** `description` parameter in the CustomResourceProvider ([#13275](https://github.com/aws/aws-cdk/issues/13275)) ([78831cf](https://github.com/aws/aws-cdk/commit/78831cf9dec0407e7d827711183ac47be070f480)), closes [#13277](https://github.com/aws/aws-cdk/issues/13277) [#13276](https://github.com/aws/aws-cdk/issues/13276) From 6a5a4f2d9bb6b09ad0d10066200fe53bb45f0737 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 8 Mar 2021 12:57:13 +0100 Subject: [PATCH 22/28] feat(dynamodb): custom timeout for replication operation (#13354) Closes #10249 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-dynamodb/README.md | 11 ++++++ .../aws-dynamodb/lib/replica-provider.ts | 19 ++++++++-- packages/@aws-cdk/aws-dynamodb/lib/table.ts | 17 ++++++--- .../aws-dynamodb/test/dynamodb.test.ts | 35 ++++++++++++------- 4 files changed, 62 insertions(+), 20 deletions(-) diff --git a/packages/@aws-cdk/aws-dynamodb/README.md b/packages/@aws-cdk/aws-dynamodb/README.md index acd3f1820c156..ac540dd11670c 100644 --- a/packages/@aws-cdk/aws-dynamodb/README.md +++ b/packages/@aws-cdk/aws-dynamodb/README.md @@ -109,6 +109,17 @@ globalTable.autoScaleWriteCapacity({ }).scaleOnUtilization({ targetUtilizationPercent: 75 }); ``` +When adding a replica region for a large table, you might want to increase the +timeout for the replication operation: + +```ts +const globalTable = new dynamodb.Table(this, 'Table', { + partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }, + replicationRegions: ['us-east-1', 'us-east-2', 'us-west-2'], + replicationTimeout: Duration.hours(2), // defaults to Duration.minutes(30) +}); +``` + ## Encryption All user data stored in Amazon DynamoDB is fully encrypted at rest. When creating a new table, you can choose to encrypt using the following customer master keys (CMK) to encrypt your table: diff --git a/packages/@aws-cdk/aws-dynamodb/lib/replica-provider.ts b/packages/@aws-cdk/aws-dynamodb/lib/replica-provider.ts index 718c0e693e454..d984c4bb7b3ff 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/replica-provider.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/replica-provider.ts @@ -9,14 +9,26 @@ import { Construct } from 'constructs'; // eslint-disable-next-line no-duplicate-imports, import/order import { Construct as CoreConstruct } from '@aws-cdk/core'; +/** + * Properties for a ReplicaProvider + */ +export interface ReplicaProviderProps { + /** + * The timeout for the replication operation. + * + * @default Duration.minutes(30) + */ + readonly timeout?: Duration; +} + export class ReplicaProvider extends NestedStack { /** * Creates a stack-singleton resource provider nested stack. */ - public static getOrCreate(scope: Construct) { + public static getOrCreate(scope: Construct, props: ReplicaProviderProps = {}) { const stack = Stack.of(scope); const uid = '@aws-cdk/aws-dynamodb.ReplicaProvider'; - return stack.node.tryFindChild(uid) as ReplicaProvider || new ReplicaProvider(stack, uid); + return stack.node.tryFindChild(uid) as ReplicaProvider ?? new ReplicaProvider(stack, uid, props); } /** @@ -34,7 +46,7 @@ export class ReplicaProvider extends NestedStack { */ public readonly isCompleteHandler: lambda.Function; - private constructor(scope: Construct, id: string) { + private constructor(scope: Construct, id: string, props: ReplicaProviderProps = {}) { super(scope as CoreConstruct, id); const code = lambda.Code.fromAsset(path.join(__dirname, 'replica-handler')); @@ -80,6 +92,7 @@ export class ReplicaProvider extends NestedStack { onEventHandler: this.onEventHandler, isCompleteHandler: this.isCompleteHandler, queryInterval: Duration.seconds(10), + totalTimeout: props.timeout, }); } } diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index 8f36894fc5df8..9334192f5610d 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -3,8 +3,8 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import { - Aws, CfnCondition, CfnCustomResource, CustomResource, Fn, - IResource, Lazy, Names, RemovalPolicy, Resource, Stack, Token, + Aws, CfnCondition, CfnCustomResource, CustomResource, Duration, + Fn, IResource, Lazy, Names, RemovalPolicy, Resource, Stack, Token, } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { DynamoDBMetrics } from './dynamodb-canned-metrics.generated'; @@ -218,6 +218,13 @@ export interface TableOptions { * @experimental */ readonly replicationRegions?: string[]; + + /** + * The timeout for a table replication operation in a single region. + * + * @default Duration.minutes(30) + */ + readonly replicationTimeout?: Duration; } /** @@ -1135,7 +1142,7 @@ export class Table extends TableBase { } if (props.replicationRegions && props.replicationRegions.length > 0) { - this.createReplicaTables(props.replicationRegions); + this.createReplicaTables(props.replicationRegions, props.replicationTimeout); } } @@ -1451,14 +1458,14 @@ export class Table extends TableBase { * * @param regions regions where to create tables */ - private createReplicaTables(regions: string[]) { + private createReplicaTables(regions: string[], timeout?: Duration) { const stack = Stack.of(this); if (!Token.isUnresolved(stack.region) && regions.includes(stack.region)) { throw new Error('`replicationRegions` cannot include the region where this stack is deployed.'); } - const provider = ReplicaProvider.getOrCreate(this); + const provider = ReplicaProvider.getOrCreate(this, { timeout }); // Documentation at https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/V2gt_IAM.html // is currently incorrect. AWS Support recommends `dynamodb:*` in both source and destination regions diff --git a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts index 768094c253213..cddba2ff1504b 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts @@ -4,6 +4,7 @@ import * as appscaling from '@aws-cdk/aws-applicationautoscaling'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import { App, Aws, CfnDeletionPolicy, ConstructNode, Duration, PhysicalName, RemovalPolicy, Resource, Stack, Tags } from '@aws-cdk/core'; +import * as cr from '@aws-cdk/custom-resources'; import { testLegacyBehavior } from 'cdk-build-tools/lib/feature-flag'; import { Construct } from 'constructs'; import { @@ -20,6 +21,8 @@ import { CfnTable, } from '../lib'; +jest.mock('@aws-cdk/custom-resources'); + /* eslint-disable quote-props */ // CDK parameters @@ -2295,12 +2298,6 @@ describe('global', () => { // THEN expect(stack).toHaveResource('Custom::DynamoDBReplica', { Properties: { - ServiceToken: { - 'Fn::GetAtt': [ - 'awscdkawsdynamodbReplicaProviderNestedStackawscdkawsdynamodbReplicaProviderNestedStackResource18E3F12D', - 'Outputs.awscdkawsdynamodbReplicaProviderframeworkonEventF9504691Arn', - ], - }, TableName: { Ref: 'TableCD117FA1', }, @@ -2311,12 +2308,6 @@ describe('global', () => { expect(stack).toHaveResource('Custom::DynamoDBReplica', { Properties: { - ServiceToken: { - 'Fn::GetAtt': [ - 'awscdkawsdynamodbReplicaProviderNestedStackawscdkawsdynamodbReplicaProviderNestedStackResource18E3F12D', - 'Outputs.awscdkawsdynamodbReplicaProviderframeworkonEventF9504691Arn', - ], - }, TableName: { Ref: 'TableCD117FA1', }, @@ -2814,6 +2805,26 @@ describe('global', () => { // THEN expect(SynthUtils.toCloudFormation(stack).Conditions).toBeUndefined(); }); + + test('can configure timeout', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new Table(stack, 'Table', { + partitionKey: { + name: 'id', + type: AttributeType.STRING, + }, + replicationRegions: ['eu-central-1'], + replicationTimeout: Duration.hours(1), + }); + + // THEN + expect(cr.Provider).toHaveBeenCalledWith(expect.anything(), expect.any(String), expect.objectContaining({ + totalTimeout: Duration.hours(1), + })); + }); }); test('L1 inside L2 expects removalpolicy to have been set', () => { From 5cc3774e5fc7bcdf88a6ef80e14149a63072570f Mon Sep 17 00:00:00 2001 From: Robert Djurasaj Date: Mon, 8 Mar 2021 05:31:50 -0700 Subject: [PATCH 23/28] chore(dynamodb-global): use NODEJS_14_X in GlobalTableCoordinator (#13307) Use `NODEJS_14_X` in `GlobalTableCoordinator` since `NODEJS_10_X` is reaching maintenance EOL. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-dynamodb-global/lib/global-table-coordinator.ts | 2 +- .../test/integ.dynamodb.global.expected.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-dynamodb-global/lib/global-table-coordinator.ts b/packages/@aws-cdk/aws-dynamodb-global/lib/global-table-coordinator.ts index 0acd9b1cdc3d6..942d48116f06f 100644 --- a/packages/@aws-cdk/aws-dynamodb-global/lib/global-table-coordinator.ts +++ b/packages/@aws-cdk/aws-dynamodb-global/lib/global-table-coordinator.ts @@ -19,7 +19,7 @@ export class GlobalTableCoordinator extends cdk.Stack { code: lambda.Code.fromAsset(path.resolve(__dirname, '../', 'lambda-packages', 'aws-global-table-coordinator', 'lib')), description: 'Lambda to make DynamoDB a global table', handler: 'index.handler', - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_14_X, timeout: cdk.Duration.minutes(5), uuid: 'D38B65A6-6B54-4FB6-9BAD-9CD40A6DAC12', }); diff --git a/packages/@aws-cdk/aws-dynamodb-global/test/integ.dynamodb.global.expected.json b/packages/@aws-cdk/aws-dynamodb-global/test/integ.dynamodb.global.expected.json index 6050c1497c918..5b4f51877ccfe 100644 --- a/packages/@aws-cdk/aws-dynamodb-global/test/integ.dynamodb.global.expected.json +++ b/packages/@aws-cdk/aws-dynamodb-global/test/integ.dynamodb.global.expected.json @@ -203,7 +203,7 @@ "Arn" ] }, - "Runtime": "nodejs10.x", + "Runtime": "nodejs14.x", "Description": "Lambda to make DynamoDB a global table", "Timeout": 300 }, From 6c5b1f42fb73a132d47945b529bab73557f2b9d8 Mon Sep 17 00:00:00 2001 From: Christoph Gysin Date: Mon, 8 Mar 2021 15:06:56 +0200 Subject: [PATCH 24/28] feat(neptune): Support IAM authentication (#13462) fixes #13461 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-docdb/lib/cluster.ts | 2 +- packages/@aws-cdk/aws-neptune/README.md | 18 +++ packages/@aws-cdk/aws-neptune/lib/cluster.ts | 91 +++++++++--- .../@aws-cdk/aws-neptune/test/cluster.test.ts | 140 +++++++++++++----- 4 files changed, 196 insertions(+), 55 deletions(-) diff --git a/packages/@aws-cdk/aws-docdb/lib/cluster.ts b/packages/@aws-cdk/aws-docdb/lib/cluster.ts index 529f446c265b9..f60a332d1b77f 100644 --- a/packages/@aws-cdk/aws-docdb/lib/cluster.ts +++ b/packages/@aws-cdk/aws-docdb/lib/cluster.ts @@ -238,7 +238,7 @@ export class DatabaseCluster extends DatabaseClusterBase { public readonly clusterResourceIdentifier: string; /** - * The connections object to implement IConectable + * The connections object to implement IConnectable */ public readonly connections: ec2.Connections; diff --git a/packages/@aws-cdk/aws-neptune/README.md b/packages/@aws-cdk/aws-neptune/README.md index fc542acf1b3da..1be41b88a1022 100644 --- a/packages/@aws-cdk/aws-neptune/README.md +++ b/packages/@aws-cdk/aws-neptune/README.md @@ -58,6 +58,24 @@ attributes: const writeAddress = cluster.clusterEndpoint.socketAddress; // "HOSTNAME:PORT" ``` +## IAM Authentication + +You can also authenticate to a database cluster using AWS Identity and Access Management (IAM) database authentication; +See for more information and a list of supported +versions and limitations. + +The following example shows enabling IAM authentication for a database cluster and granting connection access to an IAM role. + +```ts +const cluster = new rds.DatabaseCluster(stack, 'Cluster', { + vpc, + instanceType: neptune.InstanceType.R5_LARGE, + iamAuthentication: true, // Optional - will be automatically set if you call grantConnect(). +}); +const role = new Role(stack, 'DBRole', { assumedBy: new AccountPrincipal(stack.account) }); +instance.grantConnect(role); // Grant the role connection access to the DB. +``` + ## Customizing parameters Neptune allows configuring database behavior by supplying custom parameter groups. For more details, refer to the diff --git a/packages/@aws-cdk/aws-neptune/lib/cluster.ts b/packages/@aws-cdk/aws-neptune/lib/cluster.ts index 4adbe2ce8ea04..316795c23a491 100644 --- a/packages/@aws-cdk/aws-neptune/lib/cluster.ts +++ b/packages/@aws-cdk/aws-neptune/lib/cluster.ts @@ -1,7 +1,7 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; -import { Duration, IResource, RemovalPolicy, Resource, Token } from '@aws-cdk/core'; +import { Aws, Duration, IResource, Lazy, RemovalPolicy, Resource, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { Endpoint } from './endpoint'; import { InstanceType } from './instance'; @@ -119,6 +119,13 @@ export interface DatabaseClusterProps { */ readonly dbClusterName?: string; + /** + * Map AWS Identity and Access Management (IAM) accounts to database accounts + * + * @default - `false` + */ + readonly iamAuthentication?: boolean; + /** * Base identifier for instances * @@ -233,6 +240,11 @@ export interface IDatabaseCluster extends IResource, ec2.IConnectable { * @attribute ReadEndpoint */ readonly clusterReadEndpoint: Endpoint; + + /** + * Grant the given identity connection access to the database. + */ + grantConnect(grantee: iam.IGrantable): iam.Grant; } /** @@ -266,23 +278,15 @@ export interface DatabaseClusterAttributes { } /** - * Create a clustered database with a given number of instances. - * - * @resource AWS::Neptune::DBCluster + * A new or imported database cluster. */ -export class DatabaseCluster extends Resource implements IDatabaseCluster { - - /** - * The default number of instances in the Neptune cluster if none are - * specified - */ - public static readonly DEFAULT_NUM_INSTANCES = 1; +export abstract class DatabaseClusterBase extends Resource implements IDatabaseCluster { /** * Import an existing DatabaseCluster from properties */ public static fromDatabaseClusterAttributes(scope: Construct, id: string, attrs: DatabaseClusterAttributes): IDatabaseCluster { - class Import extends Resource implements IDatabaseCluster { + class Import extends DatabaseClusterBase implements IDatabaseCluster { public readonly defaultPort = ec2.Port.tcp(attrs.port); public readonly connections = new ec2.Connections({ securityGroups: [attrs.securityGroup], @@ -291,6 +295,7 @@ export class DatabaseCluster extends Resource implements IDatabaseCluster { public readonly clusterIdentifier = attrs.clusterIdentifier; public readonly clusterEndpoint = new Endpoint(attrs.clusterEndpointAddress, attrs.port); public readonly clusterReadEndpoint = new Endpoint(attrs.readerEndpointAddress, attrs.port); + protected enableIamAuthentication = true; } return new Import(scope, id); @@ -299,17 +304,65 @@ export class DatabaseCluster extends Resource implements IDatabaseCluster { /** * Identifier of the cluster */ - public readonly clusterIdentifier: string; + public abstract readonly clusterIdentifier: string; /** * The endpoint to use for read/write operations */ - public readonly clusterEndpoint: Endpoint; + public abstract readonly clusterEndpoint: Endpoint; /** * Endpoint to use for load-balanced read-only operations. */ + public abstract readonly clusterReadEndpoint: Endpoint; + + /** + * The connections object to implement IConnectable + */ + public abstract readonly connections: ec2.Connections; + + protected abstract enableIamAuthentication?: boolean; + + public grantConnect(grantee: iam.IGrantable): iam.Grant { + if (this.enableIamAuthentication === false) { + throw new Error('Cannot grant connect when IAM authentication is disabled'); + } + + this.enableIamAuthentication = true; + return iam.Grant.addToPrincipal({ + grantee, + actions: ['neptune-db:*'], + resourceArns: [ + [ + 'arn', + Aws.PARTITION, + 'neptune-db', + Aws.REGION, + Aws.ACCOUNT_ID, + `${this.clusterIdentifier}/*`, + ].join(':'), + ], + }); + } +} + +/** + * Create a clustered database with a given number of instances. + * + * @resource AWS::Neptune::DBCluster + */ +export class DatabaseCluster extends DatabaseClusterBase implements IDatabaseCluster { + + /** + * The default number of instances in the Neptune cluster if none are + * specified + */ + public static readonly DEFAULT_NUM_INSTANCES = 1; + + public readonly clusterIdentifier: string; + public readonly clusterEndpoint: Endpoint; public readonly clusterReadEndpoint: Endpoint; + public readonly connections: ec2.Connections; /** * The resource id for the cluster; for example: cluster-ABCD1234EFGH5678IJKL90MNOP. The cluster ID uniquely @@ -318,11 +371,6 @@ export class DatabaseCluster extends Resource implements IDatabaseCluster { */ public readonly clusterResourceIdentifier: string; - /** - * The connections object to implement IConectable - */ - public readonly connections: ec2.Connections; - /** * The VPC where the DB subnet group is created. */ @@ -348,6 +396,8 @@ export class DatabaseCluster extends Resource implements IDatabaseCluster { */ public readonly instanceEndpoints: Endpoint[] = []; + protected enableIamAuthentication?: boolean; + constructor(scope: Construct, id: string, props: DatabaseClusterProps) { super(scope, id); @@ -385,6 +435,8 @@ export class DatabaseCluster extends Resource implements IDatabaseCluster { const deletionProtection = props.deletionProtection ?? (props.removalPolicy === RemovalPolicy.RETAIN ? true : undefined); + this.enableIamAuthentication = props.iamAuthentication; + // Create the Neptune cluster const cluster = new CfnDBCluster(this, 'Resource', { // Basic @@ -396,6 +448,7 @@ export class DatabaseCluster extends Resource implements IDatabaseCluster { dbClusterParameterGroupName: props.clusterParameterGroup?.clusterParameterGroupName, deletionProtection: deletionProtection, associatedRoles: props.associatedRoles ? props.associatedRoles.map(role => ({ roleArn: role.roleArn })) : undefined, + iamAuthEnabled: Lazy.any({ produce: () => this.enableIamAuthentication }), // Backup backupRetentionPeriod: props.backupRetention?.toDays(), preferredBackupWindow: props.preferredBackupWindow, diff --git a/packages/@aws-cdk/aws-neptune/test/cluster.test.ts b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts index d2c5ff4b6c1ef..6bb933e34dab5 100644 --- a/packages/@aws-cdk/aws-neptune/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts @@ -1,4 +1,5 @@ -import { expect as expectCDK, haveResource, ResourcePart } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import { ABSENT, ResourcePart } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; @@ -20,7 +21,7 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + expect(stack).toHaveResource('AWS::Neptune::DBCluster', { Properties: { DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId'] }], @@ -28,20 +29,20 @@ describe('DatabaseCluster', () => { }, DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition)); + }, ResourcePart.CompleteDefinition); - expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + expect(stack).toHaveResource('AWS::Neptune::DBInstance', { DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition)); + }, ResourcePart.CompleteDefinition); - expectCDK(stack).to(haveResource('AWS::Neptune::DBSubnetGroup', { + expect(stack).toHaveResource('AWS::Neptune::DBSubnetGroup', { SubnetIds: [ { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, { Ref: 'VPCPrivateSubnet3Subnet3EDCD457' }, ], - })); + }); }); test('can create a cluster with a single instance', () => { @@ -57,10 +58,10 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + expect(stack).toHaveResource('AWS::Neptune::DBCluster', { DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId'] }], - })); + }); }); test('errors when less than one instance is specified', () => { @@ -111,11 +112,11 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + expect(stack).toHaveResource('AWS::Neptune::DBCluster', { EngineVersion: '1.0.4.1', DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId'] }], - })); + }); }); test('can create a cluster with imported vpc and security group', () => { @@ -135,10 +136,10 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + expect(stack).toHaveResource('AWS::Neptune::DBCluster', { DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, VpcSecurityGroupIds: ['SecurityGroupId12345'], - })); + }); }); test('cluster with parameter group', () => { @@ -160,9 +161,9 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + expect(stack).toHaveResource('AWS::Neptune::DBCluster', { DBClusterParameterGroupName: { Ref: 'ParamsA8366201' }, - })); + }); }); test('cluster with associated role', () => { @@ -183,7 +184,7 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + expect(stack).toHaveResource('AWS::Neptune::DBCluster', { AssociatedRoles: [ { RoleArn: { @@ -194,7 +195,7 @@ describe('DatabaseCluster', () => { }, }, ], - })); + }); }); test('cluster with imported parameter group', () => { @@ -212,9 +213,9 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + expect(stack).toHaveResource('AWS::Neptune::DBCluster', { DBClusterParameterGroupName: 'ParamGroupName', - })); + }); }); test('create an encrypted cluster with custom KMS key', () => { @@ -230,7 +231,7 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + expect(stack).toHaveResource('AWS::Neptune::DBCluster', { KmsKeyId: { 'Fn::GetAtt': [ 'Key961B73FD', @@ -238,7 +239,7 @@ describe('DatabaseCluster', () => { ], }, StorageEncrypted: true, - })); + }); }); test('creating a cluster defaults to using encryption', () => { @@ -253,9 +254,9 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + expect(stack).toHaveResource('AWS::Neptune::DBCluster', { StorageEncrypted: true, - })); + }); }); test('supplying a KMS key with storageEncryption false throws an error', () => { @@ -306,9 +307,9 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + expect(stack).toHaveResource('AWS::Neptune::DBInstance', { DBInstanceIdentifier: `${instanceIdentifierBase}1`, - })); + }); }); test('cluster identifier used', () => { @@ -325,9 +326,9 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + expect(stack).toHaveResource('AWS::Neptune::DBInstance', { DBInstanceIdentifier: `${clusterIdentifier}instance1`, - })); + }); }); test('imported cluster has supplied attributes', () => { @@ -370,9 +371,9 @@ describe('DatabaseCluster', () => { cluster.connections.allowToAnyIpv4(ec2.Port.tcp(443)); // THEN - expectCDK(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { GroupId: 'sg-123456789', - })); + }); }); test('backup retention period respected', () => { @@ -388,9 +389,9 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + expect(stack).toHaveResource('AWS::Neptune::DBCluster', { BackupRetentionPeriod: 20, - })); + }); }); test('backup maintenance window respected', () => { @@ -407,10 +408,10 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + expect(stack).toHaveResource('AWS::Neptune::DBCluster', { BackupRetentionPeriod: 20, PreferredBackupWindow: '07:34-08:04', - })); + }); }); test('regular maintenance window respected', () => { @@ -426,9 +427,78 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + expect(stack).toHaveResource('AWS::Neptune::DBCluster', { PreferredMaintenanceWindow: '07:34-08:04', - })); + }); + }); + + test('iam authentication - off by default', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Cluster', { + vpc, + instanceType: InstanceType.R5_LARGE, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Neptune::DBCluster', { + IamAuthEnabled: ABSENT, + }); + }); + + test('createGrant - creates IAM policy and enables IAM auth', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const cluster = new DatabaseCluster(stack, 'Cluster', { + vpc, + instanceType: InstanceType.R5_LARGE, + }); + const role = new iam.Role(stack, 'DBRole', { + assumedBy: new iam.AccountPrincipal(stack.account), + }); + cluster.grantConnect(role); + + // THEN + expect(stack).toHaveResourceLike('AWS::Neptune::DBCluster', { + IamAuthEnabled: true, + }); + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [{ + Effect: 'Allow', + Action: 'neptune-db:*', + Resource: { + 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':neptune-db:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'ClusterEB0386A7' }, '/*']], + }, + }], + Version: '2012-10-17', + }, + }); + }); + + test('createGrant - throws if IAM auth disabled', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const cluster = new DatabaseCluster(stack, 'Cluster', { + vpc, + instanceType: InstanceType.R5_LARGE, + iamAuthentication: false, + }); + const role = new iam.Role(stack, 'DBRole', { + assumedBy: new iam.AccountPrincipal(stack.account), + }); + + // THEN + expect(() => { cluster.grantConnect(role); }).toThrow(/Cannot grant connect when IAM authentication is disabled/); }); }); From 0ebcb4160ee16f0f7ff1072a40c8951f9a983048 Mon Sep 17 00:00:00 2001 From: Alban Esc Date: Mon, 8 Mar 2021 05:40:24 -0800 Subject: [PATCH 25/28] feat(events): dead-letter queue support for StepFunctions (#13450) Resolves #13449 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-events-targets/README.md | 36 +++++++ .../aws-events-targets/lib/state-machine.ts | 20 +++- .../test/stepfunctions/statemachine.test.ts | 93 +++++++++++++++++++ 3 files changed, 148 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-events-targets/README.md b/packages/@aws-cdk/aws-events-targets/README.md index cd843ef130a3a..4b795ce5f19b5 100644 --- a/packages/@aws-cdk/aws-events-targets/README.md +++ b/packages/@aws-cdk/aws-events-targets/README.md @@ -89,3 +89,39 @@ const rule = new events.Rule(this, 'rule', { rule.addTarget(new targets.CloudWatchLogGroup(logGroup)); ``` + +## Trigger a State Machine + +Use the `SfnStateMachine` target to trigger a State Machine. + +The code snippet below creates a Simple StateMachine that is triggered every minute with a +dummy object as input. +You can optionally attach a +[dead letter queue](https://docs.aws.amazon.com/eventbridge/latest/userguide/rule-dlq.html) +to the target. + +```ts +import * as iam from '@aws-sdk/aws-iam'; +import * as sqs from '@aws-sdk/aws-sqs'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as targets from "@aws-cdk/aws-events-targets"; + +const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), +}); + +const dlq = new sqs.Queue(stack, 'DeadLetterQueue'); + +const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('events.amazonaws.com'), +}); +const stateMachine = new sfn.StateMachine(stack, 'SM', { + definition: new sfn.Wait(stack, 'Hello', { time: sfn.WaitTime.duration(cdk.Duration.seconds(10)) }), + role, +}); + +rule.addTarget(new targets.SfnStateMachine(stateMachine, { + input: events.RuleTargetInput.fromObject({ SomeParam: 'SomeValue' }), + deadLetterQueue: dlq, +})); +``` diff --git a/packages/@aws-cdk/aws-events-targets/lib/state-machine.ts b/packages/@aws-cdk/aws-events-targets/lib/state-machine.ts index acffea0eea6f4..a2a791715e94c 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/state-machine.ts @@ -1,7 +1,8 @@ import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; +import * as sqs from '@aws-cdk/aws-sqs'; import * as sfn from '@aws-cdk/aws-stepfunctions'; -import { singletonEventRole } from './util'; +import { addToDeadLetterQueueResourcePolicy, singletonEventRole } from './util'; /** * Customize the Step Functions State Machine target @@ -20,6 +21,18 @@ export interface SfnStateMachineProps { * @default - a new role will be created */ readonly role?: iam.IRole; + + /** + * The SQS queue to be used as deadLetterQueue. + * Check out the [considerations for using a dead-letter queue](https://docs.aws.amazon.com/eventbridge/latest/userguide/rule-dlq.html#dlq-considerations). + * + * The events not successfully delivered are automatically retried for a specified period of time, + * depending on the retry policy of the target. + * If an event is not delivered before all retry attempts are exhausted, it will be sent to the dead letter queue. + * + * @default - no dead-letter queue + */ + readonly deadLetterQueue?: sqs.IQueue; } /** @@ -43,9 +56,14 @@ export class SfnStateMachine implements events.IRuleTarget { * @see https://docs.aws.amazon.com/eventbridge/latest/userguide/resource-based-policies-eventbridge.html#sns-permissions */ public bind(_rule: events.IRule, _id?: string): events.RuleTargetConfig { + if (this.props.deadLetterQueue) { + addToDeadLetterQueueResourcePolicy(_rule, this.props.deadLetterQueue); + } + return { id: '', arn: this.machine.stateMachineArn, + deadLetterConfig: this.props.deadLetterQueue ? { arn: this.props.deadLetterQueue?.queueArn } : undefined, role: this.role, input: this.props.input, targetResource: this.machine, diff --git a/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts b/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts index 3b50ef569f004..9dc3b2d0a4e83 100644 --- a/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts @@ -1,6 +1,7 @@ import '@aws-cdk/assert/jest'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; +import * as sqs from '@aws-cdk/aws-sqs'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; import * as targets from '../../lib'; @@ -110,3 +111,95 @@ test('Existing role can be used for State machine Rule target', () => { }, }); }); + +test('use a Dead Letter Queue for the rule target', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), + }); + + const dlq = new sqs.Queue(stack, 'DeadLetterQueue'); + + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('events.amazonaws.com'), + }); + const stateMachine = new sfn.StateMachine(stack, 'SM', { + definition: new sfn.Wait(stack, 'Hello', { time: sfn.WaitTime.duration(cdk.Duration.seconds(10)) }), + role, + }); + + // WHEN + rule.addTarget(new targets.SfnStateMachine(stateMachine, { + input: events.RuleTargetInput.fromObject({ SomeParam: 'SomeValue' }), + deadLetterQueue: dlq, + })); + + // the Permission resource should be in the event stack + expect(stack).toHaveResource('AWS::Events::Rule', { + ScheduleExpression: 'rate(1 minute)', + State: 'ENABLED', + Targets: [ + { + Arn: { + Ref: 'SM934E715A', + }, + DeadLetterConfig: { + Arn: { + 'Fn::GetAtt': [ + 'DeadLetterQueue9F481546', + 'Arn', + ], + }, + }, + Id: 'Target0', + Input: '{"SomeParam":"SomeValue"}', + RoleArn: { + 'Fn::GetAtt': [ + 'SMEventsRoleB320A902', + 'Arn', + ], + }, + }, + ], + }); + + expect(stack).toHaveResource('AWS::SQS::QueuePolicy', { + PolicyDocument: { + Statement: [ + { + Action: 'sqs:SendMessage', + Condition: { + ArnEquals: { + 'aws:SourceArn': { + 'Fn::GetAtt': [ + 'Rule4C995B7F', + 'Arn', + ], + }, + }, + }, + Effect: 'Allow', + Principal: { + Service: 'events.amazonaws.com', + }, + Resource: { + 'Fn::GetAtt': [ + 'DeadLetterQueue9F481546', + 'Arn', + ], + }, + Sid: 'AllowEventRuleStackRuleF6E31DD0', + }, + ], + Version: '2012-10-17', + }, + Queues: [ + { + Ref: 'DeadLetterQueue9F481546', + }, + ], + }); +}); From 56c2029563994bb45a235e177035d35660db915f Mon Sep 17 00:00:00 2001 From: Tom Jenkinson Date: Mon, 8 Mar 2021 14:14:35 +0000 Subject: [PATCH 26/28] docs(pipelines): fix code examples (#13401) Shouldn't these take the `App` instance? --- packages/@aws-cdk/pipelines/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index 512ee5e97c92b..15e6da27613d3 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -173,7 +173,7 @@ class MyPipelineStack extends Stack { } const app = new App(); -new MyPipelineStack(this, 'PipelineStack', { +new MyPipelineStack(app, 'PipelineStack', { env: { account: '111111111111', region: 'eu-west-1', @@ -198,7 +198,8 @@ const codePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline', { ], }); -const cdkPipeline = new CdkPipeline(this, 'CdkPipeline', { +const app = new App(); +const cdkPipeline = new CdkPipeline(app, 'CdkPipeline', { codePipeline, cloudAssemblyArtifact, }); From b3fba43a047df61e713e8d2271d6deee7e07b716 Mon Sep 17 00:00:00 2001 From: Jaynti Raj Date: Mon, 8 Mar 2021 06:51:06 -0800 Subject: [PATCH 27/28] feat(region-info): added AppMesh ECR account for af-south-1 region (#12814) AppMesh is launched in `af-south-1` region and has a separate account for storing Envoy images in ECR. Added the ECR account information. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .gitallowed | 1 + .../ecs-service-extensions/lib/extensions/appmesh.ts | 1 + .../test/integ.all-service-addons.expected.json | 9 +++++++++ .../test/integ.multiple-environments.expected.json | 6 ++++++ packages/@aws-cdk/region-info/build-tools/fact-tables.ts | 1 + 5 files changed, 18 insertions(+) diff --git a/.gitallowed b/.gitallowed index 972728e4b4a7e..2fa8726e1171d 100644 --- a/.gitallowed +++ b/.gitallowed @@ -22,3 +22,4 @@ account: '772975370895' account: '856666278305' account: '840364872350' account: '422531588944' +account: '924023996002' 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 35436d11ce691..7749683fb4235 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 @@ -165,6 +165,7 @@ export class AppMeshExtension extends ServiceExtension { 'me-south-1': this.accountIdForRegion('me-south-1'), 'ap-east-1': this.accountIdForRegion('ap-east-1'), + 'af-south-1': this.accountIdForRegion('af-south-1'), }, }); diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.all-service-addons.expected.json b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.all-service-addons.expected.json index 7ef708ccc31aa..a6e83c8b6ad66 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.all-service-addons.expected.json +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.all-service-addons.expected.json @@ -3354,6 +3354,9 @@ }, "ap-east-1": { "ecrRepo": "856666278305" + }, + "af-south-1": { + "ecrRepo": "924023996002" } }, "greetingenvoyimageaccountmapping": { @@ -3413,6 +3416,9 @@ }, "ap-east-1": { "ecrRepo": "856666278305" + }, + "af-south-1": { + "ecrRepo": "924023996002" } }, "greeterenvoyimageaccountmapping": { @@ -3472,6 +3478,9 @@ }, "ap-east-1": { "ecrRepo": "856666278305" + }, + "af-south-1": { + "ecrRepo": "924023996002" } } }, diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.multiple-environments.expected.json b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.multiple-environments.expected.json index 2aab9da2612fa..85c6a59e919de 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.multiple-environments.expected.json +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.multiple-environments.expected.json @@ -2173,6 +2173,9 @@ }, "ap-east-1": { "ecrRepo": "856666278305" + }, + "af-south-1": { + "ecrRepo": "924023996002" } }, "namedevelopmentenvoyimageaccountmapping": { @@ -2232,6 +2235,9 @@ }, "ap-east-1": { "ecrRepo": "856666278305" + }, + "af-south-1": { + "ecrRepo": "924023996002" } } } diff --git a/packages/@aws-cdk/region-info/build-tools/fact-tables.ts b/packages/@aws-cdk/region-info/build-tools/fact-tables.ts index 528cd8463f05e..dc7f8ff586449 100644 --- a/packages/@aws-cdk/region-info/build-tools/fact-tables.ts +++ b/packages/@aws-cdk/region-info/build-tools/fact-tables.ts @@ -158,5 +158,6 @@ export const APPMESH_ECR_ACCOUNTS: { [region: string]: string } = { 'me-south-1': '772975370895', 'ap-east-1': '856666278305', + 'af-south-1': '924023996002', }; From 90dbfb5eec19559717ac6b30f25451461027e731 Mon Sep 17 00:00:00 2001 From: Chris Reiche <35380634+creiche@users.noreply.github.com> Date: Mon, 8 Mar 2021 10:25:57 -0500 Subject: [PATCH 28/28] fix(iam): oidc-provider can't pull from hosts requiring SNI (#13397) This enables SNI when the oidcProvider tries to pull the thumbprint from a server in the downloadThumbprint function. This fixes issues when trying to add an oidcProvider that is using SNI. ---- *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/oidc-provider/external.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-iam/lib/oidc-provider/external.ts b/packages/@aws-cdk/aws-iam/lib/oidc-provider/external.ts index d926caf405e22..4ad18aed4f17d 100644 --- a/packages/@aws-cdk/aws-iam/lib/oidc-provider/external.ts +++ b/packages/@aws-cdk/aws-iam/lib/oidc-provider/external.ts @@ -28,7 +28,7 @@ async function downloadThumbprint(issuerUrl: string) { if (!purl.host) { return ko(new Error(`unable to determine host from issuer url ${issuerUrl}`)); } - const socket = tls.connect(port, purl.host, { rejectUnauthorized: false }); + const socket = tls.connect(port, purl.host, { rejectUnauthorized: false, servername: purl.host }); socket.once('error', ko); socket.once('secureConnect', () => { const cert = socket.getPeerCertificate();