From f68c30227d6dc52ea8f024324e869408cd9626d0 Mon Sep 17 00:00:00 2001 From: AWS CDK Team Date: Thu, 3 Sep 2020 15:56:10 +0000 Subject: [PATCH 1/8] chore(release): 1.62.0 --- CHANGELOG.md | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++ lerna.json | 2 +- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df38cbd61a42e..15e64588c88d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,67 @@ 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.62.0](https://github.com/aws/aws-cdk/compare/v1.61.1...v1.62.0) (2020-09-03) + + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **eks:** when importing EKS clusters using `eks.Cluster.fromClusterAttributes`, the `clusterArn` attribute is not supported anymore, and will always be derived from `clusterName`. +* **eks**: Only a single `eks.Cluster` is allowed per CloudFormation stack. +* **eks**: The `securityGroups` attribute of `ClusterAttributes` is now `securityGroupIds`. + +### Features + +* **appsync:** support Input Types for code-first approach ([#10024](https://github.com/aws/aws-cdk/issues/10024)) ([3f80ae6](https://github.com/aws/aws-cdk/commit/3f80ae6c7886c1bac1cefa5f613962e17a34cc54)) +* **appsync:** support query & mutation generation for code-first approach ([#9992](https://github.com/aws/aws-cdk/issues/9992)) ([1ed119e](https://github.com/aws/aws-cdk/commit/1ed119e2cdbc37666616f6666b0edb12c2c9ea89)), closes [#9308](https://github.com/aws/aws-cdk/issues/9308) [#9310](https://github.com/aws/aws-cdk/issues/9310) +* **aws-chatbot:** Support L2 construct for SlackChannelConfiguration of chatbot. ([#9702](https://github.com/aws/aws-cdk/issues/9702)) ([05f5e62](https://github.com/aws/aws-cdk/commit/05f5e621d82bc4c32fba954820276e8c40381d9b)), closes [#9679](https://github.com/aws/aws-cdk/issues/9679) +* **bootstrap:** customizable bootstrap template ([#9886](https://github.com/aws/aws-cdk/issues/9886)) ([2596ef7](https://github.com/aws/aws-cdk/commit/2596ef7a99c8eeba79609d60144842f5d33fdf9b)), closes [#9256](https://github.com/aws/aws-cdk/issues/9256) [#8724](https://github.com/aws/aws-cdk/issues/8724) [#3684](https://github.com/aws/aws-cdk/issues/3684) [#1528](https://github.com/aws/aws-cdk/issues/1528) [#9681](https://github.com/aws/aws-cdk/issues/9681) +* **cli:** control progress output style with --progress=bar|events ([#9623](https://github.com/aws/aws-cdk/issues/9623)) ([56de5e1](https://github.com/aws/aws-cdk/commit/56de5e15e52768a5c63c02e7101b95a95f7cbc94)), closes [#8696](https://github.com/aws/aws-cdk/issues/8696) +* **cloudfront:** import existing CloudFrontWebDistributions ([#10007](https://github.com/aws/aws-cdk/issues/10007)) ([ff33b54](https://github.com/aws/aws-cdk/commit/ff33b5416116fd23cf160078bf53651096bde284)), closes [#5607](https://github.com/aws/aws-cdk/issues/5607) +* **cloudfront:** support includeBody for Lambda@Edge ([#10008](https://github.com/aws/aws-cdk/issues/10008)) ([9ffb268](https://github.com/aws/aws-cdk/commit/9ffb2682c167fe92e302bc322d60b9ae37de934a)), closes [#7085](https://github.com/aws/aws-cdk/issues/7085) +* **ecs:** bottlerocket support ([#10097](https://github.com/aws/aws-cdk/issues/10097)) ([088abec](https://github.com/aws/aws-cdk/commit/088abec6513d8ae665a3a10bee5c5b5fe61a48b9)), closes [#10085](https://github.com/aws/aws-cdk/issues/10085) +* **eks:** kubectl layer customization ([#10090](https://github.com/aws/aws-cdk/issues/10090)) ([0aa7ada](https://github.com/aws/aws-cdk/commit/0aa7adac958fb7997b64eba8c7fc3008e8557480)), closes [#7992](https://github.com/aws/aws-cdk/issues/7992) +* **eks:** support adding k8s resources to imported clusters ([#9802](https://github.com/aws/aws-cdk/issues/9802)) ([4439481](https://github.com/aws/aws-cdk/commit/443948164e09aaa81c094c013b32aa1f67b69570)), closes [#5383](https://github.com/aws/aws-cdk/issues/5383) +* **logs:** specify log group's region for LogRetention ([#9804](https://github.com/aws/aws-cdk/issues/9804)) ([0ccbc5d](https://github.com/aws/aws-cdk/commit/0ccbc5dfe5c841ec821ac98ac219e98984237cba)) +* **pipelines:** `SimpleSynthAction` takes array of build commands ([#10152](https://github.com/aws/aws-cdk/issues/10152)) ([44fcb4e](https://github.com/aws/aws-cdk/commit/44fcb4e65219b48aa9e186d1d6c10ca632e9658d)), closes [#9357](https://github.com/aws/aws-cdk/issues/9357) +* **pipelines:** add control over underlying CodePipeline ([#10148](https://github.com/aws/aws-cdk/issues/10148)) ([41531b5](https://github.com/aws/aws-cdk/commit/41531b57ae1b19087399018b063da45356bf07bb)), closes [#9021](https://github.com/aws/aws-cdk/issues/9021) +* **rds:** add support for joining instance to domain ([#9943](https://github.com/aws/aws-cdk/issues/9943)) ([f2d77d1](https://github.com/aws/aws-cdk/commit/f2d77d16d62e80d23c200ea94e4181660d953ca2)), closes [#9869](https://github.com/aws/aws-cdk/issues/9869) +* **rds:** custom security groups for OptionGroups ([ea1072d](https://github.com/aws/aws-cdk/commit/ea1072d3baa50d8a722795557765360286195b79)), closes [#9240](https://github.com/aws/aws-cdk/issues/9240) +* **rds:** custom security groups for OptionGroups ([#10011](https://github.com/aws/aws-cdk/issues/10011)) ([5738dc1](https://github.com/aws/aws-cdk/commit/5738dc17025355e3f94edc4af242253ebb3409f6)), closes [#9240](https://github.com/aws/aws-cdk/issues/9240) +* **rds:** performance insights for DatabaseCluster instances ([#10092](https://github.com/aws/aws-cdk/issues/10092)) ([9c1b0c1](https://github.com/aws/aws-cdk/commit/9c1b0c1b27ba4680a1e15cbd6a30a8f10dfe6313)), closes [#7957](https://github.com/aws/aws-cdk/issues/7957) +* **rds:** rename DatabaseInstanceNewProps.vpcPlacement to vpcSubnets ([#10093](https://github.com/aws/aws-cdk/issues/10093)) ([ec423ef](https://github.com/aws/aws-cdk/commit/ec423eff18809173a01d0c15e02ed4f042061310)), closes [#9776](https://github.com/aws/aws-cdk/issues/9776) +* convenience method for ALB redirects ([#9913](https://github.com/aws/aws-cdk/issues/9913)) ([5bed08a](https://github.com/aws/aws-cdk/commit/5bed08a30880652a5113245bd455228bd8bf32a2)) + + +### Bug Fixes + +* **apigateway:** burst and rate limits are set to unlimited when configured to 0 ([#10088](https://github.com/aws/aws-cdk/issues/10088)) ([96f1772](https://github.com/aws/aws-cdk/commit/96f1772ab861015f24703a1315538d37ae9529ad)), closes [#10071](https://github.com/aws/aws-cdk/issues/10071) +* **appsync:** `GraphQLApi.UserPoolConfig` requires `DefaultAction` ([#10031](https://github.com/aws/aws-cdk/issues/10031)) ([6114045](https://github.com/aws/aws-cdk/commit/6114045a4861efc7364f94490b734df5cf019726)), closes [#10028](https://github.com/aws/aws-cdk/issues/10028) +* **aws-elasticloadbalancingv2:** fix load balancer deletion protection to properly update when set to false ([#9986](https://github.com/aws/aws-cdk/issues/9986)) ([a65dd19](https://github.com/aws/aws-cdk/commit/a65dd190b0856db7880177910d4096a799791ee1)) +* **aws-sns:** enable topic encryption with cross account keys ([#10056](https://github.com/aws/aws-cdk/issues/10056)) ([327b72a](https://github.com/aws/aws-cdk/commit/327b72a0f4778318a937a069a5169c2174179dc0)), closes [#10055](https://github.com/aws/aws-cdk/issues/10055) +* **aws-stepfunctions-tasks:** missing permission to get build status ([#10081](https://github.com/aws/aws-cdk/issues/10081)) ([cbdd084](https://github.com/aws/aws-cdk/commit/cbdd084d7b3eb92a311da48c279b5423e1ae22a2)), closes [#8043](https://github.com/aws/aws-cdk/issues/8043) +* **aws-stepfunctions-tasks:** SageMaker create training job has incorrect property name for AttributeNames ([#10026](https://github.com/aws/aws-cdk/issues/10026)) ([ba51ea3](https://github.com/aws/aws-cdk/commit/ba51ea34e5b3f3c3cf337754d339f724b395211e)), closes [#10014](https://github.com/aws/aws-cdk/issues/10014) +* **cfn-include:** allow Conditions to reference Mappings in their definitions ([#10105](https://github.com/aws/aws-cdk/issues/10105)) ([aa2068f](https://github.com/aws/aws-cdk/commit/aa2068f0d560de5737bd0a3df8089f8af2128e09)), closes [#10099](https://github.com/aws/aws-cdk/issues/10099) +* **cfn-include:** allow parameters to be replaced across nested stacks ([#9842](https://github.com/aws/aws-cdk/issues/9842)) ([9ea8d5c](https://github.com/aws/aws-cdk/commit/9ea8d5c2d638bdf1f5bc63be197ecefc775d6539)), closes [#9838](https://github.com/aws/aws-cdk/issues/9838) +* **cli:** AssumeRole profiles require a [default] profile ([#10032](https://github.com/aws/aws-cdk/issues/10032)) ([95c0332](https://github.com/aws/aws-cdk/commit/95c0332395d1203e8b00fda153fe08e70d0387c5)), closes [#9937](https://github.com/aws/aws-cdk/issues/9937) +* **cli:** bootstrapping qualifier length not validated ([#10121](https://github.com/aws/aws-cdk/issues/10121)) ([e069263](https://github.com/aws/aws-cdk/commit/e0692636571eec76068e4cec0a87f13fc292fea0)), closes [#9255](https://github.com/aws/aws-cdk/issues/9255) +* **cli:** Linux browser not supported for `cdk docs` ([#9549](https://github.com/aws/aws-cdk/issues/9549)) ([663913f](https://github.com/aws/aws-cdk/commit/663913f061f0fa3e2bed11b8cea763b12a3061f2)), closes [#2847](https://github.com/aws/aws-cdk/issues/2847) +* **cli:** re-bootstrapping loses previous configuration ([#10120](https://github.com/aws/aws-cdk/issues/10120)) ([4e5829a](https://github.com/aws/aws-cdk/commit/4e5829ac5bb55533435772c3f2f294394ab2c973)), closes [#10091](https://github.com/aws/aws-cdk/issues/10091) +* **cli:** unable to upgrade new style bootstrap to version ([#10030](https://github.com/aws/aws-cdk/issues/10030)) ([c5bb55c](https://github.com/aws/aws-cdk/commit/c5bb55c37c03597139522e0bb42f094c1f6b647e)), closes [#10016](https://github.com/aws/aws-cdk/issues/10016) +* **cloudfront:** Distribution does not add edgelambda trust policy ([#10006](https://github.com/aws/aws-cdk/issues/10006)) ([9098e29](https://github.com/aws/aws-cdk/commit/9098e295826c09ef568bb8fc03c217ce8a15b822)), closes [#9998](https://github.com/aws/aws-cdk/issues/9998) +* **custom-resources:** buffers returned by AwsCustomResource are unusable ([#9977](https://github.com/aws/aws-cdk/issues/9977)) ([7f351ff](https://github.com/aws/aws-cdk/commit/7f351ffeee30e1a2451e9b456c0d0a21002397da)), closes [#9969](https://github.com/aws/aws-cdk/issues/9969) [#10017](https://github.com/aws/aws-cdk/issues/10017) +* **eks:** creating a `ServiceAccount` in a different stack than the `Cluster` creates circular dependency between the two stacks ([#9701](https://github.com/aws/aws-cdk/issues/9701)) ([1e96ebc](https://github.com/aws/aws-cdk/commit/1e96ebc29e1db251a1dc1e046c302943e7556c9a)), closes [40aws-cdk/aws-eks/lib/service-account.ts#L81-L95](https://github.com/40aws-cdk/aws-eks/lib/service-account.ts/issues/L81-L95) [40aws-cdk/aws-eks/lib/cluster.ts#L914-L923](https://github.com/40aws-cdk/aws-eks/lib/cluster.ts/issues/L914-L923) [40aws-cdk/aws-eks/lib/cluster.ts#L907-L909](https://github.com/40aws-cdk/aws-eks/lib/cluster.ts/issues/L907-L909) +* **eks:** README.md grammar ([#10072](https://github.com/aws/aws-cdk/issues/10072)) ([454cdc6](https://github.com/aws/aws-cdk/commit/454cdc6106bee1ec23e8e9f390c03ebf6fcf2957)) +* **elbv2:** add protocol to AddNetworkTargetsProps ([#10054](https://github.com/aws/aws-cdk/issues/10054)) ([c7c00e7](https://github.com/aws/aws-cdk/commit/c7c00e73e5e9be5b5fa65394f80eb5fb47fe4153)), closes [aws/aws-cdk#10044](https://github.com/aws/aws-cdk/issues/10044) +* **elbv2:** consider default protocol when validating redirectHTTP ([#10100](https://github.com/aws/aws-cdk/issues/10100)) ([9e4c6d2](https://github.com/aws/aws-cdk/commit/9e4c6d22890125328d26923e44c4885ae7daecbf)) +* **glue:** tables not including classification ([#9923](https://github.com/aws/aws-cdk/issues/9923)) ([61b45f3](https://github.com/aws/aws-cdk/commit/61b45f30f8aefef8e8989b597d4cf32ea731f324)), closes [#9902](https://github.com/aws/aws-cdk/issues/9902) +* **lamba:** Add Java 8 Corretto Runtime support ([77f9703](https://github.com/aws/aws-cdk/commit/77f97039221981aea980b583e56ac88ed854a8e4)) +* **lambda:** grantInvoke fails for imported IAM identities ([#9957](https://github.com/aws/aws-cdk/issues/9957)) ([d748f44](https://github.com/aws/aws-cdk/commit/d748f4400e28fcb0933df6c57df36740381deff3)), closes [#9883](https://github.com/aws/aws-cdk/issues/9883) +* **lambda-nodejs:** cannot stat error with jsx/tsx handler ([#9958](https://github.com/aws/aws-cdk/issues/9958)) ([25cfc18](https://github.com/aws/aws-cdk/commit/25cfc18f116e9ae3861de52af9f81fcec8454ae2)) +* **lambda-python:** allowPublicSubnet and filesystem not supported ([#10022](https://github.com/aws/aws-cdk/issues/10022)) ([745922a](https://github.com/aws/aws-cdk/commit/745922aa5a5a0195869830b54d7e529bec83e37c)), closes [#10018](https://github.com/aws/aws-cdk/issues/10018) [#10027](https://github.com/aws/aws-cdk/issues/10027) +* **redshift:** single-node clusters fail with node count error ([#9961](https://github.com/aws/aws-cdk/issues/9961)) ([2cd3ea2](https://github.com/aws/aws-cdk/commit/2cd3ea21a92e624c3d07f7f4cb46391ef33e756e)), closes [#9856](https://github.com/aws/aws-cdk/issues/9856) +* **route53:** value is too long error for TXT records ([#9984](https://github.com/aws/aws-cdk/issues/9984)) ([fd4be21](https://github.com/aws/aws-cdk/commit/fd4be21112ecd0cf3fd7ddfe005968507a5d18d4)), closes [#8244](https://github.com/aws/aws-cdk/issues/8244) + ## [1.61.1](https://github.com/aws/aws-cdk/compare/v1.61.0...v1.61.1) (2020-08-28) diff --git a/lerna.json b/lerna.json index 87746feb847f4..55e11b15eb0cf 100644 --- a/lerna.json +++ b/lerna.json @@ -10,5 +10,5 @@ "tools/*" ], "rejectCycles": "true", - "version": "1.61.1" + "version": "1.62.0" } From 60bc7470bfb12be1931b2cea43bb53c4bddc7382 Mon Sep 17 00:00:00 2001 From: Eli Polonsky Date: Thu, 3 Sep 2020 19:12:44 +0300 Subject: [PATCH 2/8] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15e64588c88d8..77f99f927709b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,7 @@ All notable changes to this project will be documented in this file. See [standa * **rds:** custom security groups for OptionGroups ([#10011](https://github.com/aws/aws-cdk/issues/10011)) ([5738dc1](https://github.com/aws/aws-cdk/commit/5738dc17025355e3f94edc4af242253ebb3409f6)), closes [#9240](https://github.com/aws/aws-cdk/issues/9240) * **rds:** performance insights for DatabaseCluster instances ([#10092](https://github.com/aws/aws-cdk/issues/10092)) ([9c1b0c1](https://github.com/aws/aws-cdk/commit/9c1b0c1b27ba4680a1e15cbd6a30a8f10dfe6313)), closes [#7957](https://github.com/aws/aws-cdk/issues/7957) * **rds:** rename DatabaseInstanceNewProps.vpcPlacement to vpcSubnets ([#10093](https://github.com/aws/aws-cdk/issues/10093)) ([ec423ef](https://github.com/aws/aws-cdk/commit/ec423eff18809173a01d0c15e02ed4f042061310)), closes [#9776](https://github.com/aws/aws-cdk/issues/9776) -* convenience method for ALB redirects ([#9913](https://github.com/aws/aws-cdk/issues/9913)) ([5bed08a](https://github.com/aws/aws-cdk/commit/5bed08a30880652a5113245bd455228bd8bf32a2)) +* **elasticloadbalancingv2:** convenience method for ALB redirects ([#9913](https://github.com/aws/aws-cdk/issues/9913)) ([5bed08a](https://github.com/aws/aws-cdk/commit/5bed08a30880652a5113245bd455228bd8bf32a2)) ### Bug Fixes From c6d746ea2317206261a202b44cffa90b25d0899a Mon Sep 17 00:00:00 2001 From: Bryan Pan Date: Thu, 3 Sep 2020 22:17:14 -0700 Subject: [PATCH 3/8] build(appsync): fix jsii bug to prevent toString() override w/ dotnet (#10167) jsii bug that's preventing packaging
bug output ```shell #STDOUT> Amazon/CDK/AWS/AppSync/Directive.cs(88,32): error CS0115: 'Directive.ToString(AuthorizationType[]?)': no suitable method found to override [/tmp/npm-packqZC1CO/Amazon.CDK.MonoCDK.Experiment/Amazon.CDK.MonoCDK.Experiment.csproj] #STDOUT> Amazon/CDK/AWS/AppSync/InputType.cs(66,32): error CS0115: 'InputType.ToString(AuthorizationType[]?)': no suitable method found to override [/tmp/npm-packqZC1CO/Amazon.CDK.MonoCDK.Experiment/Amazon.CDK.MonoCDK.Experiment.csproj] #STDOUT> Amazon/CDK/AWS/AppSync/InterfaceType.cs(64,32): error CS0115: 'InterfaceType.ToString(AuthorizationType[]?)': no suitable method found to override [/tmp/npm-packqZC1CO/Amazon.CDK.MonoCDK.Experiment/Amazon.CDK.MonoCDK.Experiment.csproj] #STDOUT> Amazon/CDK/AWS/AppSync/InputType.cs(66,32): error CS0115: 'InputType.ToString(AuthorizationType[]?)': no suitable method found to override [/tmp/npm-packqZC1CO/Amazon.CDK.AWS.AppSync/Amazon.CDK.AWS.AppSync.csproj] #STDOUT> Amazon/CDK/AWS/AppSync/Directive.cs(88,32): error CS0115: 'Directive.ToString(AuthorizationType[]?)': no suitable method found to override [/tmp/npm-packqZC1CO/Amazon.CDK.AWS.AppSync/Amazon.CDK.AWS.AppSync.csproj] #STDOUT> Amazon/CDK/AWS/AppSync/InterfaceType.cs(64,32): error CS0115: 'InterfaceType.ToString(AuthorizationType[]?)': no suitable method found to override [/tmp/npm-packqZC1CO/Amazon.CDK.AWS.AppSync/Amazon.CDK.AWS.AppSync.csproj] ```
**Cause** C# `Object.toString()` does not have parameters in its function. When IIntermediateType functions inherited the `Object.toString()` function, it would override the `toString()` function with parameters causing an error. **Fix** Create `bind` methods called `bindToAuthModes` and `bindToGraphqlApi` to alleviate the need for parameterized `toString()` function. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-appsync/lib/private.ts | 2 +- .../@aws-cdk/aws-appsync/lib/schema-base.ts | 45 +++++++++++++----- .../@aws-cdk/aws-appsync/lib/schema-field.ts | 2 +- .../aws-appsync/lib/schema-intermediate.ts | 46 +++++++++++++++---- packages/@aws-cdk/aws-appsync/lib/schema.ts | 2 +- 5 files changed, 74 insertions(+), 23 deletions(-) diff --git a/packages/@aws-cdk/aws-appsync/lib/private.ts b/packages/@aws-cdk/aws-appsync/lib/private.ts index 981076113be6e..f31ab439bccc8 100644 --- a/packages/@aws-cdk/aws-appsync/lib/private.ts +++ b/packages/@aws-cdk/aws-appsync/lib/private.ts @@ -233,5 +233,5 @@ function generateDirectives(options: generateDirectivesOptions): string { // reduce over all directives and get string version of the directive // pass in the auth modes for checks to happen on compile time return options.directives.reduce((acc, directive) => - `${acc}${directive.toString(options.modes)}${options.delimiter ?? ' '}`, ' ').slice(0, -1); + `${acc}${directive._bindToAuthModes(options.modes).toString()}${options.delimiter ?? ' '}`, ' ').slice(0, -1); } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/lib/schema-base.ts b/packages/@aws-cdk/aws-appsync/lib/schema-base.ts index 628cbf80af8c3..96e537a17d815 100644 --- a/packages/@aws-cdk/aws-appsync/lib/schema-base.ts +++ b/packages/@aws-cdk/aws-appsync/lib/schema-base.ts @@ -1,4 +1,4 @@ -import { AuthorizationType } from './graphqlapi'; +import { AuthorizationType, GraphqlApi } from './graphqlapi'; import { Resolver } from './resolver'; import { ResolvableFieldOptions, BaseTypeOptions, GraphqlType } from './schema-field'; import { InterfaceType } from './schema-intermediate'; @@ -67,6 +67,8 @@ export interface IField { * Generate the directives for this field * * @param modes the authorization modes of the graphql api + * + * @default - no authorization modes */ directivesToString(modes?: AuthorizationType[]): string } @@ -135,7 +137,16 @@ export interface IIntermediateType { * * @default - no intermediate type */ - readonly intermediateType?: InterfaceType; + readonly intermediateType?: IIntermediateType; + + /** + * Method called when the stringifying Intermediate Types for schema generation + * + * @param api The binding GraphQL Api [disable-awslint:ref-via-interface] + * + * @internal + */ + _bindToGraphqlApi(api: GraphqlApi): IIntermediateType; /** * Create an GraphQL Type representing this Intermediate Type @@ -149,15 +160,11 @@ export interface IIntermediateType { /** * Generate the string of this object type - * - * @param modes the authorization modes for the graphql api */ - toString(modes?: AuthorizationType[]): string; + toString(): string; /** * Add a field to this Intermediate Type - * - * @param options - the options to add a field */ addField(options: AddFieldOptions): void; } @@ -221,6 +228,11 @@ export class Directive { */ private statement: string; + /** + * the authorization modes for this intermediate type + */ + protected modes?: AuthorizationType[]; + private readonly mode?: AuthorizationType; private constructor(statement: string, mode?: AuthorizationType) { @@ -228,15 +240,26 @@ export class Directive { this.mode = mode; } + /** + * Method called when the stringifying Directive for schema generation + * + * @param modes the authorization modes + * + * @internal + */ + public _bindToAuthModes(modes?: AuthorizationType[]): Directive { + this.modes = modes; + return this; + } + /** * Generate the directive statement - * @param modes the authorization modes of the graphql api */ - public toString(modes?: AuthorizationType[]): string { - if (modes && this.mode && !modes.some((mode) => mode === this.mode)) { + public toString(): string { + if (this.modes && this.mode && !this.modes.some((mode) => mode === this.mode)) { throw new Error(`No Authorization Type ${this.mode} declared in GraphQL Api.`); } - if (this.mode === AuthorizationType.USER_POOL && modes && modes.length > 1) { + if (this.mode === AuthorizationType.USER_POOL && this.modes && this.modes.length > 1) { this.statement = this.statement.replace('@aws_auth', '@aws_cognito_user_pools'); } return this.statement; diff --git a/packages/@aws-cdk/aws-appsync/lib/schema-field.ts b/packages/@aws-cdk/aws-appsync/lib/schema-field.ts index 4b5f8c0f3af62..ed7c8790818db 100644 --- a/packages/@aws-cdk/aws-appsync/lib/schema-field.ts +++ b/packages/@aws-cdk/aws-appsync/lib/schema-field.ts @@ -399,7 +399,7 @@ export class Field extends GraphqlType implements IField { public directivesToString(modes?: AuthorizationType[]): string { if (!this.fieldOptions || !this.fieldOptions.directives) { return ''; } return this.fieldOptions.directives.reduce((acc, directive) => - `${acc}${directive.toString(modes)} `, '\n ').slice(0, -1); + `${acc}${directive._bindToAuthModes(modes).toString()} `, '\n ').slice(0, -1); } } diff --git a/packages/@aws-cdk/aws-appsync/lib/schema-intermediate.ts b/packages/@aws-cdk/aws-appsync/lib/schema-intermediate.ts index 4f30e506b6406..a0cb9c7274ba4 100644 --- a/packages/@aws-cdk/aws-appsync/lib/schema-intermediate.ts +++ b/packages/@aws-cdk/aws-appsync/lib/schema-intermediate.ts @@ -1,4 +1,4 @@ -import { AuthorizationType } from './graphqlapi'; +import { AuthorizationType, GraphqlApi } from './graphqlapi'; import { shapeAddition } from './private'; import { Resolver } from './resolver'; import { Directive, IField, IIntermediateType, AddFieldOptions } from './schema-base'; @@ -47,6 +47,10 @@ export class InterfaceType implements IIntermediateType { * @default - no directives */ public readonly directives?: Directive[]; + /** + * the authorization modes for this intermediate type + */ + protected modes?: AuthorizationType[]; public constructor(name: string, props: IntermediateTypeProps) { this.name = name; @@ -54,6 +58,16 @@ export class InterfaceType implements IIntermediateType { this.directives = props.directives; } + /** + * Method called when the stringifying Intermediate Types for schema generation + * + * @internal + */ + public _bindToGraphqlApi(api: GraphqlApi): IIntermediateType { + this.modes = api.modes; + return this; + } + /** * Create an GraphQL Type representing this Intermediate Type * @@ -74,16 +88,16 @@ export class InterfaceType implements IIntermediateType { /** * Generate the string of this object type */ - public toString(modes?: AuthorizationType[]): string { + public toString(): string { return shapeAddition({ prefix: 'interface', name: this.name, directives: this.directives, fields: Object.keys(this.definition).map((key) => { const field = this.definition[key]; - return `${key}${field.argsToString()}: ${field.toString()}${field.directivesToString(modes)}`; + return `${key}${field.argsToString()}: ${field.toString()}${field.directivesToString(this.modes)}`; }), - modes, + modes: this.modes, }); } @@ -155,7 +169,6 @@ export class ObjectType extends InterfaceType implements IIntermediateType { }); } - /** * Add a field to this Object Type. * @@ -174,7 +187,7 @@ export class ObjectType extends InterfaceType implements IIntermediateType { /** * Generate the string of this object type */ - public toString(modes?: AuthorizationType[]): string { + public toString(): string { return shapeAddition({ prefix: 'type', name: this.name, @@ -182,9 +195,9 @@ export class ObjectType extends InterfaceType implements IIntermediateType { directives: this.directives, fields: Object.keys(this.definition).map((key) => { const field = this.definition[key]; - return `${key}${field.argsToString()}: ${field.toString()}${field.directivesToString(modes)}`; + return `${key}${field.argsToString()}: ${field.toString()}${field.directivesToString(this.modes)}`; }), - modes, + modes: this.modes, }); } @@ -219,6 +232,10 @@ export class InputType implements IIntermediateType { * the attributes of this type */ public readonly definition: { [key: string]: IField }; + /** + * the authorization modes for this intermediate type + */ + protected modes?: AuthorizationType[]; public constructor(name: string, props: IntermediateTypeProps) { this.name = name; @@ -242,15 +259,26 @@ export class InputType implements IIntermediateType { }); } + /** + * Method called when the stringifying Intermediate Types for schema generation + * + * @internal + */ + public _bindToGraphqlApi(api: GraphqlApi): IIntermediateType { + this.modes = api.modes; + return this; + } + /** * Generate the string of this input type */ - public toString(_modes?: AuthorizationType[]): string { + public toString(): string { return shapeAddition({ prefix: 'input', name: this.name, fields: Object.keys(this.definition).map((key) => `${key}${this.definition[key].argsToString()}: ${this.definition[key].toString()}`), + modes: this.modes, }); } diff --git a/packages/@aws-cdk/aws-appsync/lib/schema.ts b/packages/@aws-cdk/aws-appsync/lib/schema.ts index 53b77ea408b8e..e1b7d763b8ad7 100644 --- a/packages/@aws-cdk/aws-appsync/lib/schema.ts +++ b/packages/@aws-cdk/aws-appsync/lib/schema.ts @@ -81,7 +81,7 @@ export class Schema { apiId: api.apiId, definition: this.mode === SchemaMode.CODE ? Lazy.stringValue({ - produce: () => this.types.reduce((acc, type) => { return `${acc}${type.toString(api.modes)}\n`; }, + produce: () => this.types.reduce((acc, type) => { return `${acc}${type._bindToGraphqlApi(api).toString()}\n`; }, `${this.declareSchema()}${this.definition}`), }) : this.definition, From 2fd8682cad0dee186152546094c73d793f23bd8f Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Fri, 4 Sep 2020 09:21:54 +0100 Subject: [PATCH 4/8] chore(core,cx-api): centralize defaults for future flags (#10124) This is in preparation for v2.0 in which future flags will be turned on by default. For a period of few months, two active branches `master` and `v2-master` will be present with continuous merges from former to latter. This change will reduce the number of merge conflicts between the branches after the defaults have been flipped. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/core/lib/feature-flags.ts | 29 +++++++++++++++++ packages/@aws-cdk/core/lib/index.ts | 2 ++ packages/@aws-cdk/core/lib/private/refs.ts | 3 +- packages/@aws-cdk/core/lib/stack.ts | 9 ++++-- .../@aws-cdk/core/test/test.feature-flags.ts | 31 +++++++++++++++++++ packages/@aws-cdk/cx-api/lib/features.ts | 17 +++++++++- .../@aws-cdk/cx-api/test/features.test.ts | 7 +++++ packages/aws-cdk/bin/cdk.ts | 10 ++++-- 8 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 packages/@aws-cdk/core/lib/feature-flags.ts create mode 100644 packages/@aws-cdk/core/test/test.feature-flags.ts create mode 100644 packages/@aws-cdk/cx-api/test/features.test.ts diff --git a/packages/@aws-cdk/core/lib/feature-flags.ts b/packages/@aws-cdk/core/lib/feature-flags.ts new file mode 100644 index 0000000000000..924283af30fcc --- /dev/null +++ b/packages/@aws-cdk/core/lib/feature-flags.ts @@ -0,0 +1,29 @@ +import * as cxapi from '@aws-cdk/cx-api'; +import { Construct } from '../lib/construct-compat'; + +/** + * Features that are implemented behind a flag in order to preserve backwards + * compatibility for existing apps. The list of flags are available in the + * `@aws-cdk/cx-api` module. + * + * The state of the flag for this application is stored as a CDK context variable. + */ +export class FeatureFlags { + /** + * Inspect feature flags on the construct node's context. + */ + public static of(scope: Construct) { + return new FeatureFlags(scope); + } + + private constructor(private readonly construct: Construct) {} + + /** + * Check whether a feature flag is enabled. If configured, the flag is present in + * the construct node context. Falls back to the defaults defined in the `cx-api` + * module. + */ + public isEnabled(featureFlag: string): boolean | undefined { + return this.construct.node.tryGetContext(featureFlag) ?? cxapi.futureFlagDefault(featureFlag); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/core/lib/index.ts b/packages/@aws-cdk/core/lib/index.ts index 2ea4c92f79db4..28cae5f1d8e68 100644 --- a/packages/@aws-cdk/core/lib/index.ts +++ b/packages/@aws-cdk/core/lib/index.ts @@ -58,6 +58,8 @@ export * from './custom-resource-provider'; export * from './cfn-capabilities'; export * from './cloudformation.generated'; +export * from './feature-flags'; + // WARNING: Should not be exported, but currently is because of a bug. See the // class description for more information. export * from './private/intrinsic'; diff --git a/packages/@aws-cdk/core/lib/private/refs.ts b/packages/@aws-cdk/core/lib/private/refs.ts index 6521df69ed585..0fdc5e1bed40f 100644 --- a/packages/@aws-cdk/core/lib/private/refs.ts +++ b/packages/@aws-cdk/core/lib/private/refs.ts @@ -7,6 +7,7 @@ import { CfnElement } from '../cfn-element'; import { CfnOutput } from '../cfn-output'; import { CfnParameter } from '../cfn-parameter'; import { Construct, IConstruct } from '../construct-compat'; +import { FeatureFlags } from '../feature-flags'; import { Reference } from '../reference'; import { IResolvable } from '../resolvable'; import { Stack } from '../stack'; @@ -201,7 +202,7 @@ function getCreateExportsScope(stack: Stack) { } function generateExportName(stackExports: Construct, id: string) { - const stackRelativeExports = stackExports.node.tryGetContext(cxapi.STACK_RELATIVE_EXPORTS_CONTEXT); + const stackRelativeExports = FeatureFlags.of(stackExports).isEnabled(cxapi.STACK_RELATIVE_EXPORTS_CONTEXT); const stack = Stack.of(stackExports); const components = [ diff --git a/packages/@aws-cdk/core/lib/stack.ts b/packages/@aws-cdk/core/lib/stack.ts index de26587a2a861..bfe60415f862d 100644 --- a/packages/@aws-cdk/core/lib/stack.ts +++ b/packages/@aws-cdk/core/lib/stack.ts @@ -13,6 +13,7 @@ import { CfnResource, TagType } from './cfn-resource'; import { Construct, IConstruct, ISynthesisSession } from './construct-compat'; import { ContextProvider } from './context-provider'; import { Environment } from './environment'; +import { FeatureFlags } from './feature-flags'; import { CLOUDFORMATION_TOKEN_RESOLVER, CloudFormationLang } from './private/cloudformation-lang'; import { LogicalIDs } from './private/logical-id'; import { resolve } from './private/resolve'; @@ -358,14 +359,16 @@ export class Stack extends Construct implements ITaggable { // // Also use the new behavior if we are using the new CI/CD-ready synthesizer; that way // people only have to flip one flag. - // eslint-disable-next-line max-len - this.artifactId = this.node.tryGetContext(cxapi.ENABLE_STACK_NAME_DUPLICATES_CONTEXT) || this.node.tryGetContext(cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT) + const featureFlags = FeatureFlags.of(this); + const stackNameDupeContext = featureFlags.isEnabled(cxapi.ENABLE_STACK_NAME_DUPLICATES_CONTEXT); + const newStyleSynthesisContext = featureFlags.isEnabled(cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT); + this.artifactId = (stackNameDupeContext || newStyleSynthesisContext) ? this.generateStackArtifactId() : this.stackName; this.templateFile = `${this.artifactId}.template.json`; - this.synthesizer = props.synthesizer ?? (this.node.tryGetContext(cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT) + this.synthesizer = props.synthesizer ?? (newStyleSynthesisContext ? new DefaultStackSynthesizer() : new LegacyStackSynthesizer()); this.synthesizer.bind(this); diff --git a/packages/@aws-cdk/core/test/test.feature-flags.ts b/packages/@aws-cdk/core/test/test.feature-flags.ts new file mode 100644 index 0000000000000..abb1723e6a3dc --- /dev/null +++ b/packages/@aws-cdk/core/test/test.feature-flags.ts @@ -0,0 +1,31 @@ +import * as cxapi from '@aws-cdk/cx-api'; +import { Test } from 'nodeunit'; +import { FeatureFlags, Stack } from '../lib'; + +export = { + isEnabled: { + 'returns true when the flag is enabled'(test: Test) { + const stack = new Stack(); + stack.node.setContext(cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT, true); + + const actual = FeatureFlags.of(stack).isEnabled(cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT); + test.equals(actual, true); + test.done(); + }, + + 'falls back to the default'(test: Test) { + const stack = new Stack(); + + test.equals(FeatureFlags.of(stack).isEnabled(cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT), + cxapi.futureFlagDefault(cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT)); + test.done(); + }, + + 'invalid flag'(test: Test) { + const stack = new Stack(); + + test.equals(FeatureFlags.of(stack).isEnabled('non-existent-flag'), undefined); + test.done(); + }, + }, +} \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/lib/features.ts b/packages/@aws-cdk/cx-api/lib/features.ts index 5483e64f92021..0a4ee8b8d02b0 100644 --- a/packages/@aws-cdk/cx-api/lib/features.ts +++ b/packages/@aws-cdk/cx-api/lib/features.ts @@ -63,5 +63,20 @@ export const FUTURE_FLAGS = { [STACK_RELATIVE_EXPORTS_CONTEXT]: 'true', // We will advertise this flag when the feature is complete - // [NEW_STYLE_STACK_SYNTHESIS]: 'true', + // [NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: 'true', }; + +/** + * The set of defaults that should be applied if the feature flag is not + * explicitly configured. + */ +const FUTURE_FLAGS_DEFAULTS: { [key: string]: boolean } = { + [ENABLE_STACK_NAME_DUPLICATES_CONTEXT]: false, + [ENABLE_DIFF_NO_FAIL_CONTEXT]: false, + [STACK_RELATIVE_EXPORTS_CONTEXT]: false, + [NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: false, +}; + +export function futureFlagDefault(flag: string): boolean { + return FUTURE_FLAGS_DEFAULTS[flag]; +} \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/test/features.test.ts b/packages/@aws-cdk/cx-api/test/features.test.ts new file mode 100644 index 0000000000000..fbff6c236b984 --- /dev/null +++ b/packages/@aws-cdk/cx-api/test/features.test.ts @@ -0,0 +1,7 @@ +import * as feats from '../lib/features'; + +test('all future flags have defaults configured', () => { + Object.keys(feats.FUTURE_FLAGS).forEach(flag => { + expect(typeof(feats.futureFlagDefault(flag))).toEqual('boolean'); + }); +}); \ No newline at end of file diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index e03dc68db94d0..56265748bf3a3 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -224,13 +224,14 @@ async function initCommandLine() { return await cli.list(args.STACKS, { long: args.long }); case 'diff': + const enableDiffNoFail = isFeatureEnabled(configuration, cxapi.ENABLE_DIFF_NO_FAIL); return await cli.diff({ stackNames: args.STACKS, exclusively: args.exclusively, templatePath: args.template, strict: args.strict, contextLines: args.contextLines, - fail: args.fail || !configuration.context.get(cxapi.ENABLE_DIFF_NO_FAIL), + fail: args.fail || !enableDiffNoFail, }); case 'bootstrap': @@ -241,13 +242,14 @@ async function initCommandLine() { // anticipation of flipping the switch, in user messaging we still call it // "new" bootstrapping. let source: BootstrapSource = { source: 'legacy' }; + const newStyleStackSynthesis = isFeatureEnabled(configuration, cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT); if (args.template) { print(`Using bootstrapping template from ${args.template}`); source = { source: 'custom', templateFile: args.template }; } else if (process.env.CDK_NEW_BOOTSTRAP) { print('CDK_NEW_BOOTSTRAP set, using new-style bootstrapping'); source = { source: 'default' }; - } else if (configuration.context.get(cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT)) { + } else if (newStyleStackSynthesis) { print(`'${cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT}' context set, using new-style bootstrapping`); source = { source: 'default' }; } @@ -336,6 +338,10 @@ async function initCommandLine() { } } +function isFeatureEnabled(configuration: Configuration, featureFlag: string) { + return configuration.context.get(featureFlag) ?? cxapi.futureFlagDefault(featureFlag); +} + /** * Translate a Yargs input array to something that makes more sense in a programming language * model (telling the difference between absence and an empty array) From dd5fa2b0334e058d339525c9bfaa6200fc495e25 Mon Sep 17 00:00:00 2001 From: Eli Polonsky Date: Fri, 4 Sep 2020 12:13:55 +0300 Subject: [PATCH 5/8] Added breaking change notice for `--qualifier` --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77f99f927709b..23cce398a8d95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file. See [standa * **eks:** when importing EKS clusters using `eks.Cluster.fromClusterAttributes`, the `clusterArn` attribute is not supported anymore, and will always be derived from `clusterName`. * **eks**: Only a single `eks.Cluster` is allowed per CloudFormation stack. * **eks**: The `securityGroups` attribute of `ClusterAttributes` is now `securityGroupIds`. +* **cli**: `--qualifier` must be alphanumeric and not longer than 10 characters when bootstrapping using `newStyleStackSynthesis`. ### Features From 0f0d1e74fb71a7b415aa9a5d02258b7c5933536b Mon Sep 17 00:00:00 2001 From: Ahmed Kamel Date: Fri, 4 Sep 2020 11:28:42 +0100 Subject: [PATCH 6/8] feat(chatbot): log retention support and metrics utility methods (#10137) Closes #10135 ---- *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-chatbot/README.md | 16 ++ .../lib/slack-channel-configuration.ts | 75 +++++++ packages/@aws-cdk/aws-chatbot/package.json | 4 + .../integ.chatbot-logretention.expected.json | 195 ++++++++++++++++++ .../test/integ.chatbot-logretention.ts | 33 +++ .../test/slack-channel-configuration.test.ts | 80 +++++++ 6 files changed, 403 insertions(+) create mode 100644 packages/@aws-cdk/aws-chatbot/test/integ.chatbot-logretention.expected.json create mode 100644 packages/@aws-cdk/aws-chatbot/test/integ.chatbot-logretention.ts diff --git a/packages/@aws-cdk/aws-chatbot/README.md b/packages/@aws-cdk/aws-chatbot/README.md index 83d6afcef7773..b45609ebc066f 100644 --- a/packages/@aws-cdk/aws-chatbot/README.md +++ b/packages/@aws-cdk/aws-chatbot/README.md @@ -35,3 +35,19 @@ slackChannel.addToPrincipalPolicy(new iam.PolicyStatement({ resources: ['arn:aws:s3:::abc/xyz/123.txt'], })); ``` + +### Log Group + +Slack channel configuration automatically create a log group with the name `/aws/chatbot/` in `us-east-1` upon first execution with +log data set to never expire. + +The `logRetention` property can be used to set a different expiration period. A log group will be created if not already exists. +If the log group already exists, it's expiration will be configured to the value specified in this construct (never expire, by default). + +By default, CDK uses the AWS SDK retry options when interacting with the log group. The `logRetentionRetryOptions` property +allows you to customize the maximum number of retries and base backoff duration. + +*Note* that, if `logRetention` is set, a [CloudFormation custom +resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cfn-customresource.html) is added +to the stack that pre-creates the log group as part of the stack deployment, if it already doesn't exist, and sets the +correct log retention period (never expire, by default). diff --git a/packages/@aws-cdk/aws-chatbot/lib/slack-channel-configuration.ts b/packages/@aws-cdk/aws-chatbot/lib/slack-channel-configuration.ts index d7746c6b8d679..e686d5b8b3209 100644 --- a/packages/@aws-cdk/aws-chatbot/lib/slack-channel-configuration.ts +++ b/packages/@aws-cdk/aws-chatbot/lib/slack-channel-configuration.ts @@ -1,4 +1,6 @@ +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as iam from '@aws-cdk/aws-iam'; +import * as logs from '@aws-cdk/aws-logs'; import * as sns from '@aws-cdk/aws-sns'; import * as cdk from '@aws-cdk/core'; import { CfnSlackChannelConfiguration } from './chatbot.generated'; @@ -52,6 +54,31 @@ export interface SlackChannelConfigurationProps { * @default LoggingLevel.NONE */ readonly loggingLevel?: LoggingLevel; + + /** + * The number of days log events are kept in CloudWatch Logs. When updating + * this property, unsetting it doesn't remove the log retention policy. To + * remove the retention policy, set the value to `INFINITE`. + * + * @default logs.RetentionDays.INFINITE + */ + readonly logRetention?: logs.RetentionDays; + + /** + * The IAM role for the Lambda function associated with the custom resource + * that sets the retention policy. + * + * @default - A new role is created. + */ + readonly logRetentionRole?: iam.IRole; + + /** + * When log retention is specified, a custom resource attempts to create the CloudWatch log group. + * These options control the retry policy when interacting with CloudWatch APIs. + * + * @default - Default AWS SDK retry options. + */ + readonly logRetentionRetryOptions?: logs.LogRetentionRetryOptions; } /** @@ -104,6 +131,11 @@ export interface ISlackChannelConfiguration extends cdk.IResource, iam.IGrantabl * Adds a statement to the IAM role. */ addToRolePolicy(statement: iam.PolicyStatement): void; + + /** + * Return the given named metric for this SlackChannelConfiguration + */ + metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric; } /** @@ -129,6 +161,23 @@ abstract class SlackChannelConfigurationBase extends cdk.Resource implements ISl this.role.addToPrincipalPolicy(statement); } + + /** + * Return the given named metric for this SlackChannelConfiguration + */ + public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { + // AWS Chatbot publishes metrics to us-east-1 regardless of stack region + // https://docs.aws.amazon.com/chatbot/latest/adminguide/monitoring-cloudwatch.html + return new cloudwatch.Metric({ + namespace: 'AWS/Chatbot', + region: 'us-east-1', + dimensions: { + ConfigurationName: this.slackChannelConfigurationName, + }, + metricName, + ...props, + }); + } } /** @@ -180,6 +229,20 @@ export class SlackChannelConfiguration extends SlackChannelConfigurationBase { return new Import(scope, id); } + /** + * Return the given named metric for All SlackChannelConfigurations + */ + public static metricAll(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { + // AWS Chatbot publishes metrics to us-east-1 regardless of stack region + // https://docs.aws.amazon.com/chatbot/latest/adminguide/monitoring-cloudwatch.html + return new cloudwatch.Metric({ + namespace: 'AWS/Chatbot', + region: 'us-east-1', + metricName, + ...props, + }); + } + readonly slackChannelConfigurationArn: string; readonly slackChannelConfigurationName: string; @@ -208,6 +271,18 @@ export class SlackChannelConfiguration extends SlackChannelConfigurationBase { loggingLevel: props.loggingLevel?.toString(), }); + // Log retention + // AWS Chatbot publishes logs to us-east-1 regardless of stack region https://docs.aws.amazon.com/chatbot/latest/adminguide/cloudwatch-logs.html + if (props.logRetention) { + new logs.LogRetention(this, 'LogRetention', { + logGroupName: `/aws/chatbot/${props.slackChannelConfigurationName}`, + retention: props.logRetention, + role: props.logRetentionRole, + logGroupRegion: 'us-east-1', + logRetentionRetryOptions: props.logRetentionRetryOptions, + }); + } + this.slackChannelConfigurationArn = configuration.ref; this.slackChannelConfigurationName = props.slackChannelConfigurationName; } diff --git a/packages/@aws-cdk/aws-chatbot/package.json b/packages/@aws-cdk/aws-chatbot/package.json index 2a2acb7013526..59ddd0cd77453 100644 --- a/packages/@aws-cdk/aws-chatbot/package.json +++ b/packages/@aws-cdk/aws-chatbot/package.json @@ -73,13 +73,17 @@ "pkglint": "0.0.0" }, "dependencies": { + "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.4" }, "peerDependencies": { + "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.4" diff --git a/packages/@aws-cdk/aws-chatbot/test/integ.chatbot-logretention.expected.json b/packages/@aws-cdk/aws-chatbot/test/integ.chatbot-logretention.expected.json new file mode 100644 index 0000000000000..f2e0c5c3edda9 --- /dev/null +++ b/packages/@aws-cdk/aws-chatbot/test/integ.chatbot-logretention.expected.json @@ -0,0 +1,195 @@ +{ + "Resources": { + "MySlackChannelConfigurationRole1D3F23AE": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "chatbot.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MySlackChannelConfigurationRoleDefaultPolicyE4C1FA62": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:GetObject", + "Effect": "Allow", + "Resource": "arn:aws:s3:::abc/xyz/123.txt" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MySlackChannelConfigurationRoleDefaultPolicyE4C1FA62", + "Roles": [ + { + "Ref": "MySlackChannelConfigurationRole1D3F23AE" + } + ] + } + }, + "MySlackChannelA8E0B56C": { + "Type": "AWS::Chatbot::SlackChannelConfiguration", + "Properties": { + "ConfigurationName": "test-channel", + "IamRoleArn": { + "Fn::GetAtt": [ + "MySlackChannelConfigurationRole1D3F23AE", + "Arn" + ] + }, + "SlackChannelId": "C0187JABUE9", + "SlackWorkspaceId": "T49239U4W", + "LoggingLevel": "NONE" + } + }, + "MySlackChannelLogRetention84AA443F": { + "Type": "Custom::LogRetention", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A", + "Arn" + ] + }, + "LogGroupName": "/aws/chatbot/test-channel", + "RetentionInDays": 30, + "LogGroupRegion": "us-east-1" + } + }, + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB": { + "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" + ] + ] + } + ] + } + }, + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:PutRetentionPolicy", + "logs:DeleteRetentionPolicy" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB", + "Roles": [ + { + "Ref": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB" + } + ] + } + }, + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters74a1cab76f5603c5e27101cb3809d8745c50f708b0f4b497ed0910eb533d437bS3Bucket48EF98C9" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters74a1cab76f5603c5e27101cb3809d8745c50f708b0f4b497ed0910eb533d437bS3VersionKeyF33C73AF" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters74a1cab76f5603c5e27101cb3809d8745c50f708b0f4b497ed0910eb533d437bS3VersionKeyF33C73AF" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB", + "Arn" + ] + }, + "Runtime": "nodejs10.x" + }, + "DependsOn": [ + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB", + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB" + ] + } + }, + "Parameters": { + "AssetParameters74a1cab76f5603c5e27101cb3809d8745c50f708b0f4b497ed0910eb533d437bS3Bucket48EF98C9": { + "Type": "String", + "Description": "S3 bucket for asset \"74a1cab76f5603c5e27101cb3809d8745c50f708b0f4b497ed0910eb533d437b\"" + }, + "AssetParameters74a1cab76f5603c5e27101cb3809d8745c50f708b0f4b497ed0910eb533d437bS3VersionKeyF33C73AF": { + "Type": "String", + "Description": "S3 key for asset version \"74a1cab76f5603c5e27101cb3809d8745c50f708b0f4b497ed0910eb533d437b\"" + }, + "AssetParameters74a1cab76f5603c5e27101cb3809d8745c50f708b0f4b497ed0910eb533d437bArtifactHash976CF1BD": { + "Type": "String", + "Description": "Artifact hash for asset \"74a1cab76f5603c5e27101cb3809d8745c50f708b0f4b497ed0910eb533d437b\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-chatbot/test/integ.chatbot-logretention.ts b/packages/@aws-cdk/aws-chatbot/test/integ.chatbot-logretention.ts new file mode 100644 index 0000000000000..cbb8de485b295 --- /dev/null +++ b/packages/@aws-cdk/aws-chatbot/test/integ.chatbot-logretention.ts @@ -0,0 +1,33 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as logs from '@aws-cdk/aws-logs'; +import * as cdk from '@aws-cdk/core'; +import * as chatbot from '../lib'; + +class ChatbotLogRetentionInteg extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const slackChannel = new chatbot.SlackChannelConfiguration(this, 'MySlackChannel', { + slackChannelConfigurationName: 'test-channel', + slackWorkspaceId: 'T49239U4W', // modify to your slack workspace id + slackChannelId: 'C0187JABUE9', // modify to your slack channel id + loggingLevel: chatbot.LoggingLevel.NONE, + logRetention: logs.RetentionDays.ONE_MONTH, + }); + + slackChannel.addToRolePolicy(new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + 's3:GetObject', + ], + resources: ['arn:aws:s3:::abc/xyz/123.txt'], + })); + } +} + +const app = new cdk.App(); + +new ChatbotLogRetentionInteg(app, 'ChatbotLogRetentionInteg'); + +app.synth(); + diff --git a/packages/@aws-cdk/aws-chatbot/test/slack-channel-configuration.test.ts b/packages/@aws-cdk/aws-chatbot/test/slack-channel-configuration.test.ts index 3cf1189d9fee2..de5a5da9a63c1 100644 --- a/packages/@aws-cdk/aws-chatbot/test/slack-channel-configuration.test.ts +++ b/packages/@aws-cdk/aws-chatbot/test/slack-channel-configuration.test.ts @@ -1,5 +1,8 @@ import '@aws-cdk/assert/jest'; +import { ABSENT } from '@aws-cdk/assert'; +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as iam from '@aws-cdk/aws-iam'; +import * as logs from '@aws-cdk/aws-logs'; import * as sns from '@aws-cdk/aws-sns'; import * as cdk from '@aws-cdk/core'; import * as chatbot from '../lib'; @@ -138,6 +141,83 @@ describe('SlackChannelConfiguration', () => { }); }); + test('specifying log retention', () => { + new chatbot.SlackChannelConfiguration(stack, 'MySlackChannel', { + slackWorkspaceId: 'ABC123', + slackChannelId: 'DEF456', + slackChannelConfigurationName: 'ConfigurationName', + logRetention: logs.RetentionDays.ONE_MONTH, + }); + + expect(stack).toHaveResourceLike('Custom::LogRetention', { + LogGroupName: '/aws/chatbot/ConfigurationName', + RetentionInDays: 30, + LogGroupRegion: 'us-east-1', + }); + }); + + test('getting configuration metric', () => { + const slackChannel = new chatbot.SlackChannelConfiguration(stack, 'MySlackChannel', { + slackWorkspaceId: 'ABC123', + slackChannelId: 'DEF456', + slackChannelConfigurationName: 'ConfigurationName', + logRetention: logs.RetentionDays.ONE_MONTH, + }); + const metric = slackChannel.metric('MetricName'); + new cloudwatch.Alarm(stack, 'Alarm', { + evaluationPeriods: 1, + threshold: 0, + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, + metric: metric, + }); + + expect(metric).toEqual(new cloudwatch.Metric({ + namespace: 'AWS/Chatbot', + region: 'us-east-1', + dimensions: { + ConfigurationName: 'ConfigurationName', + }, + metricName: 'MetricName', + })); + expect(stack).toHaveResourceLike('AWS::CloudWatch::Alarm', { + Namespace: 'AWS/Chatbot', + MetricName: 'MetricName', + Dimensions: [ + { + Name: 'ConfigurationName', + Value: 'ConfigurationName', + }, + ], + ComparisonOperator: 'GreaterThanThreshold', + EvaluationPeriods: 1, + Threshold: 0, + }); + }); + + test('getting all configurations metric', () => { + const metric = chatbot.SlackChannelConfiguration.metricAll('MetricName'); + new cloudwatch.Alarm(stack, 'Alarm', { + evaluationPeriods: 1, + threshold: 0, + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, + metric: metric, + }); + + expect(metric).toEqual(new cloudwatch.Metric({ + namespace: 'AWS/Chatbot', + region: 'us-east-1', + metricName: 'MetricName', + })); + expect(stack).toHaveResourceLike('AWS::CloudWatch::Alarm', { + Namespace: 'AWS/Chatbot', + MetricName: 'MetricName', + Dimensions: ABSENT, + ComparisonOperator: 'GreaterThanThreshold', + EvaluationPeriods: 1, + Threshold: 0, + }); + }); + test('added a iam policy to a from slack channel configuration ARN will nothing to do', () => { const imported = chatbot.SlackChannelConfiguration.fromSlackChannelConfigurationArn(stack, 'MySlackChannel', 'arn:aws:chatbot::1234567890:chat-configuration/slack-channel/my-slack'); From 1260d5215d474d6edc2460ffe9658552d17ab239 Mon Sep 17 00:00:00 2001 From: Andrew Hammond <445764+ahammond@users.noreply.github.com> Date: Fri, 4 Sep 2020 04:32:49 -0700 Subject: [PATCH 7/8] feat(secrets-manager): exclude characters for password rotation applications (#10110) Fixes EXCLUDED_CHARACTERS part of #4144 --- .../@aws-cdk/aws-docdb/test/cluster.test.ts | 4 +- .../integ.cluster-rotation.lit.expected.json | 4 +- .../test/integ.instance.lit.expected.json | 2 +- .../@aws-cdk/aws-secretsmanager/README.md | 3 +- .../aws-secretsmanager/lib/secret-rotation.ts | 39 ++++++++++++------- .../test/test.secret-rotation.ts | 5 ++- 6 files changed, 36 insertions(+), 21 deletions(-) diff --git a/packages/@aws-cdk/aws-docdb/test/cluster.test.ts b/packages/@aws-cdk/aws-docdb/test/cluster.test.ts index fb26905d1a027..f227293310bf3 100644 --- a/packages/@aws-cdk/aws-docdb/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-docdb/test/cluster.test.ts @@ -580,7 +580,7 @@ describe('DatabaseCluster', () => { expectCDK(stack).to(haveResource('AWS::Serverless::Application', { Location: { ApplicationId: 'arn:aws:serverlessrepo:us-east-1:297356227824:applications/SecretsManagerMongoDBRotationSingleUser', - SemanticVersion: '1.1.3', + SemanticVersion: '1.1.60', }, Parameters: { endpoint: { @@ -698,7 +698,7 @@ describe('DatabaseCluster', () => { expectCDK(stack).to(haveResource('AWS::Serverless::Application', { Location: { ApplicationId: 'arn:aws:serverlessrepo:us-east-1:297356227824:applications/SecretsManagerMongoDBRotationMultiUser', - SemanticVersion: '1.1.3', + SemanticVersion: '1.1.60', }, Parameters: { endpoint: { diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json b/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json index 348dba3e65ae7..e58745e098767 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json @@ -765,7 +765,7 @@ "Properties": { "Location": { "ApplicationId": "arn:aws:serverlessrepo:us-east-1:297356227824:applications/SecretsManagerRDSMySQLRotationSingleUser", - "SemanticVersion": "1.1.3" + "SemanticVersion": "1.1.60" }, "Parameters": { "endpoint": { @@ -812,4 +812,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json index 9f591e2399a62..53c8c03e0f283 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json @@ -813,7 +813,7 @@ "Properties": { "Location": { "ApplicationId": "arn:aws:serverlessrepo:us-east-1:297356227824:applications/SecretsManagerRDSOracleRotationSingleUser", - "SemanticVersion": "1.1.3" + "SemanticVersion": "1.1.60" }, "Parameters": { "endpoint": { diff --git a/packages/@aws-cdk/aws-secretsmanager/README.md b/packages/@aws-cdk/aws-secretsmanager/README.md index 540cc9a7fa0be..e8a511ecef269 100644 --- a/packages/@aws-cdk/aws-secretsmanager/README.md +++ b/packages/@aws-cdk/aws-secretsmanager/README.md @@ -43,7 +43,7 @@ A secret can set `RemovalPolicy`. If it set to `RETAIN`, that removing a secret ### Grant permission to use the secret to a role -You must grant permission to a resource for that resource to be allowed to +You must grant permission to a resource for that resource to be allowed to use a secret. This can be achieved with the `Secret.grantRead` and/or `Secret.grantUpdate` method, depending on your need: @@ -86,6 +86,7 @@ new SecretRotation(this, 'SecretRotation', { secret: mySecret, target: myDatabase, // a Connectable vpc: myVpc, // The VPC where the secret rotation application will be deployed + excludeCharacters: ` ;+%{}` + `@'"\`/\\#`, // A string of characters to never use when generating new passwords. Example is a superset of the characters which will break DMS endpoints and characters which cause problems in BASH scripts. }); ``` diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/secret-rotation.ts b/packages/@aws-cdk/aws-secretsmanager/lib/secret-rotation.ts index a64d65f146d70..6ef72114413cf 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/secret-rotation.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/secret-rotation.ts @@ -22,84 +22,84 @@ export class SecretRotationApplication { /** * Conducts an AWS SecretsManager secret rotation for RDS MariaDB using the single user rotation scheme */ - public static readonly MARIADB_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerRDSMariaDBRotationSingleUser', '1.1.3'); + public static readonly MARIADB_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerRDSMariaDBRotationSingleUser', '1.1.60'); /** * Conducts an AWS SecretsManager secret rotation for RDS MariaDB using the multi user rotation scheme */ - public static readonly MARIADB_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerRDSMariaDBRotationMultiUser', '1.1.3', { + public static readonly MARIADB_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerRDSMariaDBRotationMultiUser', '1.1.60', { isMultiUser: true, }); /** * Conducts an AWS SecretsManager secret rotation for RDS MySQL using the single user rotation scheme */ - public static readonly MYSQL_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerRDSMySQLRotationSingleUser', '1.1.3'); + public static readonly MYSQL_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerRDSMySQLRotationSingleUser', '1.1.60'); /** * Conducts an AWS SecretsManager secret rotation for RDS MySQL using the multi user rotation scheme */ - public static readonly MYSQL_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerRDSMySQLRotationMultiUser', '1.1.3', { + public static readonly MYSQL_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerRDSMySQLRotationMultiUser', '1.1.60', { isMultiUser: true, }); /** * Conducts an AWS SecretsManager secret rotation for RDS Oracle using the single user rotation scheme */ - public static readonly ORACLE_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerRDSOracleRotationSingleUser', '1.1.3'); + public static readonly ORACLE_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerRDSOracleRotationSingleUser', '1.1.60'); /** * Conducts an AWS SecretsManager secret rotation for RDS Oracle using the multi user rotation scheme */ - public static readonly ORACLE_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerRDSOracleRotationMultiUser', '1.1.3', { + public static readonly ORACLE_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerRDSOracleRotationMultiUser', '1.1.60', { isMultiUser: true, }); /** * Conducts an AWS SecretsManager secret rotation for RDS PostgreSQL using the single user rotation scheme */ - public static readonly POSTGRES_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerRDSPostgreSQLRotationSingleUser', '1.1.3'); + public static readonly POSTGRES_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerRDSPostgreSQLRotationSingleUser', '1.1.60'); /** * Conducts an AWS SecretsManager secret rotation for RDS PostgreSQL using the multi user rotation scheme */ - public static readonly POSTGRES_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerRDSPostgreSQLRotationMultiUser', '1.1.3', { + public static readonly POSTGRES_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerRDSPostgreSQLRotationMultiUser', '1.1.60', { isMultiUser: true, }); /** * Conducts an AWS SecretsManager secret rotation for RDS SQL Server using the single user rotation scheme */ - public static readonly SQLSERVER_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerRDSSQLServerRotationSingleUser', '1.1.3'); + public static readonly SQLSERVER_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerRDSSQLServerRotationSingleUser', '1.1.60'); /** * Conducts an AWS SecretsManager secret rotation for RDS SQL Server using the multi user rotation scheme */ - public static readonly SQLSERVER_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerRDSSQLServerRotationMultiUser', '1.1.3', { + public static readonly SQLSERVER_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerRDSSQLServerRotationMultiUser', '1.1.60', { isMultiUser: true, }); /** * Conducts an AWS SecretsManager secret rotation for Amazon Redshift using the single user rotation scheme */ - public static readonly REDSHIFT_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerRedshiftRotationSingleUser', '1.1.3'); + public static readonly REDSHIFT_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerRedshiftRotationSingleUser', '1.1.60'); /** * Conducts an AWS SecretsManager secret rotation for Amazon Redshift using the multi user rotation scheme */ - public static readonly REDSHIFT_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerRedshiftRotationMultiUser', '1.1.3', { + public static readonly REDSHIFT_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerRedshiftRotationMultiUser', '1.1.60', { isMultiUser: true, }); /** * Conducts an AWS SecretsManager secret rotation for MongoDB using the single user rotation scheme */ - public static readonly MONGODB_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerMongoDBRotationSingleUser', '1.1.3'); + public static readonly MONGODB_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerMongoDBRotationSingleUser', '1.1.60'); /** * Conducts an AWS SecretsManager secret rotation for MongoDB using the multi user rotation scheme */ - public static readonly MONGODB_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerMongoDBRotationMultiUser', '1.1.3', { + public static readonly MONGODB_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerMongoDBRotationMultiUser', '1.1.60', { isMultiUser: true, }); @@ -193,6 +193,13 @@ export interface SecretRotationProps { * @default - a new security group is created */ readonly securityGroup?: ec2.ISecurityGroup; + + /** + * Characters which should not appear in the generated password + * + * @default - no additional characters are explicitly excluded + */ + readonly excludeCharacters?: string; } /** @@ -226,6 +233,10 @@ export class SecretRotation extends Construct { vpcSecurityGroupIds: securityGroup.securityGroupId, }; + if (props.excludeCharacters) { + parameters.excludeCharacters = props.excludeCharacters; + } + if (props.secret.encryptionKey) { parameters.kmsKeyArn = props.secret.encryptionKey.keyArn; } diff --git a/packages/@aws-cdk/aws-secretsmanager/test/test.secret-rotation.ts b/packages/@aws-cdk/aws-secretsmanager/test/test.secret-rotation.ts index 463853ad8af08..79351afc059f3 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/test.secret-rotation.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/test.secret-rotation.ts @@ -14,6 +14,7 @@ export = { defaultPort: ec2.Port.tcp(3306), securityGroups: [new ec2.SecurityGroup(stack, 'SecurityGroup', { vpc })], }); + const excludeCharacters = ' ;+%{}' + '@\'"`/\\#'; // DMS and BASH problem chars // WHEN new secretsmanager.SecretRotation(stack, 'SecretRotation', { @@ -21,6 +22,7 @@ export = { secret, target, vpc, + excludeCharacters: excludeCharacters, }); // THEN @@ -65,7 +67,7 @@ export = { expect(stack).to(haveResource('AWS::Serverless::Application', { Location: { ApplicationId: 'arn:aws:serverlessrepo:us-east-1:297356227824:applications/SecretsManagerRDSMySQLRotationSingleUser', - SemanticVersion: '1.1.3', + SemanticVersion: '1.1.60', }, Parameters: { endpoint: { @@ -84,6 +86,7 @@ export = { ], }, functionName: 'SecretRotation', + excludeCharacters: excludeCharacters, vpcSecurityGroupIds: { 'Fn::GetAtt': [ 'SecretRotationSecurityGroup9985012B', From 915eb4be3946652a00b7496b9e8610169852f27b Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Fri, 4 Sep 2020 13:23:27 +0100 Subject: [PATCH 8/8] feat(rds): database clusters from snapshots (#10130) Created the `DatabaseClusterFromSnapshot` to support creating database clusters from snapshots. I made some intentional decisions here to avoid exposing as much of the underlying "base" classes and interfaces as possible, to support future refactoring as necessary. fixes #4379 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-rds/README.md | 12 + packages/@aws-cdk/aws-rds/lib/cluster.ts | 540 ++++++++++-------- packages/@aws-cdk/aws-rds/package.json | 1 + .../@aws-cdk/aws-rds/test/test.cluster.ts | 36 +- 4 files changed, 351 insertions(+), 238 deletions(-) diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index 1d0bfbe9b947a..fb0890c336402 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -62,6 +62,18 @@ By default, the master password will be generated and stored in AWS Secrets Mana Your cluster will be empty by default. To add a default database upon construction, specify the `defaultDatabaseName` attribute. +Use `DatabaseClusterFromSnapshot` to create a cluster from a snapshot: + +```ts +new DatabaseClusterFromSnapshot(stack, 'Database', { + engine: DatabaseClusterEngine.aurora({ version: AuroraEngineVersion.VER_1_22_2 }), + instanceProps: { + vpc, + }, + snapshotIdentifier: 'mySnapshot', +}); +``` + ### Starting an instance database To set up a instance database, define a `DatabaseInstance`. You must diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index 4ddba423ba8ea..89b98756e0b8b 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -12,12 +12,12 @@ import { Endpoint } from './endpoint'; import { IParameterGroup } from './parameter-group'; import { BackupProps, InstanceProps, Login, PerformanceInsightRetention, RotationMultiUserOptions } from './props'; import { DatabaseProxy, DatabaseProxyOptions, ProxyTarget } from './proxy'; -import { CfnDBCluster, CfnDBInstance, CfnDBSubnetGroup } from './rds.generated'; +import { CfnDBCluster, CfnDBClusterProps, CfnDBInstance, CfnDBSubnetGroup } from './rds.generated'; /** - * Properties for a new database cluster + * Common properties for a new database cluster or cluster from snapshot. */ -export interface DatabaseClusterProps { +interface DatabaseClusterBaseProps { /** * What kind of database to start */ @@ -37,11 +37,6 @@ export interface DatabaseClusterProps { */ readonly instanceProps: InstanceProps; - /** - * Username and password for the administrative user - */ - readonly masterUser: Login; - /** * Backup settings * @@ -90,21 +85,6 @@ export interface DatabaseClusterProps { */ readonly deletionProtection?: boolean; - /** - * Whether to enable storage encryption. - * - * @default - true if storageEncryptionKey is provided, false otherwise - */ - readonly storageEncrypted?: boolean - - /** - * The KMS key for storage encryption. - * If specified, {@link storageEncrypted} will be set to `true`. - * - * @default - if storageEncrypted is true then the default master key, no key otherwise - */ - readonly storageEncryptionKey?: kms.IKey; - /** * A preferred maintenance window day/time range. Should be specified as a range ddd:hh24:mi-ddd:hh24:mi (24H Clock UTC). * @@ -289,87 +269,20 @@ abstract class DatabaseClusterBase extends Resource implements IDatabaseCluster } /** - * Create a clustered database with a given number of instances. - * - * @resource AWS::RDS::DBCluster + * Abstract base for ``DatabaseCluster`` and ``DatabaseClusterFromSnapshot`` */ -export class DatabaseCluster extends DatabaseClusterBase { - /** - * Import an existing DatabaseCluster from properties - */ - public static fromDatabaseClusterAttributes(scope: Construct, id: string, attrs: DatabaseClusterAttributes): IDatabaseCluster { - class Import extends DatabaseClusterBase implements IDatabaseCluster { - public readonly defaultPort = ec2.Port.tcp(attrs.port); - public readonly connections = new ec2.Connections({ - securityGroups: attrs.securityGroups, - defaultPort: this.defaultPort, - }); - public readonly clusterIdentifier = attrs.clusterIdentifier; - public readonly instanceIdentifiers: string[] = []; - public readonly clusterEndpoint = new Endpoint(attrs.clusterEndpointAddress, attrs.port); - public readonly clusterReadEndpoint = new Endpoint(attrs.readerEndpointAddress, attrs.port); - public readonly instanceEndpoints = attrs.instanceEndpointAddresses.map(a => new Endpoint(a, attrs.port)); - } - - return new Import(scope, id); - } +abstract class DatabaseClusterNew extends DatabaseClusterBase { - /** - * Identifier of the cluster - */ - public readonly clusterIdentifier: string; - - /** - * Identifiers of the replicas - */ public readonly instanceIdentifiers: string[] = []; - - /** - * The endpoint to use for read/write operations - */ - public readonly clusterEndpoint: Endpoint; - - /** - * Endpoint to use for load-balanced read-only operations. - */ - public readonly clusterReadEndpoint: Endpoint; - - /** - * Endpoints which address each individual replica. - */ public readonly instanceEndpoints: Endpoint[] = []; - /** - * Access to the network connections - */ - public readonly connections: ec2.Connections; - - /** - * The secret attached to this cluster - */ - public readonly secret?: secretsmanager.ISecret; - - private readonly singleUserRotationApplication: secretsmanager.SecretRotationApplication; - private readonly multiUserRotationApplication: secretsmanager.SecretRotationApplication; - - /** - * The VPC where the DB subnet group is created. - */ - private readonly vpc: ec2.IVpc; + protected readonly newCfnProps: CfnDBClusterProps; + protected readonly subnetGroup: CfnDBSubnetGroup; + protected readonly securityGroups: ec2.ISecurityGroup[]; - /** - * The subnets used by the DB subnet group. - * - * @default - the Vpc default strategy if not specified. - */ - private readonly vpcSubnets?: ec2.SubnetSelection; - - constructor(scope: Construct, id: string, props: DatabaseClusterProps) { + constructor(scope: Construct, id: string, props: DatabaseClusterBaseProps) { super(scope, id); - this.vpc = props.instanceProps.vpc; - this.vpcSubnets = props.instanceProps.vpcSubnets; - const { subnetIds } = props.instanceProps.vpc.selectSubnets(props.instanceProps.vpcSubnets); // Cannot test whether the subnets are in different AZs, but at least we can test the amount. @@ -377,32 +290,21 @@ export class DatabaseCluster extends DatabaseClusterBase { this.node.addError(`Cluster requires at least 2 subnets, got ${subnetIds.length}`); } - const subnetGroup = new CfnDBSubnetGroup(this, 'Subnets', { + this.subnetGroup = new CfnDBSubnetGroup(this, 'Subnets', { dbSubnetGroupDescription: `Subnets for ${id} database`, subnetIds, }); if (props.removalPolicy === RemovalPolicy.RETAIN) { - subnetGroup.applyRemovalPolicy(RemovalPolicy.RETAIN); + this.subnetGroup.applyRemovalPolicy(RemovalPolicy.RETAIN); } - const securityGroups = props.instanceProps.securityGroups ?? [ + this.securityGroups = props.instanceProps.securityGroups ?? [ new ec2.SecurityGroup(this, 'SecurityGroup', { description: 'RDS security group', vpc: props.instanceProps.vpc, }), ]; - let secret: DatabaseSecret | undefined; - if (!props.masterUser.password) { - secret = new DatabaseSecret(this, 'Secret', { - username: props.masterUser.username, - encryptionKey: props.masterUser.encryptionKey, - }); - } - - this.singleUserRotationApplication = props.engine.singleUserRotationApplication; - this.multiUserRotationApplication = props.engine.multiUserRotationApplication; - const clusterAssociatedRoles: CfnDBCluster.DBClusterRoleProperty[] = []; let { s3ImportRole, s3ExportRole } = this.setupS3ImportExport(props); if (s3ImportRole) { @@ -421,44 +323,169 @@ export class DatabaseCluster extends DatabaseClusterBase { const clusterParameterGroup = props.parameterGroup ?? clusterEngineBindConfig.parameterGroup; const clusterParameterGroupConfig = clusterParameterGroup?.bindToCluster({}); - const cluster = new CfnDBCluster(this, 'Resource', { + this.newCfnProps = { // Basic engine: props.engine.engineType, engineVersion: props.engine.engineVersion?.fullVersion, dbClusterIdentifier: props.clusterIdentifier, - dbSubnetGroupName: subnetGroup.ref, - vpcSecurityGroupIds: securityGroups.map(sg => sg.securityGroupId), + dbSubnetGroupName: this.subnetGroup.ref, + vpcSecurityGroupIds: this.securityGroups.map(sg => sg.securityGroupId), port: props.port ?? clusterEngineBindConfig.port, dbClusterParameterGroupName: clusterParameterGroupConfig?.parameterGroupName, associatedRoles: clusterAssociatedRoles.length > 0 ? clusterAssociatedRoles : undefined, deletionProtection: props.deletionProtection, // Admin - masterUsername: secret ? secret.secretValueFromJson('username').toString() : props.masterUser.username, - masterUserPassword: secret - ? secret.secretValueFromJson('password').toString() - : (props.masterUser.password - ? props.masterUser.password.toString() - : undefined), backupRetentionPeriod: props.backup?.retention?.toDays(), preferredBackupWindow: props.backup?.preferredWindow, preferredMaintenanceWindow: props.preferredMaintenanceWindow, databaseName: props.defaultDatabaseName, enableCloudwatchLogsExports: props.cloudwatchLogsExports, - // Encryption - kmsKeyId: props.storageEncryptionKey?.keyArn, - storageEncrypted: props.storageEncryptionKey ? true : props.storageEncrypted, - }); + }; + } + protected setRemovalPolicy(cluster: CfnDBCluster, removalPolicy?: RemovalPolicy) { // if removalPolicy was not specified, // leave it as the default, which is Snapshot - if (props.removalPolicy) { - cluster.applyRemovalPolicy(props.removalPolicy); + if (removalPolicy) { + cluster.applyRemovalPolicy(removalPolicy); } else { // The CFN default makes sense for DeletionPolicy, // but doesn't cover UpdateReplacePolicy. // Fix that here. cluster.cfnOptions.updateReplacePolicy = CfnDeletionPolicy.SNAPSHOT; } + } + + private setupS3ImportExport(props: DatabaseClusterBaseProps): { s3ImportRole?: IRole, s3ExportRole?: IRole } { + let s3ImportRole = props.s3ImportRole; + if (props.s3ImportBuckets && props.s3ImportBuckets.length > 0) { + if (props.s3ImportRole) { + throw new Error('Only one of s3ImportRole or s3ImportBuckets must be specified, not both.'); + } + + s3ImportRole = new Role(this, 'S3ImportRole', { + assumedBy: new ServicePrincipal('rds.amazonaws.com'), + }); + for (const bucket of props.s3ImportBuckets) { + bucket.grantRead(s3ImportRole); + } + } + + let s3ExportRole = props.s3ExportRole; + if (props.s3ExportBuckets && props.s3ExportBuckets.length > 0) { + if (props.s3ExportRole) { + throw new Error('Only one of s3ExportRole or s3ExportBuckets must be specified, not both.'); + } + + s3ExportRole = new Role(this, 'S3ExportRole', { + assumedBy: new ServicePrincipal('rds.amazonaws.com'), + }); + for (const bucket of props.s3ExportBuckets) { + bucket.grantReadWrite(s3ExportRole); + } + } + + return { s3ImportRole, s3ExportRole }; + } +} + +/** + * Properties for a new database cluster + */ +export interface DatabaseClusterProps extends DatabaseClusterBaseProps { + /** + * Username and password for the administrative user + */ + readonly masterUser: Login; + + /** + * Whether to enable storage encryption. + * + * @default - true if storageEncryptionKey is provided, false otherwise + */ + readonly storageEncrypted?: boolean + + /** + * The KMS key for storage encryption. + * If specified, {@link storageEncrypted} will be set to `true`. + * + * @default - if storageEncrypted is true then the default master key, no key otherwise + */ + readonly storageEncryptionKey?: kms.IKey; +} + +/** + * Create a clustered database with a given number of instances. + * + * @resource AWS::RDS::DBCluster + */ +export class DatabaseCluster extends DatabaseClusterNew { + /** + * Import an existing DatabaseCluster from properties + */ + public static fromDatabaseClusterAttributes(scope: Construct, id: string, attrs: DatabaseClusterAttributes): IDatabaseCluster { + class Import extends DatabaseClusterBase implements IDatabaseCluster { + public readonly defaultPort = ec2.Port.tcp(attrs.port); + public readonly connections = new ec2.Connections({ + securityGroups: attrs.securityGroups, + defaultPort: this.defaultPort, + }); + public readonly clusterIdentifier = attrs.clusterIdentifier; + public readonly instanceIdentifiers: string[] = []; + public readonly clusterEndpoint = new Endpoint(attrs.clusterEndpointAddress, attrs.port); + public readonly clusterReadEndpoint = new Endpoint(attrs.readerEndpointAddress, attrs.port); + public readonly instanceEndpoints = attrs.instanceEndpointAddresses.map(a => new Endpoint(a, attrs.port)); + } + + return new Import(scope, id); + } + + public readonly clusterIdentifier: string; + public readonly clusterEndpoint: Endpoint; + public readonly clusterReadEndpoint: Endpoint; + public readonly connections: ec2.Connections; + + /** + * The secret attached to this cluster + */ + public readonly secret?: secretsmanager.ISecret; + + private readonly vpc: ec2.IVpc; + private readonly vpcSubnets?: ec2.SubnetSelection; + + private readonly singleUserRotationApplication: secretsmanager.SecretRotationApplication; + private readonly multiUserRotationApplication: secretsmanager.SecretRotationApplication; + + constructor(scope: Construct, id: string, props: DatabaseClusterProps) { + super(scope, id, props); + + this.vpc = props.instanceProps.vpc; + this.vpcSubnets = props.instanceProps.vpcSubnets; + + this.singleUserRotationApplication = props.engine.singleUserRotationApplication; + this.multiUserRotationApplication = props.engine.multiUserRotationApplication; + + let secret: DatabaseSecret | undefined; + if (!props.masterUser.password) { + secret = new DatabaseSecret(this, 'Secret', { + username: props.masterUser.username, + encryptionKey: props.masterUser.encryptionKey, + }); + } + + const cluster = new CfnDBCluster(this, 'Resource', { + ...this.newCfnProps, + // Admin + masterUsername: secret ? secret.secretValueFromJson('username').toString() : props.masterUser.username, + masterUserPassword: secret + ? secret.secretValueFromJson('password').toString() + : (props.masterUser.password + ? props.masterUser.password.toString() + : undefined), + // Encryption + kmsKeyId: props.storageEncryptionKey?.keyArn, + storageEncrypted: props.storageEncryptionKey ? true : props.storageEncrypted, + }); this.clusterIdentifier = cluster.ref; @@ -466,20 +493,21 @@ export class DatabaseCluster extends DatabaseClusterBase { const portAttribute = Token.asNumber(cluster.attrEndpointPort); this.clusterEndpoint = new Endpoint(cluster.attrEndpointAddress, portAttribute); this.clusterReadEndpoint = new Endpoint(cluster.attrReadEndpointAddress, portAttribute); + this.connections = new ec2.Connections({ + securityGroups: this.securityGroups, + defaultPort: ec2.Port.tcp(this.clusterEndpoint.port), + }); - this.setLogRetention(props); + this.setRemovalPolicy(cluster, props.removalPolicy); if (secret) { this.secret = secret.attach(this); } - this.createInstances(props, cluster, subnetGroup, portAttribute); - - const defaultPort = ec2.Port.tcp(this.clusterEndpoint.port); - this.connections = new ec2.Connections({ securityGroups, defaultPort }); + setLogRetention(this, props); + createInstances(this, props, this.subnetGroup); } - /** * Adds the single user rotation of the master password to this cluster. * @@ -524,131 +552,169 @@ export class DatabaseCluster extends DatabaseClusterBase { target: this, }); } +} - private setupS3ImportExport(props: DatabaseClusterProps): { s3ImportRole?: IRole, s3ExportRole?: IRole } { - let s3ImportRole = props.s3ImportRole; - if (props.s3ImportBuckets && props.s3ImportBuckets.length > 0) { - if (props.s3ImportRole) { - throw new Error('Only one of s3ImportRole or s3ImportBuckets must be specified, not both.'); - } +/** + * Properties for ``DatabaseClusterFromSnapshot`` + */ +export interface DatabaseClusterFromSnapshotProps extends DatabaseClusterBaseProps { + /** + * The identifier for the DB instance snapshot or DB cluster snapshot to restore from. + * You can use either the name or the Amazon Resource Name (ARN) to specify a DB cluster snapshot. + * However, you can use only the ARN to specify a DB instance snapshot. + */ + readonly snapshotIdentifier: string; +} - s3ImportRole = new Role(this, 'S3ImportRole', { - assumedBy: new ServicePrincipal('rds.amazonaws.com'), - }); - for (const bucket of props.s3ImportBuckets) { - bucket.grantRead(s3ImportRole); - } - } +/** + * A database cluster restored from a snapshot. + * + * @resource AWS::RDS::DBInstance + */ +export class DatabaseClusterFromSnapshot extends DatabaseClusterNew { + public readonly clusterIdentifier: string; + public readonly clusterEndpoint: Endpoint; + public readonly clusterReadEndpoint: Endpoint; + public readonly connections: ec2.Connections; - let s3ExportRole = props.s3ExportRole; - if (props.s3ExportBuckets && props.s3ExportBuckets.length > 0) { - if (props.s3ExportRole) { - throw new Error('Only one of s3ExportRole or s3ExportBuckets must be specified, not both.'); - } + constructor(scope: Construct, id: string, props: DatabaseClusterFromSnapshotProps) { + super(scope, id, props); - s3ExportRole = new Role(this, 'S3ExportRole', { - assumedBy: new ServicePrincipal('rds.amazonaws.com'), - }); - for (const bucket of props.s3ExportBuckets) { - bucket.grantReadWrite(s3ExportRole); - } - } + const cluster = new CfnDBCluster(this, 'Resource', { + ...this.newCfnProps, + snapshotIdentifier: props.snapshotIdentifier, + }); - return { s3ImportRole, s3ExportRole }; - } + this.clusterIdentifier = cluster.ref; - private createInstances(props: DatabaseClusterProps, cluster: CfnDBCluster, subnetGroup: CfnDBSubnetGroup, portAttribute: number) { - const instanceCount = props.instances != null ? props.instances : 2; - if (instanceCount < 1) { - throw new Error('At least one instance is required'); - } + // create a number token that represents the port of the cluster + const portAttribute = Token.asNumber(cluster.attrEndpointPort); + this.clusterEndpoint = new Endpoint(cluster.attrEndpointAddress, portAttribute); + this.clusterReadEndpoint = new Endpoint(cluster.attrReadEndpointAddress, portAttribute); + this.connections = new ec2.Connections({ + securityGroups: this.securityGroups, + defaultPort: ec2.Port.tcp(this.clusterEndpoint.port), + }); - const instanceProps = props.instanceProps; - // Get the actual subnet objects so we can depend on internet connectivity. - const internetConnected = instanceProps.vpc.selectSubnets(instanceProps.vpcSubnets).internetConnectivityEstablished; - - let monitoringRole; - if (props.monitoringInterval && props.monitoringInterval.toSeconds()) { - monitoringRole = props.monitoringRole || new Role(this, 'MonitoringRole', { - assumedBy: new ServicePrincipal('monitoring.rds.amazonaws.com'), - managedPolicies: [ - ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonRDSEnhancedMonitoringRole'), - ], - }); + this.setRemovalPolicy(cluster, props.removalPolicy); + + setLogRetention(this, props); + createInstances(this, props, this.subnetGroup); + } +} + +/** + * Sets up CloudWatch log retention if configured. + * A function rather than protected member to prevent exposing ``DatabaseClusterBaseProps``. + */ +function setLogRetention(cluster: DatabaseClusterNew, props: DatabaseClusterBaseProps) { + if (props.cloudwatchLogsExports) { + const unsupportedLogTypes = props.cloudwatchLogsExports.filter(logType => !props.engine.supportedLogTypes.includes(logType)); + if (unsupportedLogTypes.length > 0) { + throw new Error(`Unsupported logs for the current engine type: ${unsupportedLogTypes.join(',')}`); } - const enablePerformanceInsights = instanceProps.enablePerformanceInsights - || instanceProps.performanceInsightRetention !== undefined || instanceProps.performanceInsightEncryptionKey !== undefined; - if (enablePerformanceInsights && instanceProps.enablePerformanceInsights === false) { - throw new Error('`enablePerformanceInsights` disabled, but `performanceInsightRetention` or `performanceInsightEncryptionKey` was set'); + if (props.cloudwatchLogsRetention) { + for (const log of props.cloudwatchLogsExports) { + new logs.LogRetention(cluster, `LogRetention${log}`, { + logGroupName: `/aws/rds/cluster/${cluster.clusterIdentifier}/${log}`, + retention: props.cloudwatchLogsRetention, + role: props.cloudwatchLogsRetentionRole, + }); + } } + } +} - const instanceType = instanceProps.instanceType ?? ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM); - const instanceParameterGroupConfig = instanceProps.parameterGroup?.bindToInstance({}); - for (let i = 0; i < instanceCount; i++) { - const instanceIndex = i + 1; - const instanceIdentifier = props.instanceIdentifierBase != null ? `${props.instanceIdentifierBase}${instanceIndex}` : - props.clusterIdentifier != null ? `${props.clusterIdentifier}instance${instanceIndex}` : - undefined; - - const publiclyAccessible = instanceProps.vpcSubnets && instanceProps.vpcSubnets.subnetType === ec2.SubnetType.PUBLIC; - - const instance = new CfnDBInstance(this, `Instance${instanceIndex}`, { - // Link to cluster - engine: props.engine.engineType, - engineVersion: props.engine.engineVersion?.fullVersion, - dbClusterIdentifier: cluster.ref, - dbInstanceIdentifier: instanceIdentifier, - // Instance properties - dbInstanceClass: databaseInstanceType(instanceType), - publiclyAccessible, - enablePerformanceInsights: enablePerformanceInsights || instanceProps.enablePerformanceInsights, // fall back to undefined if not set - performanceInsightsKmsKeyId: instanceProps.performanceInsightEncryptionKey?.keyArn, - performanceInsightsRetentionPeriod: enablePerformanceInsights - ? (instanceProps.performanceInsightRetention || PerformanceInsightRetention.DEFAULT) - : undefined, - // This is already set on the Cluster. Unclear to me whether it should be repeated or not. Better yes. - dbSubnetGroupName: subnetGroup.ref, - dbParameterGroupName: instanceParameterGroupConfig?.parameterGroupName, - monitoringInterval: props.monitoringInterval && props.monitoringInterval.toSeconds(), - monitoringRoleArn: monitoringRole && monitoringRole.roleArn, - }); +/** Output from the createInstances method; used to set instance identifiers and endpoints */ +interface InstanceConfig { + readonly instanceIdentifiers: string[]; + readonly instanceEndpoints: Endpoint[]; +} - // If removalPolicy isn't explicitly set, - // it's Snapshot for Cluster. - // Because of that, in this case, - // we can safely use the CFN default of Delete for DbInstances with dbClusterIdentifier set. - if (props.removalPolicy) { - instance.applyRemovalPolicy(props.removalPolicy); - } +/** + * Creates the instances for the cluster. + * A function rather than a protected method on ``DatabaseClusterNew`` to avoid exposing + * ``DatabaseClusterNew`` and ``DatabaseClusterBaseProps`` in the API. + */ +function createInstances(cluster: DatabaseClusterNew, props: DatabaseClusterBaseProps, subnetGroup: CfnDBSubnetGroup): InstanceConfig { + const instanceCount = props.instances != null ? props.instances : 2; + if (instanceCount < 1) { + throw new Error('At least one instance is required'); + } - // We must have a dependency on the NAT gateway provider here to create - // things in the right order. - instance.node.addDependency(internetConnected); + const instanceIdentifiers: string[] = []; + const instanceEndpoints: Endpoint[] = []; + const portAttribute = cluster.clusterEndpoint.port; + const instanceProps = props.instanceProps; + + // Get the actual subnet objects so we can depend on internet connectivity. + const internetConnected = instanceProps.vpc.selectSubnets(instanceProps.vpcSubnets).internetConnectivityEstablished; + + let monitoringRole; + if (props.monitoringInterval && props.monitoringInterval.toSeconds()) { + monitoringRole = props.monitoringRole || new Role(cluster, 'MonitoringRole', { + assumedBy: new ServicePrincipal('monitoring.rds.amazonaws.com'), + managedPolicies: [ + ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonRDSEnhancedMonitoringRole'), + ], + }); + } - this.instanceIdentifiers.push(instance.ref); - this.instanceEndpoints.push(new Endpoint(instance.attrEndpointAddress, portAttribute)); - } + const enablePerformanceInsights = instanceProps.enablePerformanceInsights + || instanceProps.performanceInsightRetention !== undefined || instanceProps.performanceInsightEncryptionKey !== undefined; + if (enablePerformanceInsights && instanceProps.enablePerformanceInsights === false) { + throw new Error('`enablePerformanceInsights` disabled, but `performanceInsightRetention` or `performanceInsightEncryptionKey` was set'); } - private setLogRetention(props: DatabaseClusterProps) { - if (props.cloudwatchLogsExports) { - const unsupportedLogTypes = props.cloudwatchLogsExports.filter(logType => !props.engine.supportedLogTypes.includes(logType)); - if (unsupportedLogTypes.length > 0) { - throw new Error(`Unsupported logs for the current engine type: ${unsupportedLogTypes.join(',')}`); - } + const instanceType = instanceProps.instanceType ?? ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM); + const instanceParameterGroupConfig = instanceProps.parameterGroup?.bindToInstance({}); + for (let i = 0; i < instanceCount; i++) { + const instanceIndex = i + 1; + const instanceIdentifier = props.instanceIdentifierBase != null ? `${props.instanceIdentifierBase}${instanceIndex}` : + props.clusterIdentifier != null ? `${props.clusterIdentifier}instance${instanceIndex}` : + undefined; - if (props.cloudwatchLogsRetention) { - for (const log of props.cloudwatchLogsExports) { - new logs.LogRetention(this, `LogRetention${log}`, { - logGroupName: `/aws/rds/cluster/${this.clusterIdentifier}/${log}`, - retention: props.cloudwatchLogsRetention, - role: props.cloudwatchLogsRetentionRole, - }); - } - } + const publiclyAccessible = instanceProps.vpcSubnets && instanceProps.vpcSubnets.subnetType === ec2.SubnetType.PUBLIC; + + const instance = new CfnDBInstance(cluster, `Instance${instanceIndex}`, { + // Link to cluster + engine: props.engine.engineType, + engineVersion: props.engine.engineVersion?.fullVersion, + dbClusterIdentifier: cluster.clusterIdentifier, + dbInstanceIdentifier: instanceIdentifier, + // Instance properties + dbInstanceClass: databaseInstanceType(instanceType), + publiclyAccessible, + enablePerformanceInsights: enablePerformanceInsights || instanceProps.enablePerformanceInsights, // fall back to undefined if not set + performanceInsightsKmsKeyId: instanceProps.performanceInsightEncryptionKey?.keyArn, + performanceInsightsRetentionPeriod: enablePerformanceInsights + ? (instanceProps.performanceInsightRetention || PerformanceInsightRetention.DEFAULT) + : undefined, + // This is already set on the Cluster. Unclear to me whether it should be repeated or not. Better yes. + dbSubnetGroupName: subnetGroup.ref, + dbParameterGroupName: instanceParameterGroupConfig?.parameterGroupName, + monitoringInterval: props.monitoringInterval && props.monitoringInterval.toSeconds(), + monitoringRoleArn: monitoringRole && monitoringRole.roleArn, + }); + + // If removalPolicy isn't explicitly set, + // it's Snapshot for Cluster. + // Because of that, in this case, + // we can safely use the CFN default of Delete for DbInstances with dbClusterIdentifier set. + if (props.removalPolicy) { + instance.applyRemovalPolicy(props.removalPolicy); } + + // We must have a dependency on the NAT gateway provider here to create + // things in the right order. + instance.node.addDependency(internetConnected); + + instanceIdentifiers.push(instance.ref); + instanceEndpoints.push(new Endpoint(instance.attrEndpointAddress, portAttribute)); } + + return { instanceEndpoints, instanceIdentifiers }; } /** diff --git a/packages/@aws-cdk/aws-rds/package.json b/packages/@aws-cdk/aws-rds/package.json index 7c65dfad99e40..90a92079eb6e5 100644 --- a/packages/@aws-cdk/aws-rds/package.json +++ b/packages/@aws-cdk/aws-rds/package.json @@ -105,6 +105,7 @@ "exclude": [ "props-physical-name:@aws-cdk/aws-rds.ParameterGroupProps", "props-physical-name:@aws-cdk/aws-rds.DatabaseClusterProps", + "props-physical-name:@aws-cdk/aws-rds.DatabaseClusterFromSnapshotProps", "props-physical-name:@aws-cdk/aws-rds.DatabaseInstanceProps", "props-physical-name:@aws-cdk/aws-rds.DatabaseInstanceFromSnapshotProps", "props-physical-name:@aws-cdk/aws-rds.DatabaseInstanceReadReplicaProps", diff --git a/packages/@aws-cdk/aws-rds/test/test.cluster.ts b/packages/@aws-cdk/aws-rds/test/test.cluster.ts index 09637a8deea1c..fa8480e000f5d 100644 --- a/packages/@aws-cdk/aws-rds/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-rds/test/test.cluster.ts @@ -6,7 +6,10 @@ import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; -import { AuroraMysqlEngineVersion, AuroraPostgresEngineVersion, DatabaseCluster, DatabaseClusterEngine, ParameterGroup, PerformanceInsightRetention } from '../lib'; +import { + AuroraEngineVersion, AuroraMysqlEngineVersion, AuroraPostgresEngineVersion, DatabaseCluster, DatabaseClusterEngine, + DatabaseClusterFromSnapshot, ParameterGroup, PerformanceInsightRetention, +} from '../lib'; export = { 'creating a Cluster also creates 2 DB Instances'(test: Test) { @@ -1310,6 +1313,37 @@ export = { test.done(); }, + + 'create a cluster from a snapshot'(test: Test) { + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseClusterFromSnapshot(stack, 'Database', { + engine: DatabaseClusterEngine.aurora({ version: AuroraEngineVersion.VER_1_22_2 }), + instanceProps: { + vpc, + }, + snapshotIdentifier: 'mySnapshot', + }); + + // THEN + expect(stack).to(haveResource('AWS::RDS::DBCluster', { + Properties: { + Engine: 'aurora', + EngineVersion: '5.6.mysql_aurora.1.22.2', + DBSubnetGroupName: { Ref: 'DatabaseSubnets56F17B9A' }, + VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId'] }], + SnapshotIdentifier: 'mySnapshot', + }, + DeletionPolicy: ABSENT, + UpdateReplacePolicy: 'Snapshot', + }, ResourcePart.CompleteDefinition)); + + expect(stack).to(countResources('AWS::RDS::DBInstance', 2)); + + test.done(); + }, }; function testStack() {