diff --git a/.gitignore b/.gitignore index f8f8e687c6791..2cb016405e016 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # VSCode extension -.vscode/ + +# Store launch config in repo but not settings +.vscode/settings.json /.favorites.json # TypeScript incremental build states diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000000..66f6db80dcd14 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + // Has convenient settings for attaching to a NodeJS process for debugging purposes + // that are NOT the default and otherwise every developers has to configure for + // themselves again and again. + "type": "node", + "request": "attach", + "name": "Attach to NodeJS", + // If we don't do this, every step-into into an async function call will go into + // NodeJS internals which are hard to step out of. + "skipFiles": [ + "/**" + ], + // Saves some button-pressing latency on attaching + "stopOnEntry": false + } + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0386d2dbfb9a6..9e4e7fd080d53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,57 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.42.1](https://github.com/aws/aws-cdk/compare/v1.42.0...v1.42.1) (2020-06-01) + + +### Bug Fixes + +* **lambda:** `SingletonFunction.grantInvoke()` API fails with error 'No child with id' ([#8296](https://github.com/aws/aws-cdk/issues/8296)) ([b4e264c](https://github.com/aws/aws-cdk/commit/b4e264c024bc58053412be1343bed6458628f7cb)), closes [#8240](https://github.com/aws/aws-cdk/issues/8240) + +## [1.42.0](https://github.com/aws/aws-cdk/compare/v1.41.0...v1.42.0) (2020-05-27) + + +### ⚠ BREAKING CHANGES + +* **cloudtrail:** API signatures of `addS3EventSelectors` and +`addLambdaEventSelectors` have changed. Their parameters are now +strongly typed to accept `IBucket` and `IFunction` respectively. +* **cloudtrail:** `addS3EventSelectors` and `addLambdaEventSelectors` +can no longer be used to configure all S3 data events or all Lambda data +events. Two new APIs `logAllS3DataEvents()` and +`logAllLambdaDataEvents()` have been introduced to achieve this. +* **cloudtrail:** The property `snsTopic` is now of the type `ITopic`. + +### Features + +* **cfnspec:** cloudformation spec v14.4.0 ([#8195](https://github.com/aws/aws-cdk/issues/8195)) ([99e7330](https://github.com/aws/aws-cdk/commit/99e7330fc5fc140964c47d8c6dbaee2b46b382e1)) +* **cloudtrail:** create cloudwatch event without needing to create a Trail ([#8076](https://github.com/aws/aws-cdk/issues/8076)) ([0567a23](https://github.com/aws/aws-cdk/commit/0567a2360ac713e3171c9a82767611174dadb6c6)), closes [#6716](https://github.com/aws/aws-cdk/issues/6716) +* **cloudtrail:** user specified log group ([#8079](https://github.com/aws/aws-cdk/issues/8079)) ([0a3785b](https://github.com/aws/aws-cdk/commit/0a3785b7626633fcbdf26ab793c70f2bc017314b)), closes [#6162](https://github.com/aws/aws-cdk/issues/6162) +* **codeguruprofiler:** ProfilingGroup ([#7895](https://github.com/aws/aws-cdk/issues/7895)) ([995088a](https://github.com/aws/aws-cdk/commit/995088abb00d9c75adbb65845998a8328bb5ba14)) +* **codepipeline:** use a special bootstrapless synthesizer for cross-region support Stacks ([#8091](https://github.com/aws/aws-cdk/issues/8091)) ([575f1db](https://github.com/aws/aws-cdk/commit/575f1db0474327c61c4ac626608c9f443ce231d2)), closes [#8082](https://github.com/aws/aws-cdk/issues/8082) +* **cognito:** user pool - case sensitivity for sign in ([460394f](https://github.com/aws/aws-cdk/commit/460394f3dc4737cee80504d6c8ef106ecc3b67d5)), closes [#7988](https://github.com/aws/aws-cdk/issues/7988) [#7235](https://github.com/aws/aws-cdk/issues/7235) +* **core:** CfnJson enables intrinsics in hash keys ([#8099](https://github.com/aws/aws-cdk/issues/8099)) ([195cd40](https://github.com/aws/aws-cdk/commit/195cd405d9f0869875de2ec78661aee3af2c7c7d)), closes [#8084](https://github.com/aws/aws-cdk/issues/8084) +* **eks:** improve security using IRSA conditions ([#8084](https://github.com/aws/aws-cdk/issues/8084)) ([35a01a0](https://github.com/aws/aws-cdk/commit/35a01a079af40da291007da08af6690c9a81c101)) +* **elbv2:** Supports new types of listener rule conditions ([#7848](https://github.com/aws/aws-cdk/issues/7848)) ([3d30ffa](https://github.com/aws/aws-cdk/commit/3d30ffa38c51ae26686287e993af445ea3067766)), closes [#3888](https://github.com/aws/aws-cdk/issues/3888) +* **secretsmanager:** adds grantWrite to Secret ([#7858](https://github.com/aws/aws-cdk/issues/7858)) ([3fed84b](https://github.com/aws/aws-cdk/commit/3fed84ba9eec3f53c662966e366aa629209b7bf5)) +* **sns:** add support for subscription DLQ in SNS ([383cdb8](https://github.com/aws/aws-cdk/commit/383cdb86effeafdf5d0767ed379b16b3d78a933b)) +* **stepfunctions:** new service integration classes for Lambda, SNS, and SQS ([#7946](https://github.com/aws/aws-cdk/issues/7946)) ([c038848](https://github.com/aws/aws-cdk/commit/c0388483524832ca7863de4ee9c472b8ab39de8e)), closes [#6715](https://github.com/aws/aws-cdk/issues/6715) [#6489](https://github.com/aws/aws-cdk/issues/6489) +* **stepfunctions:** support paths in Pass state ([#8070](https://github.com/aws/aws-cdk/issues/8070)) ([86eac6a](https://github.com/aws/aws-cdk/commit/86eac6af074bf78a921c52d613eca0dd4a514a49)), closes [#7181](https://github.com/aws/aws-cdk/issues/7181) +* **stepfunctions-tasks:** task for starting a job run in AWS Glue ([#8143](https://github.com/aws/aws-cdk/issues/8143)) ([a721e67](https://github.com/aws/aws-cdk/commit/a721e670cdc9888cd67ef1a24021004e18bfd23c)) + + +### Bug Fixes + +* **apigateway:** contextAccountId in AccessLogField incorrectly resolves to requestId ([7b89e80](https://github.com/aws/aws-cdk/commit/7b89e805c716fa73d41cc97fcb728634e7a59136)), closes [#7952](https://github.com/aws/aws-cdk/issues/7952) [#7951](https://github.com/aws/aws-cdk/issues/7951) +* **autoscaling:** add noDevice as a volume type ([#7253](https://github.com/aws/aws-cdk/issues/7253)) ([751958b](https://github.com/aws/aws-cdk/commit/751958b69225fdfc52622781c618f5a77f881fb6)), closes [#7242](https://github.com/aws/aws-cdk/issues/7242) +* **aws-eks:** kubectlEnabled: false conflicts with addNodegroup ([#8119](https://github.com/aws/aws-cdk/issues/8119)) ([8610889](https://github.com/aws/aws-cdk/commit/86108890a51443dc06ec6325038c7b19cbdaee76)), closes [#7993](https://github.com/aws/aws-cdk/issues/7993) +* **cli:** paper cuts ([#8164](https://github.com/aws/aws-cdk/issues/8164)) ([af2ea60](https://github.com/aws/aws-cdk/commit/af2ea60e7ae4aaab17ddd10a9142e1809b4c8246)) +* **dynamodb:** the maximum number of nonKeyAttributes is 100, not 20 ([#8186](https://github.com/aws/aws-cdk/issues/8186)) ([0393528](https://github.com/aws/aws-cdk/commit/03935280f1addef392c9b4460737cce8bb2eb8c9)), closes [#8095](https://github.com/aws/aws-cdk/issues/8095) +* **eks:** unable to add multiple service accounts ([#8122](https://github.com/aws/aws-cdk/issues/8122)) ([524440c](https://github.com/aws/aws-cdk/commit/524440c5454d15276c92581a08d4ee7cad1790eb)) +* **events:** cannot use the same target account for 2 cross-account event sources ([#8068](https://github.com/aws/aws-cdk/issues/8068)) ([395c07c](https://github.com/aws/aws-cdk/commit/395c07c0cac7739743fc71d71fddd8880b608ead)), closes [#8010](https://github.com/aws/aws-cdk/issues/8010) +* **lambda-nodejs:** build fails on Windows ([#8140](https://github.com/aws/aws-cdk/issues/8140)) ([04490b1](https://github.com/aws/aws-cdk/commit/04490b134a05ec34523541a3ca282ba8957a7964)), closes [#8107](https://github.com/aws/aws-cdk/issues/8107) +* **cloudtrail:** better typed event selector apis ([#8097](https://github.com/aws/aws-cdk/issues/8097)) ([0028778](https://github.com/aws/aws-cdk/commit/0028778c0f00f2faa8dad25345cd17f311fad5da)) + ## [1.41.0](https://github.com/aws/aws-cdk/compare/v1.40.0...v1.41.0) (2020-05-21) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ad4328ebde45a..810267a9ab428 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,6 +43,7 @@ and let us know if it's not up-to-date (even better, submit a PR with your corr - [Troubleshooting](#troubleshooting) - [Debugging](#debugging) - [Connecting the VS Code Debugger](#connecting-the-vs-code-debugger) + - [Run a CDK unit test in the debugger](#run-a-cdk-unit-test-in-the-debugger) - [Related Repositories](#related-repositories) ## Getting Started @@ -234,7 +235,7 @@ BREAKING CHANGE: Description of what broke and how to achieve this behavior now ### Step 5: Pull Request * Push to a GitHub fork or to a branch (naming convention: `/`) -* Submit a Pull Requests on GitHub and assign the PR for a review to the "awslabs/aws-cdk" team. +* Submit a Pull Request on GitHub. A reviewer will later be assigned by the maintainers. * Please follow the PR checklist written below. We trust our contributors to self-check, and this helps that process! * Discuss review comments and iterate until you get at least one “Approve”. When iterating, push new commits to the same branch. Usually all these are going to be squashed when you merge to master. The commit messages should be hints @@ -327,7 +328,7 @@ All packages in the repo use a standard base configuration found at [eslintrc.js This can be customized for any package by modifying the `.eslintrc` file found at its root. If you're using the VS Code and would like to see eslint violations on it, install the [eslint -extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint). +extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint). #### pkglint @@ -910,6 +911,24 @@ To debug your CDK application along with the CDK repository, 6. The debug view, should now have a launch configuration called 'Debug hello-cdk' and launching that will start the debugger. 7. Any time you modify the CDK app or any of the CDK modules, they need to be re-built and depending on the change the `link-all.sh` script from step#2, may need to be re-run. Only then, would VS code recognize the change and potentially the breakpoint. +### Run a CDK unit test in the debugger + +If you want to run the VSCode debugger on unit tests of the CDK project +itself, do the following: + +1. Set a breakpoint inside your unit test. +2. In your terminal, depending on the type of test, run either: + +``` +# (For tests names test.xxx.ts) +$ node --inspect-brk /path/to/aws-cdk/node_modules/.bin/nodeunit -t 'TESTNAME' + +# (For tests names xxxx.test.ts) +$ node --inspect-brk /path/to/aws-cdk/node_modules/.bin/jest -i -t 'TESTNAME' +``` + +3. On the `Run` pane of VSCode, select the run configuration **Attach to NodeJS** and click the button. + ## Related Repositories * [Samples](https://github.com/aws-samples/aws-cdk-examples): includes sample code in multiple languages diff --git a/lerna.json b/lerna.json index baa3940d032fb..ed6fe98880477 100644 --- a/lerna.json +++ b/lerna.json @@ -10,5 +10,5 @@ "tools/*" ], "rejectCycles": "true", - "version": "1.41.0" + "version": "1.42.1" } diff --git a/package.json b/package.json index 1314742465e47..6a465930f912c 100644 --- a/package.json +++ b/package.json @@ -48,25 +48,34 @@ "nohoist": [ "**/jszip", "**/jszip/**", - "@aws-cdk/cdk-assets-schema/semver", - "@aws-cdk/cdk-assets-schema/semver/**", - "@aws-cdk/core/minimatch", - "@aws-cdk/core/minimatch/**", - "@aws-cdk/cloudformation-include/yaml", - "@aws-cdk/cloudformation-include/yaml/**", "@aws-cdk/aws-codepipeline-actions/case", "@aws-cdk/aws-codepipeline-actions/case/**", "@aws-cdk/aws-ecr-assets/minimatch", "@aws-cdk/aws-ecr-assets/minimatch/**", "@aws-cdk/aws-lambda-nodejs/parcel-bundler", "@aws-cdk/aws-lambda-nodejs/parcel-bundler/**", + "@aws-cdk/cdk-assets-schema/semver", + "@aws-cdk/cdk-assets-schema/semver/**", "@aws-cdk/cloud-assembly-schema/jsonschema", "@aws-cdk/cloud-assembly-schema/jsonschema/**", "@aws-cdk/cloud-assembly-schema/semver", "@aws-cdk/cloud-assembly-schema/semver/**", + "@aws-cdk/cloudformation-include/yaml", + "@aws-cdk/cloudformation-include/yaml/**", + "@aws-cdk/core/minimatch", + "@aws-cdk/core/minimatch/**", "@aws-cdk/cx-api/semver", "@aws-cdk/cx-api/semver/**", - "@aws-cdk/cx-api/semver/**" + "monocdk-experiment/case", + "monocdk-experiment/case/**", + "monocdk-experiment/jsonschema", + "monocdk-experiment/jsonschema/**", + "monocdk-experiment/minimatch", + "monocdk-experiment/minimatch/**", + "monocdk-experiment/semver", + "monocdk-experiment/semver/**", + "monocdk-experiment/yaml", + "monocdk-experiment/yaml/**" ] } } diff --git a/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts b/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts index 713b662808ad4..03685cfc9c413 100644 --- a/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts +++ b/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts @@ -32,6 +32,13 @@ export interface PipelineDeployStackActionProps { */ readonly createChangeSetRunOrder?: number; + /** + * The name of the CodePipeline action creating the ChangeSet. + * + * @default 'ChangeSet' + */ + readonly createChangeSetActionName?: string; + /** * The runOrder for the CodePipeline action executing the ChangeSet. * @@ -39,6 +46,13 @@ export interface PipelineDeployStackActionProps { */ readonly executeChangeSetRunOrder?: number; + /** + * The name of the CodePipeline action creating the ChangeSet. + * + * @default 'Execute' + */ + readonly executeChangeSetActionName?: string; + /** * IAM role to assume when deploying changes. * @@ -116,7 +130,7 @@ export class PipelineDeployStackAction implements codepipeline.IAction { const changeSetName = props.changeSetName || 'CDK-CodePipeline-ChangeSet'; const capabilities = cfnCapabilities(props.adminPermissions, props.capabilities); this.prepareChangeSetAction = new cpactions.CloudFormationCreateReplaceChangeSetAction({ - actionName: 'ChangeSet', + actionName: props.createChangeSetActionName ?? 'ChangeSet', changeSetName, runOrder: createChangeSetRunOrder, stackName: props.stack.stackName, @@ -126,7 +140,7 @@ export class PipelineDeployStackAction implements codepipeline.IAction { capabilities, }); this.executeChangeSetAction = new cpactions.CloudFormationExecuteChangeSetAction({ - actionName: 'Execute', + actionName: props.executeChangeSetActionName ?? 'Execute', changeSetName, runOrder: executeChangeSetRunOrder, stackName: this.stack.stackName, diff --git a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts b/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts index d765eb887c14a..918279b480b30 100644 --- a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts +++ b/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts @@ -1,4 +1,4 @@ -import { expect, haveResource, isSuperObject } from '@aws-cdk/assert'; +import { expect, haveResource, haveResourceLike, isSuperObject } from '@aws-cdk/assert'; import * as cfn from '@aws-cdk/aws-cloudformation'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; @@ -406,6 +406,43 @@ export = nodeunit.testCase({ ); test.done(); }, + + 'allows overriding the ChangeSet and Execute action names'(test: nodeunit.Test) { + const stack = getTestStack(); + const selfUpdatingPipeline = createSelfUpdatingStack(stack); + selfUpdatingPipeline.pipeline.addStage({ + stageName: 'Deploy', + actions: [ + new PipelineDeployStackAction({ + input: selfUpdatingPipeline.synthesizedApp, + adminPermissions: true, + stack, + createChangeSetActionName: 'Prepare', + executeChangeSetActionName: 'Deploy', + }), + ], + }); + + expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: [ + {}, + {}, + { + Name: 'Deploy', + Actions: [ + { + Name: 'Prepare', + }, + { + Name: 'Deploy', + }, + ], + }, + ], + })); + + test.done(); + }, }); class FakeAction implements codepipeline.IAction { diff --git a/packages/@aws-cdk/assets/package.json b/packages/@aws-cdk/assets/package.json index 7892cc9fc6801..92cae774b8353 100644 --- a/packages/@aws-cdk/assets/package.json +++ b/packages/@aws-cdk/assets/package.json @@ -65,7 +65,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "@types/sinon": "^9.0.3", + "@types/sinon": "^9.0.4", "aws-cdk": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts index 419078f88aebc..c72dba724f878 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts @@ -1,7 +1,7 @@ import { CfnResource, Construct, Lazy, RemovalPolicy, Resource, Stack } from '@aws-cdk/core'; import * as crypto from 'crypto'; import { CfnDeployment } from './apigateway.generated'; -import { IRestApi, RestApi } from './restapi'; +import { IRestApi, RestApi, SpecRestApi } from './restapi'; export interface DeploymentProps { /** @@ -155,7 +155,7 @@ class LatestDeploymentResource extends CfnDeployment { * add via `addToLogicalId`. */ protected prepare() { - if (this.api instanceof RestApi) { // Ignore IRestApi that are imported + if (this.api instanceof RestApi || this.api instanceof SpecRestApi) { // Ignore IRestApi that are imported // Add CfnRestApi to the logical id so a new deployment is triggered when any of its properties change. const cfnRestApiCF = (this.api.node.defaultChild as any)._toCloudFormation(); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.expected.json index 71dd02f17ab9a..bcf74c12601fa 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.expected.json @@ -44,7 +44,7 @@ "Name": "my-api" } }, - "myapiDeployment92F2CB49": { + "myapiDeployment92F2CB49eb6b0027bfbdb20b09988607569e06bd": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { @@ -60,7 +60,7 @@ "Ref": "myapi4C7BF186" }, "DeploymentId": { - "Ref": "myapiDeployment92F2CB49" + "Ref": "myapiDeployment92F2CB49eb6b0027bfbdb20b09988607569e06bd" }, "StageName": "prod" } diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.expected.json index 3eaae1ff8fd58..e319d4fb28ccd 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.expected.json @@ -53,7 +53,7 @@ "Name": "my-api" } }, - "myapiDeployment92F2CB49": { + "myapiDeployment92F2CB49a59bca458e4fac1fcd742212ded42a65": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { @@ -69,7 +69,7 @@ "Ref": "myapi4C7BF186" }, "DeploymentId": { - "Ref": "myapiDeployment92F2CB49" + "Ref": "myapiDeployment92F2CB49a59bca458e4fac1fcd742212ded42a65" }, "StageName": "prod" } diff --git a/packages/@aws-cdk/aws-cloudtrail/README.md b/packages/@aws-cdk/aws-cloudtrail/README.md index 313f425125fb3..541c926cab6fb 100644 --- a/packages/@aws-cdk/aws-cloudtrail/README.md +++ b/packages/@aws-cdk/aws-cloudtrail/README.md @@ -13,102 +13,82 @@ --- -Add a CloudTrail construct - for ease of setting up CloudTrail logging in your account +## Trail -Example usage: +AWS CloudTrail enables governance, compliance, and operational and risk auditing of your AWS account. Actions taken by +a user, role, or an AWS service are recorded as events in CloudTrail. Learn more at the [CloudTrail +documentation](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-user-guide.html). -```ts -import * as cloudtrail from '@aws-cdk/aws-cloudtrail'; +The `Trail` construct enables ongoing delivery of events as log files to an Amazon S3 bucket. Learn more about [Creating +a Trail for Your AWS Account](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-create-and-update-a-trail.html). +The following code creates a simple CloudTrail for your account - +```ts const trail = new cloudtrail.Trail(this, 'CloudTrail'); ``` -You can instantiate the CloudTrail construct with no arguments - this will by default: +By default, this will create a new S3 Bucket that CloudTrail will write to, and choose a few other reasonable defaults +such as turning on multi-region and global service events. +The defaults for each property and how to override them are all documented on the `TrailProps` interface. - * Create a new S3 Bucket and associated Policy that allows CloudTrail to write to it - * Create a CloudTrail with the following configuration: - * Logging Enabled - * Log file validation enabled - * Multi Region set to true - * Global Service Events set to true - * The created S3 bucket - * CloudWatch Logging Disabled - * No SNS configuartion - * No tags - * No fixed name +## Log File Validation -You can override any of these properties using the `CloudTrailProps` configuraiton object. +In order to validate that the CloudTrail log file was not modified after CloudTrail delivered it, CloudTrail provides a +digital signature for each file. Learn more at [Validating CloudTrail Log File +Integrity](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-log-file-validation-intro.html). -For example, to log to CloudWatch Logs +This is enabled on the `Trail` construct by default, but can be turned off by setting `enableFileValidation` to `false`. ```ts - -import * as cloudtrail from '@aws-cdk/aws-cloudtrail'; - const trail = new cloudtrail.Trail(this, 'CloudTrail', { - sendToCloudWatchLogs: true + enableFileValidation: false, }); ``` -This creates the same setup as above - but also logs events to a created CloudWatch Log stream. -By default, the created log group has a retention period of 365 Days, but this is also configurable -via the `cloudWatchLogsRetention` property. If you would like to specify the log group explicitly, -use the `cloudwatchLogGroup` property. +## Notifications -For using CloudTrail event selector to log specific S3 events, -you can use the `CloudTrailProps` configuration object. -Example: +Amazon SNS notifications can be configured upon new log files containing Trail events are delivered to S3. +Learn more at [Configuring Amazon SNS Notifications for +CloudTrail](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/configure-sns-notifications-for-cloudtrail.html). +The following code configures an SNS topic to be notified - ```ts -import * as cloudtrail from '@aws-cdk/aws-cloudtrail'; +const topic = new sns.Topic(this, 'TrailTopic'); +const trail = new cloudtrail.Trail(this, 'CloudTrail', { + snsTopic: topic, +}); +``` -const trail = new cloudtrail.Trail(this, 'MyAmazingCloudTrail'); +## Service Integrations -// Adds an event selector to the bucket magic-bucket. -// By default, this includes management events and all operations (Read + Write) -trail.addS3EventSelector(["arn:aws:s3:::magic-bucket/"]); +Besides sending trail events to S3, they can also be configured to notify other AWS services - -// Adds an event selector to the bucket foo, with a specific configuration -trail.addS3EventSelector(["arn:aws:s3:::foo/"], { - includeManagementEvents: false, - readWriteType: ReadWriteType.ALL, -}); -``` +### Amazon CloudWatch Logs -For using CloudTrail event selector to log events about Lambda -functions, you can use `addLambdaEventSelector`. +CloudTrail events can be delivered to a CloudWatch Logs LogGroup. By default, a new LogGroup is created with a +default retention setting. The following code enables sending CloudWatch logs but specifies a particular retention +period for the created Log Group. ```ts -import * as cloudtrail from '@aws-cdk/aws-cloudtrail'; -import * as lambda from '@aws-cdk/aws-lambda'; - -const trail = new cloudtrail.Trail(this, 'MyAmazingCloudTrail'); -const lambdaFunction = new lambda.Function(stack, 'AnAmazingFunction', { - runtime: lambda.Runtime.NODEJS_10_X, - handler: "hello.handler", - code: lambda.Code.fromAsset("lambda"), +const trail = new cloudtrail.Trail(this, 'CloudTrail', { + sendToCloudWatchLogs: true, + cloudWatchLogsRetention: logs.RetentionDays.FOUR_MONTHS, }); +``` -// Add an event selector to log data events for all functions in the account. -trail.addLambdaEventSelector(["arn:aws:lambda"]); +If you would like to use a specific log group instead, this can be configured via `cloudwatchLogGroup`. -// Add an event selector to log data events for the provided Lambda functions. -trail.addLambdaEventSelector([lambdaFunction.functionArn]); -``` +### Amazon EventBridge -Using the `Trail.onEvent()` API, an EventBridge rule can be created that gets triggered for -every event logged in CloudTrail. -To only use the events that are of interest, either from a particular service, specific account or -time range, they can be filtered down using the APIs available in `aws-events`. The following code -filters events for S3 from a specific AWS account and triggers a lambda function. See [Events delivered via +Amazon EventBridge rules can be configured to be triggered when CloudTrail events occur using the `Trail.onEvent()` API. +Using APIs available in `aws-events`, these events can be filtered to match to those that are of interest, either from +a specific service, account or time range. See [Events delivered via CloudTrail](https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/EventTypes.html#events-for-services-not-listed) to learn more about the event structure for events from CloudTrail. -```ts -import * as cloudtrail from '@aws-cdk/aws-cloudtrail'; -import * as eventTargets from '@aws-cdk/aws-events-targets'; -import * as lambda from '@aws-cdk/aws-lambda'; +The following code filters events for S3 from a specific AWS account and triggers a lambda function. +```ts const myFunctionHandler = new lambda.Function(this, 'MyFunction', { code: lambda.Code.fromAsset('resource/myfunction'); runtime: lambda.Runtime.NODEJS_12_X, @@ -124,3 +104,84 @@ eventRule.addEventPattern({ source: 'aws.s3', }); ``` + +## Multi-Region & Global Service Events + +By default, a `Trail` is configured to deliver log files from multiple regions to a single S3 bucket for a given +account. This creates shadow trails (replication of the trails) in all of the other regions. Learn more about [How +CloudTrail Behaves Regionally](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-concepts.html#cloudtrail-concepts-regional-and-global-services) +and about the [`IsMultiRegion` +property](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudtrail-trail.html#cfn-cloudtrail-trail-ismultiregiontrail). + +For most services, events are recorded in the region where the action occurred. For global services such as AWS IAM, +AWS STS, Amazon CloudFront, Route 53, etc., events are delivered to any trail that includes global services. Learn more +[About Global Service Events](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-concepts.html#cloudtrail-concepts-global-service-events). + +Events for global services are turned on by default for `Trail` constructs in the CDK. + +The following code disables multi-region trail delivery and trail delivery for global services for a specific `Trail` - + +```ts +const trail = new cloudtrail.Trail(this, 'CloudTrail', { + // ... + isMultiRegionTrail: false, + includeGlobalServiceEvents: false, +}); +``` + +## Events Types + +**Management events** provide information about management operations that are performed on resources in your AWS +account. These are also known as control plane operations. Learn more about [Management +Events](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-concepts.html#cloudtrail-concepts-events). + +By default, a `Trail` logs all management events. However, they can be configured to either be turned off, or to only +log 'Read' or 'Write' events. + +The following code configures the `Trail` to only track management events that are of type 'Read'. + +```ts +const trail = new cloudtrail.Trail(this, 'CloudTrail', { + // ... + managementEvents: ReadWriteType.READ_ONLY, +}); +``` + +**Data events** provide information about the resource operations performed on or in a resource. These are also known +as data plane operations. Learn more about [Data +Events](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-concepts.html#cloudtrail-concepts-events). +By default, no data events are logged for a `Trail`. + +AWS CloudTrail supports data event logging for Amazon S3 objects and AWS Lambda functions. + +The `logAllS3DataEvents()` API configures the trail to log all S3 data events while the `addS3EventSelector()` API can +be used to configure logging of S3 data events for specific buckets and specific object prefix. The following code +configures logging of S3 data events for `fooBucket` and with object prefix `bar/`. + +```ts +import * as cloudtrail from '@aws-cdk/aws-cloudtrail'; + +const trail = new cloudtrail.Trail(this, 'MyAmazingCloudTrail'); + +// Adds an event selector to the bucket foo +trail.addS3EventSelector([{ + bucket: fooBucket, // 'fooBucket' is of type s3.IBucket + objectPrefix: 'bar/', +}]); +``` + +Similarly, the `logAllLambdaDataEvents()` configures the trail to log all Lambda data events while the +`addLambdaEventSelector()` API can be used to configure logging for specific Lambda functions. The following code +configures logging of Lambda data events for a specific Function. + +```ts +const trail = new cloudtrail.Trail(this, 'MyAmazingCloudTrail'); +const amazingFunction = new lambda.Function(stack, 'AnAmazingFunction', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: "hello.handler", + code: lambda.Code.fromAsset("lambda"), +}); + +// Add an event selector to log data events for the provided Lambda functions. +trail.addLambdaEventSelector([ lambdaFunction ]); +``` \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts b/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts index 471587a0696ef..3b3f39d64eb4c 100644 --- a/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts +++ b/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts @@ -1,8 +1,10 @@ import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; +import * as lambda from '@aws-cdk/aws-lambda'; import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; +import * as sns from '@aws-cdk/aws-sns'; import { Construct, Resource, Stack } from '@aws-cdk/core'; import { CfnTrail } from './cloudtrail.generated'; @@ -39,7 +41,7 @@ export interface TrailProps { * * @param managementEvents the management configuration type to log * - * @default - Management events will not be logged. + * @default ReadWriteType.ALL */ readonly managementEvents?: ReadWriteType; @@ -82,11 +84,11 @@ export interface TrailProps { */ readonly kmsKey?: kms.IKey; - /** The name of an Amazon SNS topic that is notified when new log files are published. + /** SNS topic that is notified when new log files are published. * * @default - No notifications. */ - readonly snsTopic?: string; // TODO: fix to use L2 SNS + readonly snsTopic?: sns.ITopic; /** * The name of the trail. We recoomend customers do not set an explicit name. @@ -105,7 +107,7 @@ export interface TrailProps { * * @default - if not supplied a bucket will be created with all the correct permisions */ - readonly bucket?: s3.IBucket + readonly bucket?: s3.IBucket; } /** @@ -129,7 +131,12 @@ export enum ReadWriteType { /** * All events */ - ALL = 'All' + ALL = 'All', + + /** + * No events + */ + NONE = 'None', } /** @@ -233,10 +240,17 @@ export class Trail extends Resource { } if (props.managementEvents) { - const managementEvent = { - includeManagementEvents: true, - readWriteType: props.managementEvents, - }; + let managementEvent; + if (props.managementEvents === ReadWriteType.NONE) { + managementEvent = { + includeManagementEvents: false, + }; + } else { + managementEvent = { + includeManagementEvents: true, + readWriteType: props.managementEvents, + }; + } this.eventSelectors.push(managementEvent); } @@ -252,7 +266,7 @@ export class Trail extends Resource { s3KeyPrefix: props.s3KeyPrefix, cloudWatchLogsLogGroupArn: this.logGroup?.logGroupArn, cloudWatchLogsRoleArn: logsRole?.roleArn, - snsTopicName: props.snsTopic, + snsTopicName: props.snsTopic?.topicName, eventSelectors: this.eventSelectors, }); @@ -316,13 +330,24 @@ export class Trail extends Resource { * Data events: These events provide insight into the resource operations performed on or within a resource. * These are also known as data plane operations. * - * @param dataResourceValues the list of data resource ARNs to include in logging (maximum 250 entries). + * @param handlers the list of lambda function handlers whose data events should be logged (maximum 250 entries). * @param options the options to configure logging of management and data events. */ - public addLambdaEventSelector(dataResourceValues: string[], options: AddEventSelectorOptions = {}) { + public addLambdaEventSelector(handlers: lambda.IFunction[], options: AddEventSelectorOptions = {}) { + if (handlers.length === 0) { return; } + const dataResourceValues = handlers.map((h) => h.functionArn); return this.addEventSelector(DataResourceType.LAMBDA_FUNCTION, dataResourceValues, options); } + /** + * Log all Lamda data events for all lambda functions the account. + * @see https://docs.aws.amazon.com/awscloudtrail/latest/userguide/logging-data-events-with-cloudtrail.html + * @default false + */ + public logAllLambdaDataEvents(options: AddEventSelectorOptions = {}) { + return this.addEventSelector(DataResourceType.LAMBDA_FUNCTION, [ 'arn:aws:lambda' ], options); + } + /** * When an event occurs in your account, CloudTrail evaluates whether the event matches the settings for your trails. * Only events that match your trail settings are delivered to your Amazon S3 bucket and Amazon CloudWatch Logs log group. @@ -332,13 +357,24 @@ export class Trail extends Resource { * Data events: These events provide insight into the resource operations performed on or within a resource. * These are also known as data plane operations. * - * @param dataResourceValues the list of data resource ARNs to include in logging (maximum 250 entries). + * @param s3Selector the list of S3 bucket with optional prefix to include in logging (maximum 250 entries). * @param options the options to configure logging of management and data events. */ - public addS3EventSelector(dataResourceValues: string[], options: AddEventSelectorOptions = {}) { + public addS3EventSelector(s3Selector: S3EventSelector[], options: AddEventSelectorOptions = {}) { + if (s3Selector.length === 0) { return; } + const dataResourceValues = s3Selector.map((sel) => `${sel.bucket.bucketArn}/${sel.objectPrefix ?? ''}`); return this.addEventSelector(DataResourceType.S3_OBJECT, dataResourceValues, options); } + /** + * Log all S3 data events for all objects for all buckets in the account. + * @see https://docs.aws.amazon.com/awscloudtrail/latest/userguide/logging-data-events-with-cloudtrail.html + * @default false + */ + public logAllS3DataEvents(options: AddEventSelectorOptions = {}) { + return this.addEventSelector(DataResourceType.S3_OBJECT, [ 'arn:aws:s3:::' ], options); + } + /** * Create an event rule for when an event is recorded by any Trail in the account. * @@ -373,6 +409,20 @@ export interface AddEventSelectorOptions { readonly includeManagementEvents?: boolean; } +/** + * Selecting an S3 bucket and an optional prefix to be logged for data events. + */ +export interface S3EventSelector { + /** S3 bucket */ + readonly bucket: s3.IBucket; + + /** + * Data events for objects whose key matches this prefix will be logged. + * @default - all objects + */ + readonly objectPrefix?: string; +} + /** * Resource type for a data event */ diff --git a/packages/@aws-cdk/aws-cloudtrail/package.json b/packages/@aws-cdk/aws-cloudtrail/package.json index ff6eaa92cdf43..cc2107d9dabab 100644 --- a/packages/@aws-cdk/aws-cloudtrail/package.json +++ b/packages/@aws-cdk/aws-cloudtrail/package.json @@ -79,6 +79,7 @@ "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, @@ -90,6 +91,7 @@ "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, diff --git a/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts b/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts index bfcb91c06ddba..50c2b766bb4c3 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts +++ b/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts @@ -1,4 +1,4 @@ -import { SynthUtils } from '@aws-cdk/assert'; +import { ABSENT, SynthUtils } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; @@ -246,54 +246,98 @@ describe('cloudtrail', () => { }); describe('with event selectors', () => { - test('with default props', () => { + test('all s3 events', () => { const stack = getTestStack(); const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); - cloudTrail.addS3EventSelector(['arn:aws:s3:::']); + cloudTrail.logAllS3DataEvents(); - expect(stack).toHaveResource('AWS::CloudTrail::Trail'); - expect(stack).toHaveResource('AWS::S3::Bucket'); - expect(stack).toHaveResource('AWS::S3::BucketPolicy', ExpectedBucketPolicyProperties); - expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); - expect(stack).not.toHaveResource('AWS::IAM::Role'); + expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + EventSelectors: [ + { + DataResources: [{ + Type: 'AWS::S3::Object', + Values: [ 'arn:aws:s3:::' ], + }], + IncludeManagementEvents: ABSENT, + ReadWriteType: ABSENT, + }, + ], + }); + }); - const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; - expect(trail.Properties.EventSelectors.length).toEqual(1); - const selector = trail.Properties.EventSelectors[0]; - expect(selector.ReadWriteType).toBeUndefined(); - expect(selector.IncludeManagementEvents).toBeUndefined(); - expect(selector.DataResources.length).toEqual(1); - const dataResource = selector.DataResources[0]; - expect(dataResource.Type).toEqual('AWS::S3::Object'); - expect(dataResource.Values.length).toEqual(1); - expect(dataResource.Values[0]).toEqual('arn:aws:s3:::'); - expect(trail.DependsOn).toEqual(['MyAmazingCloudTrailS3Policy39C120B0']); + test('specific s3 buckets and objects', () => { + const stack = getTestStack(); + const bucket = new s3.Bucket(stack, 'testBucket', { bucketName: 'test-bucket' }); + + const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); + cloudTrail.addS3EventSelector([{ bucket }]); + cloudTrail.addS3EventSelector([{ + bucket, + objectPrefix: 'prefix-1/prefix-2', + }]); + + expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + EventSelectors: [ + { + DataResources: [{ + Type: 'AWS::S3::Object', + Values: [{ + 'Fn::Join': [ + '', + [ + { 'Fn::GetAtt': [ 'testBucketDF4D7D1A', 'Arn' ]}, + '/', + ], + ], + }], + }], + }, + { + DataResources: [{ + Type: 'AWS::S3::Object', + Values: [{ + 'Fn::Join': [ + '', + [ + { 'Fn::GetAtt': [ 'testBucketDF4D7D1A', 'Arn' ]}, + '/prefix-1/prefix-2', + ], + ], + }], + }], + }, + ], + }); + }); + + test('no s3 event selector when list is empty', () => { + const stack = getTestStack(); + const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); + cloudTrail.addS3EventSelector([]); + expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + EventSelectors: [], + }); }); test('with hand-specified props', () => { const stack = getTestStack(); const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); - cloudTrail.addS3EventSelector(['arn:aws:s3:::'], { includeManagementEvents: false, readWriteType: ReadWriteType.READ_ONLY }); - - expect(stack).toHaveResource('AWS::CloudTrail::Trail'); - expect(stack).toHaveResource('AWS::S3::Bucket'); - expect(stack).toHaveResource('AWS::S3::BucketPolicy', ExpectedBucketPolicyProperties); - expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); - expect(stack).not.toHaveResource('AWS::IAM::Role'); + cloudTrail.logAllS3DataEvents({ includeManagementEvents: false, readWriteType: ReadWriteType.READ_ONLY }); - const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; - expect(trail.Properties.EventSelectors.length).toEqual(1); - const selector = trail.Properties.EventSelectors[0]; - expect(selector.ReadWriteType).toEqual('ReadOnly'); - expect(selector.IncludeManagementEvents).toEqual(false); - expect(selector.DataResources.length).toEqual(1); - const dataResource = selector.DataResources[0]; - expect(dataResource.Type).toEqual('AWS::S3::Object'); - expect(dataResource.Values.length).toEqual(1); - expect(dataResource.Values[0]).toEqual('arn:aws:s3:::'); - expect(trail.DependsOn).toEqual(['MyAmazingCloudTrailS3Policy39C120B0']); + expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + EventSelectors: [ + { + DataResources: [{ + Type: 'AWS::S3::Object', + Values: [ 'arn:aws:s3:::' ], + }], + IncludeManagementEvents: false, + ReadWriteType: 'ReadOnly', + }, + ], + }); }); test('with management event', () => { @@ -301,12 +345,14 @@ describe('cloudtrail', () => { new Trail(stack, 'MyAmazingCloudTrail', { managementEvents: ReadWriteType.WRITE_ONLY }); - const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; - expect(trail.Properties.EventSelectors.length).toEqual(1); - const selector = trail.Properties.EventSelectors[0]; - expect(selector.ReadWriteType).toEqual('WriteOnly'); - expect(selector.IncludeManagementEvents).toEqual(true); - expect(selector.DataResources).toEqual(undefined); + expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + EventSelectors: [ + { + IncludeManagementEvents: true, + ReadWriteType: 'WriteOnly', + }, + ], + }); }); test('for Lambda function data event', () => { @@ -318,46 +364,54 @@ describe('cloudtrail', () => { }); const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); - cloudTrail.addLambdaEventSelector([lambdaFunction.functionArn]); + cloudTrail.addLambdaEventSelector([lambdaFunction]); - expect(stack).toHaveResource('AWS::CloudTrail::Trail'); - expect(stack).toHaveResource('AWS::Lambda::Function'); - expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); - - const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; - expect(trail.Properties.EventSelectors.length).toEqual(1); - const selector = trail.Properties.EventSelectors[0]; - expect(selector.ReadWriteType).toBeUndefined(); - expect(selector.IncludeManagementEvents).toBeUndefined(); - expect(selector.DataResources.length).toEqual(1); - const dataResource = selector.DataResources[0]; - expect(dataResource.Type).toEqual('AWS::Lambda::Function'); - expect(dataResource.Values.length).toEqual(1); - expect(dataResource.Values[0]).toEqual({ 'Fn::GetAtt': [ 'LambdaFunctionBF21E41F', 'Arn' ] }); - expect(trail.DependsOn).toEqual(['MyAmazingCloudTrailS3Policy39C120B0']); + expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + EventSelectors: [ + { + DataResources: [{ + Type: 'AWS::Lambda::Function', + Values: [{ + 'Fn::GetAtt': [ 'LambdaFunctionBF21E41F', 'Arn' ], + }], + }], + }, + ], + }); }); test('for all Lambda function data events', () => { const stack = getTestStack(); const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); - cloudTrail.addLambdaEventSelector(['arn:aws:lambda']); + cloudTrail.logAllLambdaDataEvents(); - expect(stack).toHaveResource('AWS::CloudTrail::Trail'); - expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); - expect(stack).not.toHaveResource('AWS::IAM::Role'); + expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + EventSelectors: [ + { + DataResources: [{ + Type: 'AWS::Lambda::Function', + Values: [ 'arn:aws:lambda' ], + }], + }, + ], + }); + }); - const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; - expect(trail.Properties.EventSelectors.length).toEqual(1); - const selector = trail.Properties.EventSelectors[0]; - expect(selector.ReadWriteType).toBeUndefined(); - expect(selector.IncludeManagementEvents).toBeUndefined(); - expect(selector.DataResources.length).toEqual(1); - const dataResource = selector.DataResources[0]; - expect(dataResource.Type).toEqual('AWS::Lambda::Function'); - expect(dataResource.Values.length).toEqual(1); - expect(dataResource.Values[0]).toEqual('arn:aws:lambda'); - expect(trail.DependsOn).toEqual(['MyAmazingCloudTrailS3Policy39C120B0']); + test('managementEvents set to None correctly turns off management events', () => { + const stack = getTestStack(); + + new Trail(stack, 'MyAmazingCloudTrail', { + managementEvents: ReadWriteType.NONE, + }); + + expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + EventSelectors: [ + { + IncludeManagementEvents: false, + }, + ], + }); }); }); }); diff --git a/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail-supplied-bucket.lit.ts b/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail-supplied-bucket.lit.ts index fa57b1f2caf05..ad8614b3c1564 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail-supplied-bucket.lit.ts +++ b/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail-supplied-bucket.lit.ts @@ -37,7 +37,7 @@ Trailbucket.addToResourcePolicy(new iam.PolicyStatement({ const trail = new cloudtrail.Trail(stack, 'Trail', {bucket: Trailbucket}); -trail.addLambdaEventSelector([lambdaFunction.functionArn]); -trail.addS3EventSelector([bucket.arnForObjects('')]); +trail.addLambdaEventSelector([lambdaFunction]); +trail.addS3EventSelector([{bucket}]); app.synth(); diff --git a/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.ts b/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.ts index bee7fc432d6ed..5f53f4efeb0fa 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.ts +++ b/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.ts @@ -14,7 +14,7 @@ const lambdaFunction = new lambda.Function(stack, 'LambdaFunction', { }); const trail = new cloudtrail.Trail(stack, 'Trail'); -trail.addLambdaEventSelector([lambdaFunction.functionArn]); -trail.addS3EventSelector([bucket.arnForObjects('')]); +trail.addLambdaEventSelector([lambdaFunction]); +trail.addS3EventSelector([{bucket}]); app.synth(); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/bitbucket/source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/bitbucket/source-action.ts index 9c005cc849edc..6fb8770796824 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/bitbucket/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/bitbucket/source-action.ts @@ -69,6 +69,14 @@ export interface BitBucketSourceActionProps extends codepipeline.CommonAwsAction * @experimental */ export class BitBucketSourceAction extends Action { + /** + * The name of the property that holds the ARN of the CodeStar Connection + * inside of the CodePipeline Artifact's metadata. + * + * @internal + */ + public static readonly _CONNECTION_ARN_PROPERTY = 'CodeStarConnectionArnProperty'; + private readonly props: BitBucketSourceActionProps; constructor(props: BitBucketSourceActionProps) { @@ -98,6 +106,14 @@ export class BitBucketSourceAction extends Action { // the action needs to write the output to the pipeline bucket options.bucket.grantReadWrite(options.role); + // if codeBuildCloneOutput is true, + // save the connectionArn in the Artifact instance + // to be read by the CodeBuildAction later + if (this.props.codeBuildCloneOutput === true) { + this.props.output.setMetadata(BitBucketSourceAction._CONNECTION_ARN_PROPERTY, + this.props.connectionArn); + } + return { configuration: { ConnectionArn: this.props.connectionArn, diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts index 48bdfed738c31..53d789b665262 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts @@ -2,6 +2,7 @@ import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; +import { BitBucketSourceAction } from '..'; import { Action } from '../action'; /** @@ -153,6 +154,19 @@ export class CodeBuildAction extends Action { }); } + // if any of the inputs come from the BitBucketSourceAction + // with codeBuildCloneOutput=true, + // grant the Project's Role to use the connection + for (const inputArtifact of this.actionProperties.inputs || []) { + const connectionArn = inputArtifact.getMetadata(BitBucketSourceAction._CONNECTION_ARN_PROPERTY); + if (connectionArn) { + this.props.project.addToRolePolicy(new iam.PolicyStatement({ + actions: ['codestar-connections:UseConnection'], + resources: [connectionArn], + })); + } + } + const configuration: any = { ProjectName: this.props.project.projectName, EnvironmentVariables: this.props.environmentVariables && diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts index caaa5ee3ed174..2fa7a67b29b93 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts @@ -87,7 +87,10 @@ export class CodeCommitSourceAction extends Action { private readonly props: CodeCommitSourceActionProps; constructor(props: CodeCommitSourceActionProps) { - const branch = props.branch || 'master'; + const branch = props.branch ?? 'master'; + if (!branch) { + throw new Error("'branch' parameter cannot be an empty string"); + } super({ ...props, @@ -119,7 +122,8 @@ export class CodeCommitSourceAction extends Action { const createEvent = this.props.trigger === undefined || this.props.trigger === CodeCommitTrigger.EVENTS; if (createEvent) { - this.props.repository.onCommit(stage.pipeline.node.uniqueId + 'EventRule', { + const branchIdDisambiguator = this.branch === 'master' ? '' : `-${this.branch}-`; + this.props.repository.onCommit(`${stage.pipeline.node.uniqueId}${branchIdDisambiguator}EventRule`, { target: new targets.CodePipeline(stage.pipeline), branches: [this.branch], }); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/package.json b/packages/@aws-cdk/aws-codepipeline-actions/package.json index af13cef9e8ade..8f8cb92d237ef 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/package.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-cloudtrail": "0.0.0", - "@types/lodash": "^4.14.152", + "@types/lodash": "^4.14.153", "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/test.bitbucket-source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/test.bitbucket-source-action.ts index 90ed1a4159134..f245a720a2fd9 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/test.bitbucket-source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/test.bitbucket-source-action.ts @@ -12,32 +12,8 @@ export = { 'produces the correct configuration when added to a pipeline'(test: Test) { const stack = new Stack(); - const sourceOutput = new codepipeline.Artifact(); - new codepipeline.Pipeline(stack, 'Pipeline', { - stages: [ - { - stageName: 'Source', - actions: [ - new cpactions.BitBucketSourceAction({ - actionName: 'BitBucket', - owner: 'aws', - repo: 'aws-cdk', - output: sourceOutput, - connectionArn: 'arn:aws:codestar-connections:us-east-1:123456789012:connection/12345678-abcd-12ab-34cdef5678gh', - }), - ], - }, - { - stageName: 'Build', - actions: [ - new cpactions.CodeBuildAction({ - actionName: 'CodeBuild', - project: new codebuild.PipelineProject(stack, 'MyProject'), - input: sourceOutput, - }), - ], - }, - ], + createBitBucketAndCodeBuildPipeline(stack, { + codeBuildCloneOutput: false, }); expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { @@ -73,4 +49,69 @@ export = { test.done(); }, }, + + 'setting codeBuildCloneOutput=true adds permission to use the connection to the following CodeBuild Project'(test: Test) { + const stack = new Stack(); + + createBitBucketAndCodeBuildPipeline(stack, { + codeBuildCloneOutput: true, + }); + + expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': [ + 'logs:CreateLogGroup', + 'logs:CreateLogStream', + 'logs:PutLogEvents', + ], + }, + {}, + {}, + {}, + {}, + { + 'Action': 'codestar-connections:UseConnection', + 'Effect': 'Allow', + 'Resource': 'arn:aws:codestar-connections:us-east-1:123456789012:connection/12345678-abcd-12ab-34cdef5678gh', + }, + ], + }, + })); + + test.done(); + }, }; + +function createBitBucketAndCodeBuildPipeline(stack: Stack, props: { codeBuildCloneOutput: boolean }): void { + const sourceOutput = new codepipeline.Artifact(); + new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [ + new cpactions.BitBucketSourceAction({ + actionName: 'BitBucket', + owner: 'aws', + repo: 'aws-cdk', + output: sourceOutput, + connectionArn: 'arn:aws:codestar-connections:us-east-1:123456789012:connection/12345678-abcd-12ab-34cdef5678gh', + codeBuildCloneOutput: props.codeBuildCloneOutput, + }), + ], + }, + { + stageName: 'Build', + actions: [ + new cpactions.CodeBuildAction({ + actionName: 'CodeBuild', + project: new codebuild.PipelineProject(stack, 'MyProject'), + input: sourceOutput, + outputs: [new codepipeline.Artifact()], + }), + ], + }, + ], + }); +} diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/test.codecommit-source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/test.codecommit-source-action.ts index 33f0d72bca24d..0650c50f2b596 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/test.codecommit-source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/test.codecommit-source-action.ts @@ -110,6 +110,66 @@ export = { test.done(); }, + 'cannot be created with an empty branch'(test: Test) { + const stack = new Stack(); + const repo = new codecommit.Repository(stack, 'MyRepo', { + repositoryName: 'my-repo', + }); + + test.throws(() => { + new cpactions.CodeCommitSourceAction({ + actionName: 'Source2', + repository: repo, + output: new codepipeline.Artifact(), + branch: '', + }); + }, /'branch' parameter cannot be an empty string/); + + test.done(); + }, + + 'allows using the same repository multiple times with different branches when trigger=EVENTS'(test: Test) { + const stack = new Stack(); + + const repo = new codecommit.Repository(stack, 'MyRepo', { + repositoryName: 'my-repo', + }); + const sourceOutput1 = new codepipeline.Artifact(); + const sourceOutput2 = new codepipeline.Artifact(); + new codepipeline.Pipeline(stack, 'MyPipeline', { + stages: [ + { + stageName: 'Source', + actions: [ + new cpactions.CodeCommitSourceAction({ + actionName: 'Source1', + repository: repo, + output: sourceOutput1, + }), + new cpactions.CodeCommitSourceAction({ + actionName: 'Source2', + repository: repo, + output: sourceOutput2, + branch: 'develop', + }), + ], + }, + { + stageName: 'Build', + actions: [ + new cpactions.CodeBuildAction({ + actionName: 'Build', + project: new codebuild.PipelineProject(stack, 'MyProject'), + input: sourceOutput1, + }), + ], + }, + ], + }); + + test.done(); + }, + 'exposes variables for other actions to consume'(test: Test) { const stack = new Stack(); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-pipeline.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-pipeline.ts index ca18d03b47eef..e9ad9cc11141d 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-pipeline.ts @@ -18,7 +18,7 @@ const bucket = new s3.Bucket(stack, 'PipelineBucket', { }); const key = 'key'; const trail = new cloudtrail.Trail(stack, 'CloudTrail'); -trail.addS3EventSelector([bucket.arnForObjects(key)], { readWriteType: cloudtrail.ReadWriteType.WRITE_ONLY, includeManagementEvents: false }); +trail.addS3EventSelector([ { bucket, objectPrefix: key }], { readWriteType: cloudtrail.ReadWriteType.WRITE_ONLY, includeManagementEvents: false }); sourceStage.addAction(new cpactions.S3SourceAction({ actionName: 'Source', output: new codepipeline.Artifact('SourceArtifact'), diff --git a/packages/@aws-cdk/aws-codepipeline/lib/artifact.ts b/packages/@aws-cdk/aws-codepipeline/lib/artifact.ts index 79339691272b6..fab9b46edcfe6 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/artifact.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/artifact.ts @@ -17,6 +17,7 @@ export class Artifact { } private _artifactName?: string; + private readonly metadata: { [key: string]: any } = {}; constructor(artifactName?: string) { validation.validateArtifactName(artifactName); @@ -80,6 +81,25 @@ export class Artifact { }; } + /** + * Add arbitrary extra payload to the artifact under a given key. + * This can be used by CodePipeline actions to communicate data between themselves. + * If metadata was already present under the given key, + * it will be overwritten with the new value. + */ + public setMetadata(key: string, value: any): void { + this.metadata[key] = value; + } + + /** + * Retrieve the metadata stored in this artifact under the given key. + * If there is no metadata stored under the given key, + * null will be returned. + */ + public getMetadata(key: string): any { + return this.metadata[key]; + } + public toString() { return this.artifactName; } diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index 26c86887e98bc..b498c20945f83 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -749,34 +749,52 @@ export class Pipeline extends PipelineBase { private validateArtifacts(): string[] { const ret = new Array(); - const outputArtifactNames = new Set(); - for (const stage of this._stages) { - const sortedActions = stage.actionDescriptors.sort((a1, a2) => a1.runOrder - a2.runOrder); - - for (const action of sortedActions) { - // start with inputs - const inputArtifacts = action.inputs; - for (const inputArtifact of inputArtifacts) { - if (!inputArtifact.artifactName) { - ret.push(`Action '${action.actionName}' has an unnamed input Artifact that's not used as an output`); - } else if (!outputArtifactNames.has(inputArtifact.artifactName)) { - ret.push(`Artifact '${inputArtifact.artifactName}' was used as input before being used as output`); + const producers: Record = {}; + const firstConsumers: Record = {}; + + for (const [stageIndex, stage] of enumerate(this._stages)) { + // For every output artifact, get the producer + for (const action of stage.actionDescriptors) { + const actionLoc = new PipelineLocation(stageIndex, stage, action); + + for (const outputArtifact of action.outputs) { + // output Artifacts always have a name set + const name = outputArtifact.artifactName!; + if (producers[name]) { + ret.push(`Both Actions '${producers[name].actionName}' and '${action.actionName}' are producting Artifact '${name}'. Every artifact can only be produced once.`); + continue; } + + producers[name] = actionLoc; } - // then process outputs by adding them to the Set - const outputArtifacts = action.outputs; - for (const outputArtifact of outputArtifacts) { - // output Artifacts always have a name set - if (outputArtifactNames.has(outputArtifact.artifactName!)) { - ret.push(`Artifact '${outputArtifact.artifactName}' has been used as an output more than once`); - } else { - outputArtifactNames.add(outputArtifact.artifactName!); + // For every input artifact, get the first consumer + for (const inputArtifact of action.inputs) { + const name = inputArtifact.artifactName; + if (!name) { + ret.push(`Action '${action.actionName}' is using an unnamed input Artifact, which is not being produced in this pipeline`); + continue; } + + firstConsumers[name] = firstConsumers[name] ? firstConsumers[name].first(actionLoc) : actionLoc; } } } + // Now validate that every input artifact is produced before it's + // being consumed. + for (const [artifactName, consumerLoc] of Object.entries(firstConsumers)) { + const producerLoc = producers[artifactName]; + if (!producerLoc) { + ret.push(`Action '${consumerLoc.actionName}' is using input Artifact '${artifactName}', which is not being produced in this pipeline`); + continue; + } + + if (consumerLoc.beforeOrEqual(producerLoc)) { + ret.push(`${consumerLoc} is consuming input Artifact '${artifactName}' before it is being produced at ${producerLoc}`); + } + } + return ret; } @@ -874,3 +892,44 @@ interface CrossRegionInfo { readonly region?: string; } + +function enumerate(xs: A[]): Array<[number, A]> { + const ret = new Array<[number, A]>(); + for (let i = 0; i < xs.length; i++) { + ret.push([i, xs[i]]); + } + return ret; +} + +class PipelineLocation { + constructor(private readonly stageIndex: number, private readonly stage: IStage, private readonly action: FullActionDescriptor) { + } + + public get stageName() { + return this.stage.stageName; + } + + public get actionName() { + return this.action.actionName; + } + + /** + * Returns whether a is before or the same order as b + */ + public beforeOrEqual(rhs: PipelineLocation) { + if (this.stageIndex !== rhs.stageIndex) { return rhs.stageIndex < rhs.stageIndex; } + return this.action.runOrder <= rhs.action.runOrder; + } + + /** + * Returns the first location between this and the other one + */ + public first(rhs: PipelineLocation) { + return this.beforeOrEqual(rhs) ? this : rhs; + } + + public toString() { + // runOrders are 1-based, so make the stageIndex also 1-based otherwise it's going to be confusing. + return `Stage ${this.stageIndex + 1} Action ${this.action.runOrder} ('${this.stageName}'/'${this.actionName}')`; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline/test/test.artifacts.ts b/packages/@aws-cdk/aws-codepipeline/test/test.artifacts.ts index 4003e0bc41c43..b638a3c1c7b90 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/test.artifacts.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/test.artifacts.ts @@ -46,7 +46,7 @@ export = { test.equal(errors.length, 1); const error = errors[0]; test.same(error.source, pipeline); - test.equal(error.message, "Action 'Build' has an unnamed input Artifact that's not used as an output"); + test.equal(error.message, "Action 'Build' is using an unnamed input Artifact, which is not being produced in this pipeline"); test.done(); }, @@ -82,7 +82,7 @@ export = { test.equal(errors.length, 1); const error = errors[0]; test.same(error.source, pipeline); - test.equal(error.message, "Artifact 'named' was used as input before being used as output"); + test.equal(error.message, "Action 'Build' is using input Artifact 'named', which is not being produced in this pipeline"); test.done(); }, @@ -119,7 +119,7 @@ export = { test.equal(errors.length, 1); const error = errors[0]; test.same(error.source, pipeline); - test.equal(error.message, "Artifact 'Artifact_Source_Source' has been used as an output more than once"); + test.equal(error.message, "Both Actions 'Source' and 'Build' are producting Artifact 'Artifact_Source_Source'. Every artifact can only be produced once."); test.done(); }, @@ -173,6 +173,59 @@ export = { test.done(); }, + 'violation of runOrder constraints is detected and reported'(test: Test) { + const stack = new cdk.Stack(); + + const sourceOutput1 = new codepipeline.Artifact('sourceOutput1'); + const buildOutput1 = new codepipeline.Artifact('buildOutput1'); + const sourceOutput2 = new codepipeline.Artifact('sourceOutput2'); + + const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [ + new FakeSourceAction({ + actionName: 'source1', + output: sourceOutput1, + }), + new FakeSourceAction({ + actionName: 'source2', + output: sourceOutput2, + }), + ], + }, + { + stageName: 'Build', + actions: [ + new FakeBuildAction({ + actionName: 'build1', + input: sourceOutput1, + output: buildOutput1, + runOrder: 3, + }), + new FakeBuildAction({ + actionName: 'build2', + input: sourceOutput2, + extraInputs: [buildOutput1], + output: new codepipeline.Artifact('buildOutput2'), + runOrder: 2, + }), + ], + }, + ], + }); + + const errors = validate(stack); + + test.equal(errors.length, 1); + const error = errors[0]; + test.same(error.source, pipeline); + test.equal(error.message, "Stage 2 Action 2 ('Build'/'build2') is consuming input Artifact 'buildOutput1' before it is being produced at Stage 2 Action 3 ('Build'/'build1')"); + + test.done(); + }, + 'without a name, sanitize the auto stage-action derived name'(test: Test) { const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 192fd76826e64..229c6d1cbd00c 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -446,4 +446,32 @@ pool.addDomain('CustomDomain', { Read more about [Using the Amazon Cognito Domain](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-assign-domain-prefix.html) and [Using Your Own -Domain](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-add-custom-domain.html). +Domain](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-add-custom-domain.html) + +The `signInUrl()` methods returns the fully qualified URL to the login page for the user pool. This page comes from the +hosted UI configured with Cognito. Learn more at [Hosted UI with the Amazon Cognito +Console](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-integration.html#cognito-user-pools-create-an-app-integration). + +```ts +const userpool = new UserPool(this, 'UserPool', { + // ... +}); +const client = userpool.addClient('Client', { + // ... + oAuth: { + flows: { + implicitCodeGrant: true, + }, + callbackUrls: [ + 'https://myapp.com/home', + 'https://myapp.com/users', + ] + } +}) +const domain = userpool.addDomain('Domain', { + // ... +}); +const signInUrl = domain.signInUrl(client, { + redirectUrl: 'https://myapp.com/home', // must be a URL configured under 'callbackUrls' with the client +}) +``` \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts index 039c17376b8fe..4c945a829aacf 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts @@ -46,22 +46,22 @@ export interface OAuthSettings { /** * OAuth flows that are allowed with this client. * @see - the 'Allowed OAuth Flows' section at https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-idp-settings.html - * @default - all OAuth flows disabled + * @default {authorizationCodeGrant:true,implicitCodeGrant:true} */ - readonly flows: OAuthFlows; + readonly flows?: OAuthFlows; /** * List of allowed redirect URLs for the identity providers. - * @default - no callback URLs + * @default - ['https://example.com'] if either authorizationCodeGrant or implicitCodeGrant flows are enabled, no callback URLs otherwise. */ readonly callbackUrls?: string[]; /** * OAuth scopes that are allowed with this client. * @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-idp-settings.html - * @default - no OAuth scopes are configured. + * @default [OAuthScope.PHONE,OAuthScope.EMAIL,OAuthScope.OPENID,OAuthScope.PROFILE,OAuthScope.COGNITO_ADMIN] */ - readonly scopes: OAuthScope[]; + readonly scopes?: OAuthScope[]; } /** @@ -221,6 +221,10 @@ export class UserPoolClient extends Resource implements IUserPoolClient { } public readonly userPoolClientId: string; + /** + * The OAuth flows enabled for this client. + */ + public readonly oAuthFlows: OAuthFlows; private readonly _userPoolClientName?: string; /* @@ -234,16 +238,31 @@ export class UserPoolClient extends Resource implements IUserPoolClient { constructor(scope: Construct, id: string, props: UserPoolClientProps) { super(scope, id); + this.oAuthFlows = props.oAuth?.flows ?? { + implicitCodeGrant: true, + authorizationCodeGrant: true, + }; + + let callbackUrls: string[] | undefined = props.oAuth?.callbackUrls; + if (this.oAuthFlows.authorizationCodeGrant || this.oAuthFlows.implicitCodeGrant) { + if (callbackUrls === undefined) { + callbackUrls = [ 'https://example.com' ]; + } else if (callbackUrls.length === 0) { + throw new Error('callbackUrl must not be empty when codeGrant or implicitGrant OAuth flows are enabled.'); + } + } + const resource = new CfnUserPoolClient(this, 'Resource', { clientName: props.userPoolClientName, generateSecret: props.generateSecret, userPoolId: props.userPool.userPoolId, explicitAuthFlows: this.configureAuthFlows(props), - allowedOAuthFlows: this.configureOAuthFlows(props.oAuth), + allowedOAuthFlows: this.configureOAuthFlows(), allowedOAuthScopes: this.configureOAuthScopes(props.oAuth), - callbackUrLs: (props.oAuth?.callbackUrls && props.oAuth?.callbackUrls.length > 0) ? props.oAuth?.callbackUrls : undefined, + callbackUrLs: callbackUrls && callbackUrls.length > 0 ? callbackUrls : undefined, allowedOAuthFlowsUserPoolClient: props.oAuth ? true : undefined, preventUserExistenceErrors: this.configurePreventUserExistenceErrors(props.preventUserExistenceErrors), + supportedIdentityProviders: [ 'COGNITO' ], }); this.userPoolClientId = resource.ref; @@ -275,20 +294,14 @@ export class UserPoolClient extends Resource implements IUserPoolClient { return authFlows; } - private configureOAuthFlows(oAuth?: OAuthSettings): string[] | undefined { - if (oAuth?.flows.authorizationCodeGrant || oAuth?.flows.implicitCodeGrant) { - if (oAuth?.callbackUrls === undefined || oAuth?.callbackUrls.length === 0) { - throw new Error('callbackUrl must be specified when codeGrant or implicitGrant OAuth flows are enabled.'); - } - if (oAuth?.flows.clientCredentials) { - throw new Error('clientCredentials OAuth flow cannot be selected along with codeGrant or implicitGrant.'); - } + private configureOAuthFlows(): string[] | undefined { + if ((this.oAuthFlows.authorizationCodeGrant || this.oAuthFlows.implicitCodeGrant) && this.oAuthFlows.clientCredentials) { + throw new Error('clientCredentials OAuth flow cannot be selected along with codeGrant or implicitGrant.'); } - const oAuthFlows: string[] = []; - if (oAuth?.flows.clientCredentials) { oAuthFlows.push('client_credentials'); } - if (oAuth?.flows.implicitCodeGrant) { oAuthFlows.push('implicit'); } - if (oAuth?.flows.authorizationCodeGrant) { oAuthFlows.push('code'); } + if (this.oAuthFlows.clientCredentials) { oAuthFlows.push('client_credentials'); } + if (this.oAuthFlows.implicitCodeGrant) { oAuthFlows.push('implicit'); } + if (this.oAuthFlows.authorizationCodeGrant) { oAuthFlows.push('code'); } if (oAuthFlows.length === 0) { return undefined; @@ -296,16 +309,15 @@ export class UserPoolClient extends Resource implements IUserPoolClient { return oAuthFlows; } - private configureOAuthScopes(oAuth?: OAuthSettings): string[] | undefined { - const oAuthScopes = new Set(oAuth?.scopes.map((x) => x.scopeName)); + private configureOAuthScopes(oAuth?: OAuthSettings): string[] { + const scopes = oAuth?.scopes ?? [ OAuthScope.PROFILE, OAuthScope.PHONE, OAuthScope.EMAIL, OAuthScope.OPENID, + OAuthScope.COGNITO_ADMIN ]; + const scopeNames = new Set(scopes.map((x) => x.scopeName)); const autoOpenIdScopes = [ OAuthScope.PHONE, OAuthScope.EMAIL, OAuthScope.PROFILE ]; - if (autoOpenIdScopes.reduce((agg, s) => agg || oAuthScopes.has(s.scopeName), false)) { - oAuthScopes.add(OAuthScope.OPENID.scopeName); - } - if (oAuthScopes.size > 0) { - return Array.from(oAuthScopes); + if (autoOpenIdScopes.reduce((agg, s) => agg || scopeNames.has(s.scopeName), false)) { + scopeNames.add(OAuthScope.OPENID.scopeName); } - return undefined; + return Array.from(scopeNames); } private configurePreventUserExistenceErrors(prevent?: boolean): string | undefined { diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts index b1518861e2fbb..e829cd2c03713 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts @@ -1,8 +1,9 @@ import { ICertificate } from '@aws-cdk/aws-certificatemanager'; -import { Construct, IResource, Resource } from '@aws-cdk/core'; +import { Construct, IResource, Resource, Stack } from '@aws-cdk/core'; import { AwsCustomResource, AwsCustomResourcePolicy, AwsSdkCall, PhysicalResourceId } from '@aws-cdk/custom-resources'; import { CfnUserPoolDomain } from './cognito.generated'; import { IUserPool } from './user-pool'; +import { UserPoolClient } from './user-pool-client'; /** * Represents a user pool domain. @@ -80,6 +81,7 @@ export interface UserPoolDomainProps extends UserPoolDomainOptions { */ export class UserPoolDomain extends Resource implements IUserPoolDomain { public readonly domainName: string; + private isCognitoDomain: boolean; constructor(scope: Construct, id: string, props: UserPoolDomainProps) { super(scope, id); @@ -92,6 +94,8 @@ export class UserPoolDomain extends Resource implements IUserPoolDomain { throw new Error('domainPrefix for cognitoDomain can contain only lowercase alphabets, numbers and hyphens'); } + this.isCognitoDomain = !!props.cognitoDomain; + const domainName = props.cognitoDomain?.domainPrefix || props.customDomain?.domainName!; const resource = new CfnUserPoolDomain(this, 'Resource', { userPoolId: props.userPool.userPoolId, @@ -126,4 +130,48 @@ export class UserPoolDomain extends Resource implements IUserPoolDomain { }); return customResource.getResponseField('DomainDescription.CloudFrontDistribution'); } + + /** + * The URL to the hosted UI associated with this domain + */ + public baseUrl(): string { + if (this.isCognitoDomain) { + return `https://${this.domainName}.auth.${Stack.of(this).region}.amazoncognito.com`; + } + return `https://${this.domainName}`; + } + + /** + * The URL to the sign in page in this domain using a specific UserPoolClient + * @param client [disable-awslint:ref-via-interface] the user pool client that the UI will use to interact with the UserPool + * @param options options to customize the behaviour of this method. + */ + public signInUrl(client: UserPoolClient, options: SignInUrlOptions): string { + let responseType: string; + if (client.oAuthFlows.authorizationCodeGrant) { + responseType = 'code'; + } else if (client.oAuthFlows.implicitCodeGrant) { + responseType = 'token'; + } else { + throw new Error('signInUrl is not supported for clients without authorizationCodeGrant or implicitCodeGrant flow enabled'); + } + const path = options.signInPath ?? '/login'; + return `${this.baseUrl()}${path}?client_id=${client.userPoolClientId}&response_type=${responseType}&redirect_uri=${options.redirectUri}`; + } +} + +/** + * Options to customize the behaviour of `signInUrl()` + */ +export interface SignInUrlOptions { + /** + * Where to redirect to after sign in + */ + readonly redirectUri: string; + + /** + * The path in the URI where the sign-in page is located + * @default '/login' + */ + readonly signInPath?: string; } diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index 24fef0a42db70..a0bc9a32d2874 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -3,7 +3,7 @@ import * as lambda from '@aws-cdk/aws-lambda'; import { Construct, Duration, IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; import { CfnUserPool } from './cognito.generated'; import { ICustomAttribute, RequiredAttributes } from './user-pool-attr'; -import { IUserPoolClient, UserPoolClient, UserPoolClientOptions } from './user-pool-client'; +import { UserPoolClient, UserPoolClientOptions } from './user-pool-client'; import { UserPoolDomain, UserPoolDomainOptions } from './user-pool-domain'; /** @@ -526,33 +526,52 @@ export interface IUserPool extends IResource { readonly userPoolArn: string; /** - * Create a user pool client. + * Add a new app client to this user pool. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html + */ + addClient(id: string, options?: UserPoolClientOptions): UserPoolClient; + + /** + * Associate a domain to this user pool. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-assign-domain.html */ - addClient(id: string, options?: UserPoolClientOptions): IUserPoolClient; + addDomain(id: string, options: UserPoolDomainOptions): UserPoolDomain; +} + +abstract class UserPoolBase extends Resource implements IUserPool { + public abstract readonly userPoolId: string; + public abstract readonly userPoolArn: string; + + public addClient(id: string, options?: UserPoolClientOptions): UserPoolClient { + return new UserPoolClient(this, id, { + userPool: this, + ...options, + }); + } + + public addDomain(id: string, options: UserPoolDomainOptions): UserPoolDomain { + return new UserPoolDomain(this, id, { + userPool: this, + ...options, + }); + } } /** * Define a Cognito User Pool */ -export class UserPool extends Resource implements IUserPool { +export class UserPool extends UserPoolBase { /** * Import an existing user pool based on its id. */ public static fromUserPoolId(scope: Construct, id: string, userPoolId: string): IUserPool { - class Import extends Resource implements IUserPool { + class Import extends UserPoolBase { public readonly userPoolId = userPoolId; public readonly userPoolArn = Stack.of(this).formatArn({ service: 'cognito-idp', resource: 'userpool', resourceName: userPoolId, }); - - public addClient(clientId: string, options?: UserPoolClientOptions): IUserPoolClient { - return new UserPoolClient(this, clientId, { - userPool: this, - ...options, - }); - } } return new Import(scope, id); } @@ -669,28 +688,6 @@ export class UserPool extends Resource implements IUserPool { (this.triggers as any)[operation.operationName] = fn.functionArn; } - /** - * Add a new app client to this user pool. - * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html - */ - public addClient(id: string, options?: UserPoolClientOptions): IUserPoolClient { - return new UserPoolClient(this, id, { - userPool: this, - ...options, - }); - } - - /** - * Associate a domain to this user pool. - * @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-assign-domain.html - */ - public addDomain(id: string, options: UserPoolDomainOptions): UserPoolDomain { - return new UserPoolDomain(this, id, { - userPool: this, - ...options, - }); - } - private addLambdaPermission(fn: lambda.IFunction, name: string): void { const capitalize = name.charAt(0).toUpperCase() + name.slice(1); fn.addPermission(`${capitalize}Cognito`, { diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.expected.json index 63556451e98ff..c39124006db33 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.expected.json @@ -79,8 +79,7 @@ "email", "openid", "profile", - "aws.cognito.signin.user.admin", - "my-resource-server/my-scope" + "aws.cognito.signin.user.admin" ], "CallbackURLs": [ "https://redirect-here.myapp.com" @@ -94,8 +93,11 @@ "ALLOW_REFRESH_TOKEN_AUTH" ], "GenerateSecret": true, - "PreventUserExistenceErrors": "ENABLED" + "PreventUserExistenceErrors": "ENABLED", + "SupportedIdentityProviders": [ + "COGNITO" + ] } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts index 92a8bd8f19321..6856739811bb3 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts @@ -27,7 +27,6 @@ userpool.addClient('myuserpoolclient', { OAuthScope.OPENID, OAuthScope.PROFILE, OAuthScope.COGNITO_ADMIN, - OAuthScope.custom('my-resource-server/my-scope'), ], callbackUrls: [ 'https://redirect-here.myapp.com' ], }, diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.expected.json new file mode 100644 index 0000000000000..254b68b5d32b1 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.expected.json @@ -0,0 +1,126 @@ +{ + "Resources": { + "UserPoolsmsRole4EA729DD": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "sts:ExternalId": "integuserpooldomainsigninurlUserPool1325E89F" + } + }, + "Effect": "Allow", + "Principal": { + "Service": "cognito-idp.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "sns-publish" + } + ] + } + }, + "UserPool6BA7E5F2": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsConfiguration": { + "ExternalId": "integuserpooldomainsigninurlUserPool1325E89F", + "SnsCallerArn": { + "Fn::GetAtt": [ + "UserPoolsmsRole4EA729DD", + "Arn" + ] + } + }, + "SmsVerificationMessage": "The verification code to your new account is {####}", + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + } + } + }, + "UserPoolDomainD0EA232A": { + "Type": "AWS::Cognito::UserPoolDomain", + "Properties": { + "Domain": "cdk-integ-user-pool-domain", + "UserPoolId": { + "Ref": "UserPool6BA7E5F2" + } + } + }, + "UserPoolUserPoolClient40176907": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "UserPool6BA7E5F2" + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "SupportedIdentityProviders": [ + "COGNITO" + ] + } + } + }, + "Outputs": { + "SignInUrl": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "UserPoolDomainD0EA232A" + }, + ".auth.", + { + "Ref": "AWS::Region" + }, + ".amazoncognito.com/login?client_id=", + { + "Ref": "UserPoolUserPoolClient40176907" + }, + "&response_type=code&redirect_uri=https://example.com" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.ts new file mode 100644 index 0000000000000..c02f116ccc691 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.ts @@ -0,0 +1,31 @@ +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { UserPool } from '../lib'; + +/* + * Stack verification steps: + * * Run the command `curl -sS -D - '' -o /dev/null` should return HTTP/2 200. + * * It didn't work if it returns 302 or 400. + */ + +const app = new App(); +const stack = new Stack(app, 'integ-user-pool-domain-signinurl'); + +const userpool = new UserPool(stack, 'UserPool'); + +const domain = userpool.addDomain('Domain', { + cognitoDomain: { + domainPrefix: 'cdk-integ-user-pool-domain', + }, +}); + +const client = userpool.addClient('UserPoolClient', { + oAuth: { + callbackUrls: [ 'https://example.com' ], + }, +}); + +new CfnOutput(stack, 'SignInUrl', { + value: domain.signInUrl(client, { + redirectUri: 'https://example.com', + }), +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-code.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-code.expected.json index 27623ad280e39..b14204b367441 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-code.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-code.expected.json @@ -83,8 +83,25 @@ "UserPoolId": { "Ref": "myuserpool01998219" }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], "ClientName": "signup-test", - "GenerateSecret": false + "GenerateSecret": false, + "SupportedIdentityProviders": [ + "COGNITO" + ] } } }, diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.expected.json index 1895949b168a7..02893c7ef113f 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.expected.json @@ -75,23 +75,40 @@ } } }, - "myuserpoolclient8A58A3E4": { - "Type": "AWS::Cognito::UserPoolClient", + "myuserpoolmyuserpooldomainEE1E11AF": { + "Type": "AWS::Cognito::UserPoolDomain", "Properties": { + "Domain": "integ-user-pool-signup-link", "UserPoolId": { "Ref": "myuserpool01998219" - }, - "ClientName": "signup-test", - "GenerateSecret": false + } } }, - "myuserpooldomain": { - "Type": "AWS::Cognito::UserPoolDomain", + "myuserpoolclient8A58A3E4": { + "Type": "AWS::Cognito::UserPoolClient", "Properties": { - "Domain": "integuserpoolsignuplinkmyuserpoolA8374994", "UserPoolId": { "Ref": "myuserpool01998219" - } + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "ClientName": "signup-test", + "GenerateSecret": false, + "SupportedIdentityProviders": [ + "COGNITO" + ] } } }, diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.ts index 089249329fdbc..92f0452010f22 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.ts +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.ts @@ -1,5 +1,5 @@ import { App, CfnOutput, Stack } from '@aws-cdk/core'; -import { CfnUserPoolDomain, UserPool, UserPoolClient, VerificationEmailStyle } from '../lib'; +import { UserPool, UserPoolClient, VerificationEmailStyle } from '../lib'; /* * Stack verification steps: @@ -41,10 +41,10 @@ const client = new UserPoolClient(stack, 'myuserpoolclient', { generateSecret: false, }); -// replace with L2 once Domain support is available -new CfnUserPoolDomain(stack, 'myuserpooldomain', { - userPoolId: userpool.userPoolId, - domain: userpool.node.uniqueId, +userpool.addDomain('myuserpooldomain', { + cognitoDomain: { + domainPrefix: 'integ-user-pool-signup-link', + }, }); new CfnOutput(stack, 'user-pool-id', { diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts index d1e0862df0a50..838584da1d25f 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts @@ -17,6 +17,10 @@ describe('User Pool Client', () => { // THEN expect(stack).toHaveResource('AWS::Cognito::UserPoolClient', { UserPoolId: stack.resolve(pool.userPoolId), + AllowedOAuthFlows: [ 'implicit', 'code' ], + AllowedOAuthScopes: [ 'profile', 'phone', 'email', 'openid', 'aws.cognito.signin.user.admin' ], + CallbackURLs: [ 'https://example.com' ], + SupportedIdentityProviders: [ 'COGNITO' ], }); }); @@ -91,21 +95,6 @@ describe('User Pool Client', () => { }); }); - test('AllowedOAuthFlows is absent by default', () => { - // GIVEN - const stack = new Stack(); - const pool = new UserPool(stack, 'Pool'); - - // WHEN - pool.addClient('Client'); - - // THEN - expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { - AllowedOAuthFlows: ABSENT, - // AllowedOAuthFlowsUserPoolClient: ABSENT, - }); - }); - test('AllowedOAuthFlows are correctly named', () => { // GIVEN const stack = new Stack(); @@ -118,7 +107,6 @@ describe('User Pool Client', () => { authorizationCodeGrant: true, implicitCodeGrant: true, }, - callbackUrls: [ 'redirect-url' ], scopes: [ OAuthScope.PHONE ], }, }); @@ -127,7 +115,6 @@ describe('User Pool Client', () => { flows: { clientCredentials: true, }, - callbackUrls: [ 'redirect-url' ], scopes: [ OAuthScope.PHONE ], }, }); @@ -144,28 +131,72 @@ describe('User Pool Client', () => { }); }); - test('fails when callbackUrls are not specified for codeGrant or implicitGrant', () => { + test('callbackUrl defaults are correctly chosen', () => { const stack = new Stack(); const pool = new UserPool(stack, 'Pool'); - expect(() => pool.addClient('Client1', { + pool.addClient('Client1', { oAuth: { - flows: { authorizationCodeGrant: true }, - scopes: [ OAuthScope.PHONE ], + flows: { + clientCredentials: true, + }, }, - })).toThrow(/callbackUrl must be specified/); + }); - expect(() => pool.addClient('Client2', { + pool.addClient('Client2', { + oAuth: { + flows: { + authorizationCodeGrant: true, + }, + }, + }); + + pool.addClient('Client3', { + oAuth: { + flows: { + implicitCodeGrant: true, + }, + }, + }); + + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + AllowedOAuthFlows: [ 'client_credentials' ], + CallbackURLs: ABSENT, + }); + + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + AllowedOAuthFlows: [ 'implicit' ], + CallbackURLs: [ 'https://example.com' ], + }); + + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + AllowedOAuthFlows: [ 'code' ], + CallbackURLs: [ 'https://example.com' ], + }); + }); + + test('fails when callbackUrls is empty for codeGrant or implicitGrant', () => { + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + expect(() => pool.addClient('Client1', { oAuth: { flows: { implicitCodeGrant: true }, - scopes: [ OAuthScope.PHONE ], + callbackUrls: [], }, - })).toThrow(/callbackUrl must be specified/); + })).toThrow(/callbackUrl must not be empty/); expect(() => pool.addClient('Client3', { + oAuth: { + flows: { authorizationCodeGrant: true }, + callbackUrls: [], + }, + })).toThrow(/callbackUrl must not be empty/); + + expect(() => pool.addClient('Client4', { oAuth: { flows: { clientCredentials: true }, - scopes: [ OAuthScope.PHONE ], + callbackUrls: [], }, })).not.toThrow(); }); @@ -180,7 +211,6 @@ describe('User Pool Client', () => { authorizationCodeGrant: true, clientCredentials: true, }, - callbackUrls: [ 'redirect-url' ], scopes: [ OAuthScope.PHONE ], }, })).toThrow(/clientCredentials OAuth flow cannot be selected/); @@ -191,7 +221,6 @@ describe('User Pool Client', () => { implicitCodeGrant: true, clientCredentials: true, }, - callbackUrls: [ 'redirect-url' ], scopes: [ OAuthScope.PHONE ], }, })).toThrow(/clientCredentials OAuth flow cannot be selected/); diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-domain.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-domain.test.ts index 8aa2a7972732b..b2a9c2bb326ad 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-domain.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-domain.test.ts @@ -125,4 +125,67 @@ describe('User Pool Client', () => { }, }); }); + + describe('signInUrl', () => { + test('returns the expected URL', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + const domain = pool.addDomain('Domain', { + cognitoDomain: { + domainPrefix: 'cognito-domain-prefix', + }, + }); + const client = pool.addClient('Client', { + oAuth: { + callbackUrls: [ 'https://example.com' ], + }, + }); + + // WHEN + const signInUrl = domain.signInUrl(client, { + redirectUri: 'https://example.com', + }); + + // THEN + expect(stack.resolve(signInUrl)).toEqual({ + 'Fn::Join': [ + '', [ + 'https://', + { Ref: 'PoolDomainCFC71F56' }, + '.auth.', + { Ref: 'AWS::Region' }, + '.amazoncognito.com/login?client_id=', + { Ref: 'PoolClient8A3E5EB7' }, + '&response_type=code&redirect_uri=https://example.com', + ], + ], + }); + }); + + test('correctly uses the signInPath', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + const domain = pool.addDomain('Domain', { + cognitoDomain: { + domainPrefix: 'cognito-domain-prefix', + }, + }); + const client = pool.addClient('Client', { + oAuth: { + callbackUrls: [ 'https://example.com' ], + }, + }); + + // WHEN + const signInUrl = domain.signInUrl(client, { + redirectUri: 'https://example.com', + signInPath: '/testsignin', + }); + + // THEN + expect(signInUrl).toMatch(/amazoncognito\.com\/testsignin\?/); + }); + }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts index e076d9e79bd2f..83d4863b751c3 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts @@ -818,6 +818,35 @@ test('addClient', () => { }); }); +test('addDomain', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const userpool = new UserPool(stack, 'Pool'); + userpool.addDomain('UserPoolDomain', { + cognitoDomain: { + domainPrefix: 'userpooldomain', + }, + }); + const imported = UserPool.fromUserPoolId(stack, 'imported', 'imported-userpool-id'); + imported.addDomain('UserPoolImportedDomain', { + cognitoDomain: { + domainPrefix: 'userpoolimporteddomain', + }, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolDomain', { + Domain: 'userpooldomain', + UserPoolId: stack.resolve(userpool.userPoolId), + }); + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolDomain', { + Domain: 'userpoolimporteddomain', + UserPoolId: stack.resolve(imported.userPoolId), + }); +}); + function fooFunction(scope: Construct, name: string): lambda.IFunction { return new lambda.Function(scope, name, { functionName: name, diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index f63ff13a25cf4..1c1802f039153 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -412,7 +412,7 @@ export interface ITable extends IResource { export interface TableAttributes { /** * The ARN of the dynamodb table. - * One of this, or {@link tabeName}, is required. + * One of this, or {@link tableName}, is required. * * @default - no table arn */ @@ -420,7 +420,7 @@ export interface TableAttributes { /** * The table name of the dynamodb table. - * One of this, or {@link tabeArn}, is required. + * One of this, or {@link tableArn}, is required. * * @default - no table name */ @@ -439,6 +439,28 @@ export interface TableAttributes { * @default - no key */ readonly encryptionKey?: kms.IKey; + + /** + * The name of the global indexes set for this Table. + * Note that you need to set either this property, + * or {@link localIndexes}, + * if you want methods like grantReadData() + * to grant permissions for indexes as well as the table itself. + * + * @default - no global indexes + */ + readonly globalIndexes?: string[]; + + /** + * The name of the local indexes set for this Table. + * Note that you need to set either this property, + * or {@link globalIndexes}, + * if you want methods like grantReadData() + * to grant permissions for indexes as well as the table itself. + * + * @default - no local indexes + */ + readonly localIndexes?: string[]; } abstract class TableBase extends Resource implements ITable { @@ -682,7 +704,7 @@ abstract class TableBase extends Resource implements ITable { private combinedGrant( grantee: iam.IGrantable, opts: {keyActions?: string[], tableActions?: string[], streamActions?: string[]}, - ) { + ): iam.Grant { if (opts.tableActions) { const resources = [this.tableArn, Lazy.stringValue({ produce: () => this.hasIndex ? `${this.tableArn}/index/*` : Aws.NO_VALUE }), @@ -773,6 +795,8 @@ export class Table extends TableBase { public readonly tableArn: string; public readonly tableStreamArn?: string; public readonly encryptionKey?: kms.IKey; + protected readonly hasIndex = (attrs.globalIndexes ?? []).length > 0 || + (attrs.localIndexes ?? []).length > 0; constructor(_tableArn: string, tableName: string, tableStreamArn?: string) { super(scope, id); @@ -781,10 +805,6 @@ export class Table extends TableBase { this.tableStreamArn = tableStreamArn; this.encryptionKey = attrs.encryptionKey; } - - protected get hasIndex(): boolean { - return false; - } } let name: string; diff --git a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts index 068cfaf5b0edb..c0c0fe9633ac0 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts @@ -2182,6 +2182,63 @@ describe('import', () => { Roles: [stack.resolve(role.roleName)], }); }); + + test('creates the correct index grant if indexes have been provided when importing', () => { + const stack = new Stack(); + + const table = Table.fromTableAttributes(stack, 'ImportedTable', { + tableName: 'MyTableName', + globalIndexes: ['global'], + localIndexes: ['local'], + }); + + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.AnyPrincipal(), + }); + + table.grantReadData(role); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'dynamodb:BatchGetItem', + 'dynamodb:GetRecords', + 'dynamodb:GetShardIterator', + 'dynamodb:Query', + 'dynamodb:GetItem', + 'dynamodb:Scan', + ], + Resource: [ + { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':dynamodb:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':table/MyTableName', + ]], + }, + { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':dynamodb:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':table/MyTableName/index/*', + ]], + }, + ], + }, + ], + }, + }); + }); }); }); diff --git a/packages/@aws-cdk/aws-events-targets/README.md b/packages/@aws-cdk/aws-events-targets/README.md index e599a7c067f4f..57d52739fe23f 100644 --- a/packages/@aws-cdk/aws-events-targets/README.md +++ b/packages/@aws-cdk/aws-events-targets/README.md @@ -22,6 +22,7 @@ Currently supported are: * Start a StepFunctions state machine * Queue a Batch job * Make an AWS API call +* Put a record to a Kinesis stream See the README of the `@aws-cdk/aws-events` library for more information on CloudWatch Events. diff --git a/packages/@aws-cdk/aws-events-targets/lib/index.ts b/packages/@aws-cdk/aws-events-targets/lib/index.ts index 3ad01340cfbe5..7031423e6b739 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/index.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/index.ts @@ -8,3 +8,4 @@ export * from './lambda'; export * from './ecs-task-properties'; export * from './ecs-task'; export * from './state-machine'; +export * from './kinesis-stream'; diff --git a/packages/@aws-cdk/aws-events-targets/lib/kinesis-stream.ts b/packages/@aws-cdk/aws-events-targets/lib/kinesis-stream.ts new file mode 100644 index 0000000000000..535a8b3923b51 --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/lib/kinesis-stream.ts @@ -0,0 +1,63 @@ +import * as events from '@aws-cdk/aws-events'; +import * as iam from '@aws-cdk/aws-iam'; +import * as kinesis from '@aws-cdk/aws-kinesis'; +import { singletonEventRole } from './util'; + +/** + * Customize the Kinesis Stream Event Target + */ +export interface KinesisStreamProps { + /** + * Partition Key Path for records sent to this stream + * + * @default - eventId as the partition key + */ + readonly partitionKeyPath?: string; + + /** + * The message to send to the stream. + * + * Must be a valid JSON text passed to the target stream. + * + * @default - the entire CloudWatch event + */ + readonly message?: events.RuleTargetInput; + +} + +/** + * Use a Kinesis Stream as a target for AWS CloudWatch event rules. + * + * @example + * + * // put to a Kinesis stream every time code is committed + * // to a CodeCommit repository + * repository.onCommit(new targets.KinesisStream(stream)); + * + */ +export class KinesisStream implements events.IRuleTarget { + + constructor(private readonly stream: kinesis.IStream, private readonly props: KinesisStreamProps = {}) { + } + + /** + * Returns a RuleTarget that can be used to trigger this Kinesis Stream as a + * result from a CloudWatch event. + */ + public bind(_rule: events.IRule, _id?: string): events.RuleTargetConfig { + const policyStatements = [new iam.PolicyStatement({ + actions: ['kinesis:PutRecord', 'kinesis:PutRecords'], + resources: [this.stream.streamArn], + })]; + + return { + id: '', + arn: this.stream.streamArn, + role: singletonEventRole(this.stream, policyStatements), + input: this.props.message, + targetResource: this.stream, + kinesisParameters: this.props.partitionKeyPath ? { partitionKeyPath: this.props.partitionKeyPath } : undefined, + }; + } + +} diff --git a/packages/@aws-cdk/aws-events-targets/package.json b/packages/@aws-cdk/aws-events-targets/package.json index 0216eabf50638..f51b406d5249f 100644 --- a/packages/@aws-cdk/aws-events-targets/package.json +++ b/packages/@aws-cdk/aws-events-targets/package.json @@ -88,6 +88,7 @@ "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/aws-stepfunctions": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", + "@aws-cdk/aws-kinesis": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, @@ -106,7 +107,8 @@ "@aws-cdk/aws-stepfunctions": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", "@aws-cdk/core": "0.0.0", - "constructs": "^3.0.2" + "constructs": "^3.0.2", + "@aws-cdk/aws-kinesis": "0.0.0" }, "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" diff --git a/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.expected.json b/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.expected.json new file mode 100644 index 0000000000000..460d13d03e0ca --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.expected.json @@ -0,0 +1,118 @@ +{ + "Resources":{ + "MyStream5C050E93":{ + "Type":"AWS::Kinesis::Stream", + "Properties":{ + "ShardCount":1, + "RetentionPeriodHours":24, + "StreamEncryption":{ + "Fn::If":[ + "AwsCdkKinesisEncryptedStreamsUnsupportedRegions", + { + "Ref":"AWS::NoValue" + }, + { + "EncryptionType":"KMS", + "KeyId":"alias/aws/kinesis" + } + ] + } + } + }, + "MyStreamEventsRole5B6CC6AF":{ + "Type":"AWS::IAM::Role", + "Properties":{ + "AssumeRolePolicyDocument":{ + "Statement":[ + { + "Action":"sts:AssumeRole", + "Effect":"Allow", + "Principal":{ + "Service":"events.amazonaws.com" + } + } + ], + "Version":"2012-10-17" + } + } + }, + "MyStreamEventsRoleDefaultPolicy2089B49E":{ + "Type":"AWS::IAM::Policy", + "Properties":{ + "PolicyDocument":{ + "Statement":[ + { + "Action":[ + "kinesis:PutRecord", + "kinesis:PutRecords" + ], + "Effect":"Allow", + "Resource":{ + "Fn::GetAtt":[ + "MyStream5C050E93", + "Arn" + ] + } + } + ], + "Version":"2012-10-17" + }, + "PolicyName":"MyStreamEventsRoleDefaultPolicy2089B49E", + "Roles":[ + { + "Ref":"MyStreamEventsRole5B6CC6AF" + } + ] + } + }, + "EveryMinute2BBCEA8F":{ + "Type":"AWS::Events::Rule", + "Properties":{ + "ScheduleExpression":"rate(1 minute)", + "State":"ENABLED", + "Targets":[ + { + "Arn":{ + "Fn::GetAtt":[ + "MyStream5C050E93", + "Arn" + ] + }, + "Id":"Target0", + "KinesisParameters":{ + "PartitionKeyPath":"$.id" + }, + "RoleArn":{ + "Fn::GetAtt":[ + "MyStreamEventsRole5B6CC6AF", + "Arn" + ] + } + } + ] + } + } + }, + "Conditions":{ + "AwsCdkKinesisEncryptedStreamsUnsupportedRegions":{ + "Fn::Or":[ + { + "Fn::Equals":[ + { + "Ref":"AWS::Region" + }, + "cn-north-1" + ] + }, + { + "Fn::Equals":[ + { + "Ref":"AWS::Region" + }, + "cn-northwest-1" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.ts b/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.ts new file mode 100644 index 0000000000000..5174aefa255b0 --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.ts @@ -0,0 +1,22 @@ +import * as events from '@aws-cdk/aws-events'; +import * as kinesis from '@aws-cdk/aws-kinesis'; +import * as cdk from '@aws-cdk/core'; +import * as targets from '../../lib'; + +// --------------------------------- +// Define a rule that triggers a put to a Kinesis stream every 1min. + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-kinesis-event-target'); + +const stream = new kinesis.Stream(stack, 'MyStream'); +const event = new events.Rule(stack, 'EveryMinute', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), +}); + +event.addTarget(new targets.KinesisStream(stream, { + partitionKeyPath: events.EventField.eventId, +})); + +app.synth(); diff --git a/packages/@aws-cdk/aws-events-targets/test/kinesis/kinesis-stream.test.ts b/packages/@aws-cdk/aws-events-targets/test/kinesis/kinesis-stream.test.ts new file mode 100644 index 0000000000000..67caca7d781ea --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/test/kinesis/kinesis-stream.test.ts @@ -0,0 +1,103 @@ +import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; +import * as events from '@aws-cdk/aws-events'; +import * as kinesis from '@aws-cdk/aws-kinesis'; +import { Stack } from '@aws-cdk/core'; +import * as targets from '../../lib'; + +describe('KinesisStream event target', () => { + let stack: Stack; + let stream: kinesis.Stream; + let streamArn: any; + + beforeEach(() => { + stack = new Stack(); + stream = new kinesis.Stream(stack, 'MyStream'); + streamArn = { 'Fn::GetAtt': [ 'MyStream5C050E93', 'Arn' ] }; + }); + + describe('when added to an event rule as a target', () => { + let rule: events.Rule; + + beforeEach(() => { + rule = new events.Rule(stack, 'rule', { + schedule: events.Schedule.expression('rate(1 minute)'), + }); + }); + + describe('with default settings', () => { + beforeEach(() => { + rule.addTarget(new targets.KinesisStream(stream)); + }); + + test("adds the stream's ARN and role to the targets of the rule", () => { + expect(stack).to(haveResource('AWS::Events::Rule', { + Targets: [ + { + Arn: streamArn, + Id: 'Target0', + RoleArn: { 'Fn::GetAtt': [ 'MyStreamEventsRole5B6CC6AF', 'Arn' ] }, + }, + ], + })); + }); + + test("creates a policy that has PutRecord and PutRecords permissions on the stream's ARN", () => { + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ 'kinesis:PutRecord', 'kinesis:PutRecords' ], + Effect: 'Allow', + Resource: streamArn, + }, + ], + Version: '2012-10-17', + }, + })); + }); + }); + + describe('with an explicit partition key path', () => { + beforeEach(() => { + rule.addTarget(new targets.KinesisStream(stream, { + partitionKeyPath: events.EventField.eventId, + })); + }); + + test('sets the partition key path', () => { + expect(stack).to(haveResourceLike('AWS::Events::Rule', { + Targets: [ + { + Arn: streamArn, + Id: 'Target0', + RoleArn: { 'Fn::GetAtt': [ 'MyStreamEventsRole5B6CC6AF', 'Arn' ] }, + KinesisParameters: { + PartitionKeyPath: '$.id', + }, + }, + ], + })); + }); + }); + + describe('with an explicit message', () => { + beforeEach(() => { + rule.addTarget(new targets.KinesisStream(stream, { + message: events.RuleTargetInput.fromText('fooBar'), + })); + }); + + test('sets the input', () => { + expect(stack).to(haveResourceLike('AWS::Events::Rule', { + Targets: [ + { + Arn: streamArn, + Id: 'Target0', + Input: '"fooBar"', + }, + ], + })); + }); + }); + }); +}); diff --git a/packages/@aws-cdk/aws-globalaccelerator/.eslintrc.js b/packages/@aws-cdk/aws-globalaccelerator/.eslintrc.js new file mode 100644 index 0000000000000..a9d39af55b7e5 --- /dev/null +++ b/packages/@aws-cdk/aws-globalaccelerator/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-globalaccelerator/.gitignore b/packages/@aws-cdk/aws-globalaccelerator/.gitignore new file mode 100644 index 0000000000000..e9fee23607e76 --- /dev/null +++ b/packages/@aws-cdk/aws-globalaccelerator/.gitignore @@ -0,0 +1,19 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +tslint.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk +nyc.config.js +!.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-globalaccelerator/.npmignore b/packages/@aws-cdk/aws-globalaccelerator/.npmignore new file mode 100644 index 0000000000000..fb37683c5a457 --- /dev/null +++ b/packages/@aws-cdk/aws-globalaccelerator/.npmignore @@ -0,0 +1,23 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json + +.eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-globalaccelerator/LICENSE b/packages/@aws-cdk/aws-globalaccelerator/LICENSE new file mode 100644 index 0000000000000..b71ec1688783a --- /dev/null +++ b/packages/@aws-cdk/aws-globalaccelerator/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-globalaccelerator/NOTICE b/packages/@aws-cdk/aws-globalaccelerator/NOTICE new file mode 100644 index 0000000000000..bfccac9a7f69c --- /dev/null +++ b/packages/@aws-cdk/aws-globalaccelerator/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-globalaccelerator/README.md b/packages/@aws-cdk/aws-globalaccelerator/README.md new file mode 100644 index 0000000000000..1765922b5589b --- /dev/null +++ b/packages/@aws-cdk/aws-globalaccelerator/README.md @@ -0,0 +1,16 @@ +## AWS::GlobalAccelerator Construct Library + +--- + +![cfn-resources: Stable](https://img.shields.io/badge/cfn--resources-stable-success.svg?style=for-the-badge) + +> All classes with the `Cfn` prefix in this module ([CFN Resources](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) are always stable and safe to use. + +--- + + +This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. + +```ts +import globalaccelerator = require('@aws-cdk/aws-globalaccelerator'); +``` diff --git a/packages/@aws-cdk/aws-globalaccelerator/jest.config.js b/packages/@aws-cdk/aws-globalaccelerator/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-globalaccelerator/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-globalaccelerator/lib/index.ts b/packages/@aws-cdk/aws-globalaccelerator/lib/index.ts new file mode 100644 index 0000000000000..32d3860d45724 --- /dev/null +++ b/packages/@aws-cdk/aws-globalaccelerator/lib/index.ts @@ -0,0 +1,2 @@ +// AWS::GlobalAccelerator CloudFormation Resources: +export * from './globalaccelerator.generated'; diff --git a/packages/@aws-cdk/aws-globalaccelerator/package.json b/packages/@aws-cdk/aws-globalaccelerator/package.json new file mode 100644 index 0000000000000..4fab6990574a5 --- /dev/null +++ b/packages/@aws-cdk/aws-globalaccelerator/package.json @@ -0,0 +1,87 @@ +{ + "name": "@aws-cdk/aws-globalaccelerator", + "version": "0.0.0", + "description": "The CDK Construct Library for AWS::GlobalAccelerator", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "dotnet": { + "namespace": "Amazon.CDK.AWS.GlobalAccelerator", + "packageId": "Amazon.CDK.AWS.GlobalAccelerator", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "java": { + "package": "software.amazon.awscdk.services.globalaccelerator", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "globalaccelerator" + } + }, + "python": { + "distName": "aws-cdk.aws-globalaccelerator", + "module": "aws_cdk.aws_globalaccelerator" + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-globalaccelerator" + }, + "homepage": "https://github.com/aws/aws-cdk", + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "cfn2ts": "cfn2ts", + "build+test+package": "npm run build+test && npm run package", + "build+test": "npm run build && npm test", + "compat": "cdk-compat" + }, + "cdk-build": { + "cloudformation": "AWS::GlobalAccelerator", + "jest": true + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "AWS::GlobalAccelerator", + "aws-globalaccelerator" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assert": "0.0.0", + "cdk-build-tools": "0.0.0", + "cfn2ts": "0.0.0", + "pkglint": "0.0.0" + }, + "dependencies": { + "@aws-cdk/core": "0.0.0" + }, + "peerDependencies": { + "@aws-cdk/core": "0.0.0" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "cfn-only", + "awscdkio": { + "announce": false + } +} diff --git a/packages/@aws-cdk/aws-redshift/test/redshift.test.ts b/packages/@aws-cdk/aws-globalaccelerator/test/globalaccelerator.test.ts similarity index 100% rename from packages/@aws-cdk/aws-redshift/test/redshift.test.ts rename to packages/@aws-cdk/aws-globalaccelerator/test/globalaccelerator.test.ts diff --git a/packages/@aws-cdk/aws-imagebuilder/.eslintrc.js b/packages/@aws-cdk/aws-imagebuilder/.eslintrc.js new file mode 100644 index 0000000000000..a9d39af55b7e5 --- /dev/null +++ b/packages/@aws-cdk/aws-imagebuilder/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-imagebuilder/.gitignore b/packages/@aws-cdk/aws-imagebuilder/.gitignore new file mode 100644 index 0000000000000..e9fee23607e76 --- /dev/null +++ b/packages/@aws-cdk/aws-imagebuilder/.gitignore @@ -0,0 +1,19 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +tslint.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk +nyc.config.js +!.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-imagebuilder/.npmignore b/packages/@aws-cdk/aws-imagebuilder/.npmignore new file mode 100644 index 0000000000000..fb37683c5a457 --- /dev/null +++ b/packages/@aws-cdk/aws-imagebuilder/.npmignore @@ -0,0 +1,23 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json + +.eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-imagebuilder/LICENSE b/packages/@aws-cdk/aws-imagebuilder/LICENSE new file mode 100644 index 0000000000000..b71ec1688783a --- /dev/null +++ b/packages/@aws-cdk/aws-imagebuilder/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-imagebuilder/NOTICE b/packages/@aws-cdk/aws-imagebuilder/NOTICE new file mode 100644 index 0000000000000..bfccac9a7f69c --- /dev/null +++ b/packages/@aws-cdk/aws-imagebuilder/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-imagebuilder/README.md b/packages/@aws-cdk/aws-imagebuilder/README.md new file mode 100644 index 0000000000000..ef5f4bc99f7fa --- /dev/null +++ b/packages/@aws-cdk/aws-imagebuilder/README.md @@ -0,0 +1,16 @@ +## AWS::ImageBuilder Construct Library + +--- + +![cfn-resources: Stable](https://img.shields.io/badge/cfn--resources-stable-success.svg?style=for-the-badge) + +> All classes with the `Cfn` prefix in this module ([CFN Resources](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) are always stable and safe to use. + +--- + + +This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. + +```ts +import imagebuilder = require('@aws-cdk/aws-imagebuilder'); +``` diff --git a/packages/@aws-cdk/aws-imagebuilder/jest.config.js b/packages/@aws-cdk/aws-imagebuilder/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-imagebuilder/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-imagebuilder/lib/index.ts b/packages/@aws-cdk/aws-imagebuilder/lib/index.ts new file mode 100644 index 0000000000000..4f8727183ba0d --- /dev/null +++ b/packages/@aws-cdk/aws-imagebuilder/lib/index.ts @@ -0,0 +1,2 @@ +// AWS::ImageBuilder CloudFormation Resources: +export * from './imagebuilder.generated'; diff --git a/packages/@aws-cdk/aws-imagebuilder/package.json b/packages/@aws-cdk/aws-imagebuilder/package.json new file mode 100644 index 0000000000000..4137199aeba1f --- /dev/null +++ b/packages/@aws-cdk/aws-imagebuilder/package.json @@ -0,0 +1,87 @@ +{ + "name": "@aws-cdk/aws-imagebuilder", + "version": "0.0.0", + "description": "The CDK Construct Library for AWS::ImageBuilder", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "dotnet": { + "namespace": "Amazon.CDK.AWS.ImageBuilder", + "packageId": "Amazon.CDK.AWS.ImageBuilder", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "java": { + "package": "software.amazon.awscdk.services.imagebuilder", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "imagebuilder" + } + }, + "python": { + "distName": "aws-cdk.aws-imagebuilder", + "module": "aws_cdk.aws_imagebuilder" + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-imagebuilder" + }, + "homepage": "https://github.com/aws/aws-cdk", + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "cfn2ts": "cfn2ts", + "build+test+package": "npm run build+test && npm run package", + "build+test": "npm run build && npm test", + "compat": "cdk-compat" + }, + "cdk-build": { + "cloudformation": "AWS::ImageBuilder", + "jest": true + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "AWS::ImageBuilder", + "aws-imagebuilder" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assert": "0.0.0", + "cdk-build-tools": "0.0.0", + "cfn2ts": "0.0.0", + "pkglint": "0.0.0" + }, + "dependencies": { + "@aws-cdk/core": "0.0.0" + }, + "peerDependencies": { + "@aws-cdk/core": "0.0.0" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "cfn-only", + "awscdkio": { + "announce": false + } +} diff --git a/packages/@aws-cdk/aws-imagebuilder/test/imagebuilder.test.ts b/packages/@aws-cdk/aws-imagebuilder/test/imagebuilder.test.ts new file mode 100644 index 0000000000000..e394ef336bfb4 --- /dev/null +++ b/packages/@aws-cdk/aws-imagebuilder/test/imagebuilder.test.ts @@ -0,0 +1,6 @@ +import '@aws-cdk/assert/jest'; +import {} from '../lib'; + +test('No tests are specified for this package', () => { + expect(true).toBe(true); +}); diff --git a/packages/@aws-cdk/aws-lambda-nodejs/README.md b/packages/@aws-cdk/aws-lambda-nodejs/README.md index 2063950e3dedb..9643aff6f3ab1 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/README.md +++ b/packages/@aws-cdk/aws-lambda-nodejs/README.md @@ -41,6 +41,17 @@ new lambda.NodejsFunction(this, 'MyFunction', { All other properties of `lambda.Function` are supported, see also the [AWS Lambda construct library](https://github.com/aws/aws-cdk/tree/master/packages/%40aws-cdk/aws-lambda). +Use the `containerEnvironment` prop to pass environments variables to the Docker container +running Parcel: + +```ts +new lambda.NodejsFunction(this, 'my-handler', { + containerEnvironment: { + NODE_ENV: 'production', + }, +}); +``` + ### Configuring Parcel The `NodejsFunction` construct exposes some [Parcel](https://parceljs.org/) options via properties: `minify`, `sourceMaps`, `buildDir` and `cacheDir`. diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/builder.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/builder.ts index dd8e3ba2f8565..20ddcfd45c54d 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/builder.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/builder.ts @@ -54,6 +54,13 @@ export interface BuilderOptions { * mounted in the Docker container. */ readonly projectRoot: string; + + /** + * The environment variables to pass to the container running Parcel. + * + * @default - no environment variables are passed to the container + */ + readonly environment?: { [key: string]: string; }; } /** @@ -111,6 +118,7 @@ export class Builder { '-v', `${this.options.projectRoot}:${containerProjectRoot}`, '-v', `${path.resolve(this.options.outDir)}:${containerOutDir}`, ...(this.options.cacheDir ? ['-v', `${path.resolve(this.options.cacheDir)}:${containerCacheDir}`] : []), + ...flatten(Object.entries(this.options.environment || {}).map(([k, v]) => ['--env', `${k}=${v}`])), '-w', path.dirname(containerEntryPath).replace(/\\/g, '/'), // Always use POSIX paths in the container 'parcel-bundler', ]; @@ -164,3 +172,7 @@ export class Builder { fs.writeFileSync(this.pkgPath, this.originalPkg); } } + +function flatten(x: string[][]) { + return Array.prototype.concat([], ...x); +} diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts index 276885e5a22d3..82c7b2df7833b 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts @@ -84,6 +84,13 @@ export interface NodejsFunctionProps extends lambda.FunctionOptions { * @default - the closest path containing a .git folder */ readonly projectRoot?: string; + + /** + * The environment variables to pass to the container running Parcel. + * + * @default - no environment variables are passed to the container + */ + readonly containerEnvironment?: { [key: string]: string; }; } /** @@ -119,6 +126,7 @@ export class NodejsFunction extends lambda.Function { nodeVersion: extractVersion(runtime), nodeDockerTag: props.nodeDockerTag || `${process.versions.node}-alpine`, projectRoot: path.resolve(projectRoot), + environment: props.containerEnvironment, }); builder.build(); diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/builder.test.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/builder.test.ts index 6c7f5e41ae3e0..55502d783ec26 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/builder.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/builder.test.ts @@ -80,6 +80,30 @@ test('with Windows paths', () => { ])); }); +test('with env vars', () => { + const builder = new Builder({ + entry: '/project/folder/entry.ts', + global: 'handler', + outDir: '/out-dir', + cacheDir: '/cache-dir', + nodeDockerTag: 'lts-alpine', + nodeVersion: '12', + projectRoot: '/project', + environment: { + KEY1: 'VALUE1', + KEY2: 'VALUE2', + }, + }); + builder.build(); + + // docker run + expect(spawnSync).toHaveBeenCalledWith('docker', expect.arrayContaining([ + 'run', + '--env', 'KEY1=VALUE1', + '--env', 'KEY2=VALUE2', + ])); +}); + test('throws in case of error', () => { const builder = new Builder({ entry: '/project/folder/error', diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts index 7a31b8fea17f0..bd3bbeb5a0d9c 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts @@ -54,6 +54,21 @@ test('NodejsFunction with .js handler', () => { })); }); +test('NodejsFunction with container env vars', () => { + // WHEN + new NodejsFunction(stack, 'handler1', { + containerEnvironment: { + KEY: 'VALUE', + }, + }); + + expect(Builder).toHaveBeenCalledWith(expect.objectContaining({ + environment: { + KEY: 'VALUE', + }, + })); +}); + test('throws when entry is not js/ts', () => { expect(() => new NodejsFunction(stack, 'Fn', { entry: 'handler.py', diff --git a/packages/@aws-cdk/aws-lambda/lib/function-base.ts b/packages/@aws-cdk/aws-lambda/lib/function-base.ts index 65a6fe6c05c26..b9a2e6b4ef166 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function-base.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function-base.ts @@ -284,7 +284,7 @@ export abstract class FunctionBase extends Resource implements IFunction { action: 'lambda:InvokeFunction', }); - return { statementAdded: true, policyDependable: this.node.findChild(identifier) } as iam.AddToResourcePolicyResult; + return { statementAdded: true, policyDependable: this._functionNode().findChild(identifier) } as iam.AddToResourcePolicyResult; }, node: this.node, }, @@ -318,6 +318,15 @@ export abstract class FunctionBase extends Resource implements IFunction { }); } + /** + * Returns the construct tree node that corresponds to the lambda function. + * For use internally for constructs, when the tree is set up in non-standard ways. Ex: SingletonFunction. + * @internal + */ + protected _functionNode(): ConstructNode { + return this.node; + } + private parsePermissionPrincipal(principal?: iam.IPrincipal) { if (!principal) { return undefined; diff --git a/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts b/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts index d9bf1f97372a0..f8515dc84e841 100644 --- a/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts +++ b/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts @@ -81,6 +81,14 @@ export class SingletonFunction extends FunctionBase { down.node.addDependency(this.lambdaFunction); } + /** + * Returns the construct tree node that corresponds to the lambda function. + * @internal + */ + protected _functionNode(): cdk.ConstructNode { + return this.lambdaFunction.node; + } + private ensureLambda(props: SingletonFunctionProps): IFunction { const constructName = (props.lambdaPurpose || 'SingletonLambda') + slugify(props.uuid); const existing = cdk.Stack.of(this).node.tryFindChild(constructName); diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index 292d921efb0fa..f890a203545a3 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -68,9 +68,9 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/aws-lambda": "^8.10.39", - "@types/lodash": "^4.14.152", + "@types/lodash": "^4.14.153", "@types/nodeunit": "^0.0.31", - "@types/sinon": "^9.0.3", + "@types/sinon": "^9.0.4", "aws-sdk": "^2.681.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda/test/test.singleton-lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.singleton-lambda.ts index 5f815d8f5e237..05512ec54b2f0 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.singleton-lambda.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.singleton-lambda.ts @@ -113,4 +113,32 @@ export = { test.done(); }, + + 'grantInvoke works correctly'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const singleton = new lambda.SingletonFunction(stack, 'Singleton', { + uuid: '84c0de93-353f-4217-9b0b-45b6c993251a', + code: new lambda.InlineCode('def hello(): pass'), + runtime: lambda.Runtime.PYTHON_2_7, + handler: 'index.hello', + }); + + // WHEN + const invokeResult = singleton.grantInvoke(new iam.ServicePrincipal('events.amazonaws.com')); + const statement = stack.resolve(invokeResult.resourceStatement); + + // THEN + expect(stack).to(haveResource('AWS::Lambda::Permission', { + Action: 'lambda:InvokeFunction', + Principal: 'events.amazonaws.com', + })); + test.deepEqual(statement.action, [ 'lambda:InvokeFunction' ]); + test.deepEqual(statement.principal, { Service: [ 'events.amazonaws.com' ] }); + test.deepEqual(statement.effect, 'Allow'); + test.deepEqual(statement.resource, [{ + 'Fn::GetAtt': [ 'SingletonLambda84c0de93353f42179b0b45b6c993251a840BCC38', 'Arn' ], + }]); + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-macie/.eslintrc.js b/packages/@aws-cdk/aws-macie/.eslintrc.js new file mode 100644 index 0000000000000..a9d39af55b7e5 --- /dev/null +++ b/packages/@aws-cdk/aws-macie/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-macie/.gitignore b/packages/@aws-cdk/aws-macie/.gitignore new file mode 100644 index 0000000000000..e9fee23607e76 --- /dev/null +++ b/packages/@aws-cdk/aws-macie/.gitignore @@ -0,0 +1,19 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +tslint.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk +nyc.config.js +!.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-macie/.npmignore b/packages/@aws-cdk/aws-macie/.npmignore new file mode 100644 index 0000000000000..fb37683c5a457 --- /dev/null +++ b/packages/@aws-cdk/aws-macie/.npmignore @@ -0,0 +1,23 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json + +.eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-macie/LICENSE b/packages/@aws-cdk/aws-macie/LICENSE new file mode 100644 index 0000000000000..b71ec1688783a --- /dev/null +++ b/packages/@aws-cdk/aws-macie/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-macie/NOTICE b/packages/@aws-cdk/aws-macie/NOTICE new file mode 100644 index 0000000000000..bfccac9a7f69c --- /dev/null +++ b/packages/@aws-cdk/aws-macie/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-macie/README.md b/packages/@aws-cdk/aws-macie/README.md new file mode 100644 index 0000000000000..9f4352257b41e --- /dev/null +++ b/packages/@aws-cdk/aws-macie/README.md @@ -0,0 +1,16 @@ +## AWS::Macie Construct Library + +--- + +![cfn-resources: Stable](https://img.shields.io/badge/cfn--resources-stable-success.svg?style=for-the-badge) + +> All classes with the `Cfn` prefix in this module ([CFN Resources](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) are always stable and safe to use. + +--- + + +This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. + +```ts +import macie = require('@aws-cdk/aws-macie'); +``` diff --git a/packages/@aws-cdk/aws-macie/jest.config.js b/packages/@aws-cdk/aws-macie/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-macie/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-macie/lib/index.ts b/packages/@aws-cdk/aws-macie/lib/index.ts new file mode 100644 index 0000000000000..3e9701436c653 --- /dev/null +++ b/packages/@aws-cdk/aws-macie/lib/index.ts @@ -0,0 +1,2 @@ +// AWS::Macie CloudFormation Resources: +export * from './macie.generated'; diff --git a/packages/@aws-cdk/aws-macie/package.json b/packages/@aws-cdk/aws-macie/package.json new file mode 100644 index 0000000000000..ec9bf60d76782 --- /dev/null +++ b/packages/@aws-cdk/aws-macie/package.json @@ -0,0 +1,87 @@ +{ + "name": "@aws-cdk/aws-macie", + "version": "0.0.0", + "description": "The CDK Construct Library for AWS::Macie", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "dotnet": { + "namespace": "Amazon.CDK.AWS.Macie", + "packageId": "Amazon.CDK.AWS.Macie", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "java": { + "package": "software.amazon.awscdk.services.macie", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "macie" + } + }, + "python": { + "distName": "aws-cdk.aws-macie", + "module": "aws_cdk.aws_macie" + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-macie" + }, + "homepage": "https://github.com/aws/aws-cdk", + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "cfn2ts": "cfn2ts", + "build+test+package": "npm run build+test && npm run package", + "build+test": "npm run build && npm test", + "compat": "cdk-compat" + }, + "cdk-build": { + "cloudformation": "AWS::Macie", + "jest": true + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "AWS::Macie", + "aws-macie" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assert": "0.0.0", + "cdk-build-tools": "0.0.0", + "cfn2ts": "0.0.0", + "pkglint": "0.0.0" + }, + "dependencies": { + "@aws-cdk/core": "0.0.0" + }, + "peerDependencies": { + "@aws-cdk/core": "0.0.0" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "cfn-only", + "awscdkio": { + "announce": false + } +} diff --git a/packages/@aws-cdk/aws-macie/test/macie.test.ts b/packages/@aws-cdk/aws-macie/test/macie.test.ts new file mode 100644 index 0000000000000..e394ef336bfb4 --- /dev/null +++ b/packages/@aws-cdk/aws-macie/test/macie.test.ts @@ -0,0 +1,6 @@ +import '@aws-cdk/assert/jest'; +import {} from '../lib'; + +test('No tests are specified for this package', () => { + expect(true).toBe(true); +}); diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index b3196514f46ce..6f75c650c3fce 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -3,7 +3,7 @@ import { IRole, ManagedPolicy, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as s3 from '@aws-cdk/aws-s3'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; -import { Construct, Duration, RemovalPolicy, Resource, Token } from '@aws-cdk/core'; +import { CfnDeletionPolicy, Construct, Duration, RemovalPolicy, Resource, Token } from '@aws-cdk/core'; import { DatabaseClusterAttributes, IDatabaseCluster } from './cluster-ref'; import { DatabaseSecret } from './database-secret'; import { Endpoint } from './endpoint'; @@ -124,9 +124,9 @@ export interface DatabaseClusterProps { * The removal policy to apply when the cluster and its instances are removed * from the stack or replaced during an update. * - * @default - Retain cluster. + * @default - RemovalPolicy.SNAPSHOT (remove the cluster and instances, but retain a snapshot of the data) */ - readonly removalPolicy?: RemovalPolicy + readonly removalPolicy?: RemovalPolicy; /** * The interval, in seconds, between points when Amazon RDS collects enhanced @@ -354,6 +354,9 @@ export class DatabaseCluster extends DatabaseClusterBase { dbSubnetGroupDescription: `Subnets for ${id} database`, subnetIds, }); + if (props.removalPolicy === RemovalPolicy.RETAIN) { + subnetGroup.applyRemovalPolicy(RemovalPolicy.RETAIN); + } const securityGroup = props.instanceProps.securityGroup !== undefined ? props.instanceProps.securityGroup : new ec2.SecurityGroup(this, 'SecurityGroup', { @@ -461,9 +464,16 @@ export class DatabaseCluster extends DatabaseClusterBase { storageEncrypted: props.kmsKey ? true : props.storageEncrypted, }); - cluster.applyRemovalPolicy(props.removalPolicy, { - applyToUpdateReplacePolicy: true, - }); + // if removalPolicy was not specified, + // leave it as the default, which is Snapshot + if (props.removalPolicy) { + cluster.applyRemovalPolicy(props.removalPolicy); + } else { + // The CFN default makes sense for DeletionPolicy, + // but doesn't cover UpdateReplacePolicy. + // Fix that here. + cluster.cfnOptions.updateReplacePolicy = CfnDeletionPolicy.SNAPSHOT; + } this.clusterIdentifier = cluster.ref; @@ -519,9 +529,13 @@ export class DatabaseCluster extends DatabaseClusterBase { monitoringRoleArn: monitoringRole && monitoringRole.roleArn, }); - instance.applyRemovalPolicy(props.removalPolicy, { - applyToUpdateReplacePolicy: true, - }); + // If removalPolicy isn't explicitly set, + // it's Snapshot for Cluster. + // Because of that, in this case, + // we can safely use the CFN default of Delete for DbInstances with dbClusterIdentifier set. + if (props.removalPolicy) { + instance.applyRemovalPolicy(props.removalPolicy); + } // We must have a dependency on the NAT gateway provider here to create // things in the right order. diff --git a/packages/@aws-cdk/aws-rds/lib/instance.ts b/packages/@aws-cdk/aws-rds/lib/instance.ts index 3b58a7d25f175..103ecc5df17bf 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance.ts @@ -5,7 +5,7 @@ import * as kms from '@aws-cdk/aws-kms'; import * as lambda from '@aws-cdk/aws-lambda'; import * as logs from '@aws-cdk/aws-logs'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; -import { Construct, Duration, IResource, Lazy, RemovalPolicy, Resource, SecretValue, Stack, Token } from '@aws-cdk/core'; +import { CfnDeletionPolicy, Construct, Duration, IResource, Lazy, RemovalPolicy, Resource, SecretValue, Stack, Token } from '@aws-cdk/core'; import { DatabaseSecret } from './database-secret'; import { Endpoint } from './endpoint'; import { IOptionGroup } from './option-group'; @@ -536,9 +536,9 @@ export interface DatabaseInstanceNewProps { * The CloudFormation policy to apply when the instance is removed from the * stack or replaced during an update. * - * @default RemovalPolicy.Retain + * @default - RemovalPolicy.SNAPSHOT (remove the resource, but retain a snapshot of the data) */ - readonly removalPolicy?: RemovalPolicy + readonly removalPolicy?: RemovalPolicy; /** * Upper limit to which RDS can scale the storage in GiB(Gibibyte). @@ -886,9 +886,7 @@ export class DatabaseInstance extends DatabaseInstanceSource implements IDatabas const portAttribute = Token.asNumber(instance.attrEndpointPort); this.instanceEndpoint = new Endpoint(instance.attrEndpointAddress, portAttribute); - instance.applyRemovalPolicy(props.removalPolicy, { - applyToUpdateReplacePolicy: true, - }); + applyInstanceDeletionPolicy(instance, props.removalPolicy); if (secret) { this.secret = secret.attach(this); @@ -984,9 +982,7 @@ export class DatabaseInstanceFromSnapshot extends DatabaseInstanceSource impleme const portAttribute = Token.asNumber(instance.attrEndpointPort); this.instanceEndpoint = new Endpoint(instance.attrEndpointAddress, portAttribute); - instance.applyRemovalPolicy(props.removalPolicy, { - applyToUpdateReplacePolicy: true, - }); + applyInstanceDeletionPolicy(instance, props.removalPolicy); if (secret) { this.secret = secret.attach(this); @@ -1054,9 +1050,7 @@ export class DatabaseInstanceReadReplica extends DatabaseInstanceNew implements const portAttribute = Token.asNumber(instance.attrEndpointPort); this.instanceEndpoint = new Endpoint(instance.attrEndpointAddress, portAttribute); - instance.applyRemovalPolicy(props.removalPolicy, { - applyToUpdateReplacePolicy: true, - }); + applyInstanceDeletionPolicy(instance, props.removalPolicy); this.setLogRetention(); } @@ -1072,3 +1066,14 @@ function renderProcessorFeatures(features: ProcessorFeatures): CfnDBInstance.Pro return featuresList.length === 0 ? undefined : featuresList; } + +function applyInstanceDeletionPolicy(cfnDbInstance: CfnDBInstance, removalPolicy: RemovalPolicy | undefined): void { + if (!removalPolicy) { + // the default DeletionPolicy is 'Snapshot', which is fine, + // but we should also make it 'Snapshot' for UpdateReplace policy + cfnDbInstance.cfnOptions.updateReplacePolicy = CfnDeletionPolicy.SNAPSHOT; + } else { + // just apply whatever removal policy the customer explicitly provided + cfnDbInstance.applyRemovalPolicy(removalPolicy); + } +} diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json b/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json index 79fa1f3e2dab7..348dba3e65ae7 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json @@ -706,8 +706,7 @@ } ] }, - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + "UpdateReplacePolicy": "Snapshot" }, "DatabaseInstance1844F58FD": { "Type": "AWS::RDS::DBInstance", @@ -725,9 +724,7 @@ "VPCPrivateSubnet1DefaultRouteAE1D6490", "VPCPrivateSubnet2DefaultRouteF4F5CFD2", "VPCPrivateSubnet3DefaultRoute27F311AE" - ], - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + ] }, "DatabaseInstance2AA380DEE": { "Type": "AWS::RDS::DBInstance", @@ -745,9 +742,7 @@ "VPCPrivateSubnet1DefaultRouteAE1D6490", "VPCPrivateSubnet2DefaultRouteF4F5CFD2", "VPCPrivateSubnet3DefaultRoute27F311AE" - ], - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + ] }, "DatabaseRotationSingleUserSecurityGroupAC6E0E73": { "Type": "AWS::EC2::SecurityGroup", @@ -817,4 +812,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.expected.json b/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.expected.json index b9dc043a54b40..710884195806a 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.expected.json @@ -668,8 +668,7 @@ } ] }, - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + "UpdateReplacePolicy": "Snapshot" }, "DatabaseInstance1844F58FD": { "Type": "AWS::RDS::DBInstance", @@ -687,9 +686,7 @@ "DependsOn": [ "VPCPublicSubnet1DefaultRoute91CEF279", "VPCPublicSubnet2DefaultRouteB7481BBA" - ], - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + ] }, "DatabaseInstance2AA380DEE": { "Type": "AWS::RDS::DBInstance", @@ -707,9 +704,7 @@ "DependsOn": [ "VPCPublicSubnet1DefaultRoute91CEF279", "VPCPublicSubnet2DefaultRouteB7481BBA" - ], - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + ] } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json b/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json index 13642f995eeb1..37f63d001843e 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json @@ -500,8 +500,7 @@ } ] }, - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + "UpdateReplacePolicy": "Snapshot" }, "DatabaseInstance1844F58FD": { "Type": "AWS::RDS::DBInstance", @@ -519,9 +518,7 @@ "DependsOn": [ "VPCPublicSubnet1DefaultRoute91CEF279", "VPCPublicSubnet2DefaultRouteB7481BBA" - ], - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + ] }, "DatabaseInstance2AA380DEE": { "Type": "AWS::RDS::DBInstance", @@ -539,9 +536,7 @@ "DependsOn": [ "VPCPublicSubnet1DefaultRoute91CEF279", "VPCPublicSubnet2DefaultRouteB7481BBA" - ], - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + ] } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json index ae832999de3b7..d5c7708151b53 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json @@ -694,8 +694,7 @@ } ] }, - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + "UpdateReplacePolicy": "Snapshot" }, "InstanceLogRetentiontrace487771C8": { "Type": "Custom::LogRetention", @@ -1122,4 +1121,4 @@ "Description": "Artifact hash for asset \"82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4e\"" } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-rds/test/test.cluster.ts b/packages/@aws-cdk/aws-rds/test/test.cluster.ts index f2f420d72b415..5293cf2f0dd1d 100644 --- a/packages/@aws-cdk/aws-rds/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-rds/test/test.cluster.ts @@ -1,4 +1,4 @@ -import { expect, haveResource, ResourcePart, SynthUtils } from '@aws-cdk/assert'; +import { ABSENT, countResources, expect, haveResource, haveResourceLike, ResourcePart, SynthUtils } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import { ManagedPolicy, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; @@ -8,7 +8,7 @@ import { Test } from 'nodeunit'; import { ClusterParameterGroup, DatabaseCluster, DatabaseClusterEngine, ParameterGroup } from '../lib'; export = { - 'check that instantiation works'(test: Test) { + 'creating a Cluster also creates 2 DB Instances'(test: Test) { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -35,17 +35,19 @@ export = { MasterUserPassword: 'tooshort', VpcSecurityGroupIds: [ {'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId']}], }, - DeletionPolicy: 'Retain', - UpdateReplacePolicy: 'Retain', + DeletionPolicy: ABSENT, + UpdateReplacePolicy: 'Snapshot', }, ResourcePart.CompleteDefinition)); + expect(stack).to(countResources('AWS::RDS::DBInstance', 2)); expect(stack).to(haveResource('AWS::RDS::DBInstance', { - DeletionPolicy: 'Retain', - UpdateReplacePolicy: 'Retain', + DeletionPolicy: ABSENT, + UpdateReplacePolicy: ABSENT, }, ResourcePart.CompleteDefinition)); test.done(); }, + 'can create a cluster with a single instance'(test: Test) { // GIVEN const stack = testStack(); @@ -146,6 +148,28 @@ export = { test.done(); }, + "sets the retention policy of the SubnetGroup to 'Retain' if the Cluster is created with 'Retain'"(test: Test) { + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc'); + + new DatabaseCluster(stack, 'Cluster', { + masterUser: { username: 'admin' }, + engine: DatabaseClusterEngine.AURORA, + instanceProps: { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.M5, ec2.InstanceSize.LARGE), + vpc, + }, + removalPolicy: cdk.RemovalPolicy.RETAIN, + }); + + expect(stack).to(haveResourceLike('AWS::RDS::DBSubnetGroup', { + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }, ResourcePart.CompleteDefinition)); + + test.done(); + }, + 'creates a secret when master credentials are not specified'(test: Test) { // GIVEN const stack = testStack(); diff --git a/packages/@aws-cdk/aws-rds/test/test.instance.ts b/packages/@aws-cdk/aws-rds/test/test.instance.ts index 8c191a05af31e..baefed5b6b157 100644 --- a/packages/@aws-cdk/aws-rds/test/test.instance.ts +++ b/packages/@aws-cdk/aws-rds/test/test.instance.ts @@ -1,4 +1,4 @@ -import { countResources, expect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import { ABSENT, countResources, expect, haveResource, ResourcePart } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as targets from '@aws-cdk/aws-events-targets'; import { ManagedPolicy, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; @@ -105,13 +105,8 @@ export = { }, ], }, - DeletionPolicy: 'Retain', - UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition)); - - expect(stack).to(haveResource('AWS::RDS::DBInstance', { - DeletionPolicy: 'Retain', - UpdateReplacePolicy: 'Retain', + DeletionPolicy: ABSENT, + UpdateReplacePolicy: 'Snapshot', }, ResourcePart.CompleteDefinition)); expect(stack).to(haveResource('AWS::RDS::DBSubnetGroup', { diff --git a/packages/@aws-cdk/aws-redshift/README.md b/packages/@aws-cdk/aws-redshift/README.md index 2bf53e2033f34..05736c4c15c2c 100644 --- a/packages/@aws-cdk/aws-redshift/README.md +++ b/packages/@aws-cdk/aws-redshift/README.md @@ -9,4 +9,52 @@ --- +### Starting a Redshift Cluster Database + +To set up a Redshift cluster, define a `Cluster`. It will be launched in a VPC. +You can specify a VPC, otherwise one will be created. The nodes are always launched in private subnets and are encrypted by default. + +``` typescript +import redshift = require('@aws-cdk/aws-redshift'); +... +const cluster = new redshift.Cluster(this, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + vpc + }); +``` +By default, the master password will be generated and stored in AWS Secrets Manager. + +A default database named `default_db` will be created in the cluster. To change the name of this database set the `defaultDatabaseName` attribute in the constructor properties. + +### Connecting + +To control who can access the cluster, use the `.connections` attribute. Redshift Clusters have +a default port, so you don't need to specify the port: + +```ts +cluster.connections.allowFromAnyIpv4('Open to the world'); +``` + +The endpoint to access your database cluster will be available as the `.clusterEndpoint` attribute: + +```ts +cluster.clusterEndpoint.socketAddress; // "HOSTNAME:PORT" +``` + +### Rotating credentials + +When the master password is generated and stored in AWS Secrets Manager, it can be rotated automatically: +```ts +cluster.addRotationSingleUser(); // Will rotate automatically after 30 days +``` + +The multi user rotation scheme is also available: +```ts +cluster.addRotationMultiUser('MyUser', { + secret: myImportedSecret +}); +``` + This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. diff --git a/packages/@aws-cdk/aws-redshift/lib/cluster.ts b/packages/@aws-cdk/aws-redshift/lib/cluster.ts new file mode 100644 index 0000000000000..48caa7aabf1db --- /dev/null +++ b/packages/@aws-cdk/aws-redshift/lib/cluster.ts @@ -0,0 +1,540 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import * as kms from '@aws-cdk/aws-kms'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; +import { Construct, Duration, IResource, RemovalPolicy, Resource, SecretValue, Token } from '@aws-cdk/core'; +import { DatabaseSecret } from './database-secret'; +import { Endpoint } from './endpoint'; +import { IClusterParameterGroup } from './parameter-group'; +import { CfnCluster, CfnClusterSubnetGroup } from './redshift.generated'; + +/** + * Possible Node Types to use in the cluster + * used for defining {@link ClusterProps.nodeType}. + */ +export enum NodeType { + /** + * ds2.xlarge + */ + DS2_XLARGE = 'ds2.xlarge', + /** + * ds2.8xlarge + */ + DS2_8XLARGE = 'ds2.8xlarge', + /** + * dc1.large + */ + DC1_LARGE = 'dc1.large', + /** + * dc1.8xlarge + */ + DC1_8XLARGE = 'dc1.8xlarge', + /** + * dc2.large + */ + DC2_LARGE = 'dc2.large', + /** + * dc2.8xlarge + */ + DC2_8XLARGE = 'dc2.8xlarge', + /** + * ra3.16xlarge + */ + RA3_16XLARGE = 'ra3.16xlarge', +} + +/** + * What cluster type to use. + * Used by {@link ClusterProps.clusterType} + */ +export enum ClusterType { + /** + * single-node cluster, the {@link ClusterProps.numberOfNodes} parameter is not required + */ + SINGLE_NODE = 'single-node', + /** + * multi-node cluster, set the amount of nodes using {@link ClusterProps.numberOfNodes} parameter + */ + MULTI_NODE = 'multi-node', +} + +/** + * Username and password combination + */ +export interface Login { + /** + * Username + */ + readonly masterUsername: string; + + /** + * Password + * + * Do not put passwords in your CDK code directly. + * + * @default a Secrets Manager generated password + */ + readonly masterPassword?: SecretValue; + + /** + * KMS encryption key to encrypt the generated secret. + * + * @default default master key + */ + readonly encryptionKey?: kms.IKey; +} + +/** + * Options to add the multi user rotation + */ +export interface RotationMultiUserOptions { + /** + * The secret to rotate. It must be a JSON string with the following format: + * ``` + * { + * "engine": , + * "host": , + * "username": , + * "password": , + * "dbname": , + * "port": , + * "masterarn": + * } + * ``` + */ + readonly secret: secretsmanager.ISecret; + + /** + * Specifies the number of days after the previous rotation before + * Secrets Manager triggers the next automatic rotation. + * + * @default Duration.days(30) + */ + readonly automaticallyAfter?: Duration; +} + +/** + * Create a Redshift Cluster with a given number of nodes. + * Implemented by {@link Cluster} via {@link ClusterBase}. + */ +export interface ICluster extends IResource, ec2.IConnectable, secretsmanager.ISecretAttachmentTarget { + /** + * Name of the cluster + * + * @attribute ClusterName + */ + readonly clusterName: string; + + /** + * The endpoint to use for read/write operations + * + * @attribute EndpointAddress,EndpointPort + */ + readonly clusterEndpoint: Endpoint; +} + +/** + * Properties that describe an existing cluster instance + */ +export interface ClusterAttributes { + /** + * The security groups of the redshift cluster + * + * @default no security groups will be attached to the import + */ + readonly securityGroups?: ec2.ISecurityGroup[]; + + /** + * Identifier for the cluster + */ + readonly clusterName: string; + + /** + * Cluster endpoint address + */ + readonly clusterEndpointAddress: string; + + /** + * Cluster endpoint port + */ + readonly clusterEndpointPort: number; + +} + +/** + * Properties for a new database cluster + */ +export interface ClusterProps { + + /** + * An optional identifier for the cluster + * + * @default - A name is automatically generated. + */ + readonly clusterName?: string; + + /** + * Additional parameters to pass to the database engine + * https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-parameter-groups.html + * + * @default - No parameter group. + */ + readonly parameterGroup?: IClusterParameterGroup; + + /** + * Number of compute nodes in the cluster + * + * Value must be at least 1 and no more than 100. + * + * @default 1 + */ + readonly numberOfNodes?: number; + + /** + * The node type to be provisioned for the cluster. + * + * @default {@link NodeType.DC2_LARGE} + */ + readonly nodeType?: NodeType; + + /** + * Settings for the individual instances that are launched + * + * @default {@link ClusterType.MULTI_NODE} + */ + readonly clusterType?: ClusterType; + + /** + * What port to listen on + * + * @default - The default for the engine is used. + */ + readonly port?: number; + + /** + * Whether to enable encryption of data at rest in the cluster. + * + * @default true + */ + readonly encrypted?: boolean + + /** + * The KMS key to use for encryption of data at rest. + * + * @default - AWS-managed key, if encryption at rest is enabled + */ + readonly encryptionKey?: kms.IKey; + + /** + * A preferred maintenance window day/time range. Should be specified as a range ddd:hh24:mi-ddd:hh24:mi (24H Clock UTC). + * + * Example: 'Sun:23:45-Mon:00:15' + * + * @default - 30-minute window selected at random from an 8-hour block of time for + * each AWS Region, occurring on a random day of the week. + * @see https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_UpgradeDBInstance.Maintenance.html#Concepts.DBMaintenance + */ + readonly preferredMaintenanceWindow?: string; + + /** + * The VPC to place the cluster in. + */ + readonly vpc: ec2.IVpc; + + /** + * Where to place the instances within the VPC + * + * @default private subnets + */ + readonly vpcSubnets?: ec2.SubnetSelection; + + /** + * Security group. + * + * @default a new security group is created. + */ + readonly securityGroups?: ec2.ISecurityGroup[]; + + /** + * Username and password for the administrative user + */ + readonly masterUser: Login; + + /** + * A list of AWS Identity and Access Management (IAM) role that can be used by the cluster to access other AWS services. + * Specify a maximum of 10 roles. + * + * @default - No role is attached to the cluster. + */ + readonly roles?: iam.IRole[]; + + /** + * Name of a database which is automatically created inside the cluster + * + * @default - default_db + */ + readonly defaultDatabaseName?: string; + + /** + * Bucket to send logs to. + * Logging information includes queries and connection attempts, for the specified Amazon Redshift cluster. + * + * @default - No Logs + */ + readonly loggingBucket?: s3.IBucket + + /** + * Prefix used for logging + * + * @default - no prefix + */ + readonly loggingKeyPrefix?: string + + /** + * The removal policy to apply when the cluster and its instances are removed + * from the stack or replaced during an update. + * + * @default RemovalPolicy.RETAIN + */ + readonly removalPolicy?: RemovalPolicy +} + +/** + * A new or imported clustered database. + */ +abstract class ClusterBase extends Resource implements ICluster { + /** + * Name of the cluster + */ + public abstract readonly clusterName: string; + + /** + * The endpoint to use for read/write operations + */ + public abstract readonly clusterEndpoint: Endpoint; + + /** + * Access to the network connections + */ + public abstract readonly connections: ec2.Connections; + + /** + * Renders the secret attachment target specifications. + */ + public asSecretAttachmentTarget(): secretsmanager.SecretAttachmentTargetProps { + return { + targetId: this.clusterName, + targetType: secretsmanager.AttachmentTargetType.REDSHIFT_CLUSTER, + }; + } +} + +/** + * Create a Redshift cluster a given number of nodes. + * + * @resource AWS::Redshift::Cluster + */ +export class Cluster extends ClusterBase { + /** + * Import an existing DatabaseCluster from properties + */ + public static fromClusterAttributes(scope: Construct, id: string, attrs: ClusterAttributes): ICluster { + class Import extends ClusterBase { + public readonly connections = new ec2.Connections({ + securityGroups: attrs.securityGroups, + defaultPort: ec2.Port.tcp(attrs.clusterEndpointPort), + }); + public readonly clusterName = attrs.clusterName; + public readonly instanceIdentifiers: string[] = []; + public readonly clusterEndpoint = new Endpoint(attrs.clusterEndpointAddress, attrs.clusterEndpointPort); + } + + return new Import(scope, id); + } + + /** + * Identifier of the cluster + */ + public readonly clusterName: string; + + /** + * The endpoint to use for read/write operations + */ + public readonly clusterEndpoint: 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; + + /** + * The subnets used by the DB subnet group. + */ + private readonly vpcSubnets?: ec2.SubnetSelection; + + constructor(scope: Construct, id: string, props: ClusterProps) { + super(scope, id); + + this.vpc = props.vpc; + this.vpcSubnets = props.vpcSubnets ? props.vpcSubnets : { + subnetType: ec2.SubnetType.PRIVATE, + }; + + const removalPolicy = props.removalPolicy ? props.removalPolicy : RemovalPolicy.RETAIN; + + const { subnetIds } = this.vpc.selectSubnets(this.vpcSubnets); + + const subnetGroup = new CfnClusterSubnetGroup(this, 'Subnets', { + description: `Subnets for ${id} Redshift cluster`, + subnetIds, + }); + + subnetGroup.applyRemovalPolicy(removalPolicy, { + applyToUpdateReplacePolicy: true, + }); + + const securityGroups = props.securityGroups !== undefined ? + props.securityGroups : [new ec2.SecurityGroup(this, 'SecurityGroup', { + description: 'Redshift security group', + vpc: this.vpc, + securityGroupName: 'redshift SG', + })]; + + const securityGroupIds = securityGroups.map(sg => sg.securityGroupId); + + let secret: DatabaseSecret | undefined; + if (!props.masterUser.masterPassword) { + secret = new DatabaseSecret(this, 'Secret', { + username: props.masterUser.masterUsername, + encryptionKey: props.masterUser.encryptionKey, + }); + } + + const clusterType = props.clusterType || ClusterType.MULTI_NODE; + const nodeCount = props.numberOfNodes !== undefined ? props.numberOfNodes : (clusterType === ClusterType.MULTI_NODE ? 2 : 1); + + if (clusterType === ClusterType.MULTI_NODE && nodeCount < 2) { + throw new Error('Number of nodes for cluster type multi-node must be at least 2'); + } + + if (props.encrypted === false && props.encryptionKey !== undefined) { + throw new Error('Cannot set property encryptionKey without enabling encryption!'); + } + + this.singleUserRotationApplication = secretsmanager.SecretRotationApplication.REDSHIFT_ROTATION_SINGLE_USER; + this.multiUserRotationApplication = secretsmanager.SecretRotationApplication.REDSHIFT_ROTATION_MULTI_USER; + + let loggingProperties; + if (props.loggingBucket) { + loggingProperties = { + bucketName: props.loggingBucket.bucketName, + s3KeyPrefix: props.loggingKeyPrefix, + }; + } + + const cluster = new CfnCluster(this, 'Resource', { + // Basic + allowVersionUpgrade: true, + automatedSnapshotRetentionPeriod: 1, + clusterType, + clusterIdentifier: props.clusterName, + clusterSubnetGroupName: subnetGroup.ref, + vpcSecurityGroupIds: securityGroupIds, + port: props.port, + clusterParameterGroupName: props.parameterGroup && props.parameterGroup.clusterParameterGroupName, + // Admin + masterUsername: secret ? secret.secretValueFromJson('username').toString() : props.masterUser.masterUsername, + masterUserPassword: secret + ? secret.secretValueFromJson('password').toString() + : (props.masterUser.masterPassword + ? props.masterUser.masterPassword.toString() + : 'default'), + preferredMaintenanceWindow: props.preferredMaintenanceWindow, + nodeType: props.nodeType || NodeType.DC2_LARGE, + numberOfNodes: nodeCount, + loggingProperties, + iamRoles: props.roles ? props.roles.map(role => role.roleArn) : undefined, + dbName: props.defaultDatabaseName || 'default_db', + publiclyAccessible: false, + // Encryption + kmsKeyId: props.encryptionKey && props.encryptionKey.keyArn, + encrypted: props.encrypted !== undefined ? props.encrypted : true, + }); + + cluster.applyRemovalPolicy(removalPolicy, { + applyToUpdateReplacePolicy: true, + }); + + this.clusterName = cluster.ref; + + // create a number token that represents the port of the cluster + const portAttribute = Token.asNumber(cluster.attrEndpointPort); + this.clusterEndpoint = new Endpoint(cluster.attrEndpointAddress, portAttribute); + + if (secret) { + this.secret = secret.attach(this); + } + + const defaultPort = ec2.Port.tcp(this.clusterEndpoint.port); + this.connections = new ec2.Connections({ securityGroups, defaultPort }); + } + + /** + * Adds the single user rotation of the master password to this cluster. + * + * @param [automaticallyAfter=Duration.days(30)] Specifies the number of days after the previous rotation + * before Secrets Manager triggers the next automatic rotation. + */ + public addRotationSingleUser(automaticallyAfter?: Duration): secretsmanager.SecretRotation { + if (!this.secret) { + throw new Error('Cannot add single user rotation for a cluster without secret.'); + } + + const id = 'RotationSingleUser'; + const existing = this.node.tryFindChild(id); + if (existing) { + throw new Error('A single user rotation was already added to this cluster.'); + } + + return new secretsmanager.SecretRotation(this, id, { + secret: this.secret, + automaticallyAfter, + application: this.singleUserRotationApplication, + vpc: this.vpc, + vpcSubnets: this.vpcSubnets, + target: this, + }); + } + + /** + * Adds the multi user rotation to this cluster. + */ + public addRotationMultiUser(id: string, options: RotationMultiUserOptions): secretsmanager.SecretRotation { + if (!this.secret) { + throw new Error('Cannot add multi user rotation for a cluster without secret.'); + } + return new secretsmanager.SecretRotation(this, id, { + secret: options.secret, + masterSecret: this.secret, + automaticallyAfter: options.automaticallyAfter, + application: this.multiUserRotationApplication, + vpc: this.vpc, + vpcSubnets: this.vpcSubnets, + target: this, + }); + } +} diff --git a/packages/@aws-cdk/aws-redshift/lib/database-secret.ts b/packages/@aws-cdk/aws-redshift/lib/database-secret.ts new file mode 100644 index 0000000000000..7e7617be2be83 --- /dev/null +++ b/packages/@aws-cdk/aws-redshift/lib/database-secret.ts @@ -0,0 +1,39 @@ +import * as kms from '@aws-cdk/aws-kms'; +import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; +import { Construct } from '@aws-cdk/core'; + +/** + * Construction properties for a DatabaseSecret. + */ +export interface DatabaseSecretProps { + /** + * The username. + */ + readonly username: string; + + /** + * The KMS key to use to encrypt the secret. + * + * @default default master key + */ + readonly encryptionKey?: kms.IKey; +} + +/** + * A database secret. + * + * @resource AWS::SecretsManager::Secret + */ +export class DatabaseSecret extends secretsmanager.Secret { + constructor(scope: Construct, id: string, props: DatabaseSecretProps) { + super(scope, id, { + encryptionKey: props.encryptionKey, + generateSecretString: { + passwordLength: 30, // Redshift password could be up to 64 characters + secretStringTemplate: JSON.stringify({ username: props.username }), + generateStringKey: 'password', + excludeCharacters: '"@/\\\ \'', + }, + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-redshift/lib/endpoint.ts b/packages/@aws-cdk/aws-redshift/lib/endpoint.ts new file mode 100644 index 0000000000000..0ee19b8d82113 --- /dev/null +++ b/packages/@aws-cdk/aws-redshift/lib/endpoint.ts @@ -0,0 +1,31 @@ +import { Token } from '@aws-cdk/core'; + +/** + * Connection endpoint of a redshift cluster + * + * Consists of a combination of hostname and port. + */ +export class Endpoint { + /** + * The hostname of the endpoint + */ + public readonly hostname: string; + + /** + * The port of the endpoint + */ + public readonly port: number; + + /** + * The combination of "HOSTNAME:PORT" for this endpoint + */ + public readonly socketAddress: string; + + constructor(address: string, port: number) { + this.hostname = address; + this.port = port; + + const portDesc = Token.isUnresolved(port) ? Token.asString(port) : port; + this.socketAddress = `${address}:${portDesc}`; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-redshift/lib/index.ts b/packages/@aws-cdk/aws-redshift/lib/index.ts index e1441fcf6bb03..6d5e5d00bb134 100644 --- a/packages/@aws-cdk/aws-redshift/lib/index.ts +++ b/packages/@aws-cdk/aws-redshift/lib/index.ts @@ -1,2 +1,7 @@ +export * from './cluster'; +export * from './parameter-group'; +export * from './database-secret'; +export * from './endpoint'; + // AWS::Redshift CloudFormation Resources: export * from './redshift.generated'; diff --git a/packages/@aws-cdk/aws-redshift/lib/parameter-group.ts b/packages/@aws-cdk/aws-redshift/lib/parameter-group.ts new file mode 100644 index 0000000000000..ea5698b235628 --- /dev/null +++ b/packages/@aws-cdk/aws-redshift/lib/parameter-group.ts @@ -0,0 +1,77 @@ +import { Construct, IResource, Resource } from '@aws-cdk/core'; +import { CfnClusterParameterGroup } from './redshift.generated'; + +/** + * A parameter group + */ +export interface IClusterParameterGroup extends IResource { + /** + * The name of this parameter group + * + * @attribute + */ + readonly clusterParameterGroupName: string; +} + +/** + * A new cluster or instance parameter group + */ +abstract class ClusterParameterGroupBase extends Resource implements IClusterParameterGroup { + /** + * The name of the parameter group + */ + public abstract readonly clusterParameterGroupName: string; +} + +/** + * Properties for a parameter group + */ +export interface ClusterParameterGroupProps { + /** + * Description for this parameter group + * + * @default a CDK generated description + */ + readonly description?: string; + + /** + * The parameters in this parameter group + */ + readonly parameters: { [name: string]: string }; +} + +/** + * A cluster parameter group + * + * @resource AWS::Redshift::ClusterParameterGroup + */ +export class ClusterParameterGroup extends ClusterParameterGroupBase { + /** + * Imports a parameter group + */ + public static fromClusterParameterGroupName(scope: Construct, id: string, clusterParameterGroupName: string): IClusterParameterGroup { + class Import extends Resource implements IClusterParameterGroup { + public readonly clusterParameterGroupName = clusterParameterGroupName; + } + return new Import(scope, id); + } + + /** + * The name of the parameter group + */ + public readonly clusterParameterGroupName: string; + + constructor(scope: Construct, id: string, props: ClusterParameterGroupProps) { + super(scope, id); + + const resource = new CfnClusterParameterGroup(this, 'Resource', { + description: props.description || 'Cluster parameter group for family redshift-1.0', + parameterGroupFamily: 'redshift-1.0', + parameters: Object.entries(props.parameters).map(([name, value]) => { + return {parameterName: name, parameterValue: value}; + }), + }); + + this.clusterParameterGroupName = resource.ref; + } +} diff --git a/packages/@aws-cdk/aws-redshift/package.json b/packages/@aws-cdk/aws-redshift/package.json index 07283d9304b9f..3b645e15ba91e 100644 --- a/packages/@aws-cdk/aws-redshift/package.json +++ b/packages/@aws-cdk/aws-redshift/package.json @@ -66,20 +66,39 @@ "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", + "jest": "^25.5.3", "pkglint": "0.0.0" }, "dependencies": { + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-secretsmanager": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-secretsmanager": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, + "awslint": { + "exclude": [ + "docs-public-apis:@aws-cdk/aws-redshift.ParameterGroupParameters.parameterName", + "docs-public-apis:@aws-cdk/aws-redshift.ParameterGroupParameters.parameterValue", + "props-physical-name:@aws-cdk/aws-redshift.ClusterParameterGroupProps", + "props-physical-name:@aws-cdk/aws-redshift.DatabaseSecretProps" + ] + }, "stability": "experimental", "maturity": "cfn-only", "awscdkio": { diff --git a/packages/@aws-cdk/aws-redshift/test/cluster.test.ts b/packages/@aws-cdk/aws-redshift/test/cluster.test.ts new file mode 100644 index 0000000000000..385a2f53208b5 --- /dev/null +++ b/packages/@aws-cdk/aws-redshift/test/cluster.test.ts @@ -0,0 +1,329 @@ +import { expect as cdkExpect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as kms from '@aws-cdk/aws-kms'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; + +import { Cluster, ClusterParameterGroup, ClusterType, NodeType } from '../lib'; + +test('check that instantiation works', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + masterPassword: cdk.SecretValue.plainText('tooshort'), + }, + vpc, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::Cluster', { + Properties: { + AllowVersionUpgrade: true, + MasterUsername: 'admin', + MasterUserPassword: 'tooshort', + ClusterType: 'multi-node', + AutomatedSnapshotRetentionPeriod: 1, + Encrypted: true, + NumberOfNodes: 2, + NodeType: 'dc2.large', + DBName: 'default_db', + PubliclyAccessible: false, + ClusterSubnetGroupName: { Ref: 'RedshiftSubnetsDFE70E0A' }, + VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['RedshiftSecurityGroup796D74A7', 'GroupId'] }], + }, + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }, ResourcePart.CompleteDefinition)); + + cdkExpect(stack).to(haveResource('AWS::Redshift::ClusterSubnetGroup', { + Properties: { + Description: 'Subnets for Redshift Redshift cluster', + SubnetIds: [ + { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, + { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, + { Ref: 'VPCPrivateSubnet3Subnet3EDCD457' }, + ], + }, + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }, ResourcePart.CompleteDefinition)); +}); + +test('can create a cluster with imported vpc and security group', () => { + // GIVEN + const stack = testStack(); + const vpc = ec2.Vpc.fromLookup(stack, 'VPC', { + vpcId: 'VPC12345', + }); + const sg = ec2.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'SecurityGroupId12345'); + + // WHEN + new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + masterPassword: cdk.SecretValue.plainText('tooshort'), + }, + vpc, + securityGroups: [sg], + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::Cluster', { + ClusterSubnetGroupName: { Ref: 'RedshiftSubnetsDFE70E0A' }, + MasterUsername: 'admin', + MasterUserPassword: 'tooshort', + VpcSecurityGroupIds: ['SecurityGroupId12345'], + })); +}); + +test('creates a secret when master credentials are not specified', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + vpc, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::Cluster', { + MasterUsername: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'RedshiftSecretA08D42D6', + }, + ':SecretString:username::}}', + ], + ], + }, + MasterUserPassword: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'RedshiftSecretA08D42D6', + }, + ':SecretString:password::}}', + ], + ], + }, + })); + + cdkExpect(stack).to(haveResource('AWS::SecretsManager::Secret', { + GenerateSecretString: { + ExcludeCharacters: '"@/\\\ \'', + GenerateStringKey: 'password', + PasswordLength: 30, + SecretStringTemplate: '{"username":"admin"}', + }, + })); +}); + +test('SIngle Node CLusters spawn only single node', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + vpc, + nodeType: NodeType.DC1_8XLARGE, + clusterType: ClusterType.SINGLE_NODE, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::Cluster', { + ClusterType: 'single-node', + NodeType: 'dc1.8xlarge', + NumberOfNodes: 1, + })); +}); + +test('create an encrypted cluster with custom KMS key', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + encryptionKey: new kms.Key(stack, 'Key'), + vpc, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::Cluster', { + KmsKeyId: { + 'Fn::GetAtt': [ + 'Key961B73FD', + 'Arn', + ], + }, + })); +}); + +test('cluster with parameter group', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const group = new ClusterParameterGroup(stack, 'Params', { + description: 'bye', + parameters: { + param: 'value', + }, + }); + + new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + vpc, + parameterGroup: group, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::Cluster', { + ClusterParameterGroupName: { Ref: 'ParamsA8366201' }, + })); + +}); + +test('imported cluster with imported security group honors allowAllOutbound', () => { + // GIVEN + const stack = testStack(); + + const cluster = Cluster.fromClusterAttributes(stack, 'Database', { + clusterEndpointAddress: 'addr', + clusterName: 'identifier', + clusterEndpointPort: 3306, + securityGroups: [ + ec2.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'sg-123456789', { + allowAllOutbound: false, + }), + ], + }); + + // WHEN + cluster.connections.allowToAnyIpv4(ec2.Port.tcp(443)); + + // THEN + cdkExpect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + GroupId: 'sg-123456789', + })); +}); + +test('can create a cluster with logging enabled', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const bucket = s3.Bucket.fromBucketName(stack, 'bucket', 'logging-bucket'); + + // WHEN + new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + vpc, + loggingBucket: bucket, + loggingKeyPrefix: 'prefix', + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::Cluster', { + LoggingProperties: { + BucketName: 'logging-bucket', + S3KeyPrefix: 'prefix', + }, + })); +}); + +test('throws when trying to add rotation to a cluster without secret', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const cluster = new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + masterPassword: cdk.SecretValue.plainText('tooshort'), + }, + vpc, + }); + + // THEN + expect(() => { + cluster.addRotationSingleUser(); + }).toThrowError(); + +}); + +test('throws validation error when trying to set encryptionKey without enabling encryption', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const key = new kms.Key(stack, 'kms-key'); + + // WHEN + const props = { + encrypted: false, + encryptionKey: key, + masterUser: { + masterUsername: 'admin', + }, + vpc, + }; + + // THEN + expect(() => { + new Cluster(stack, 'Redshift', props ); + }).toThrowError(); + +}); + +test('throws when trying to add single user rotation multiple times', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + vpc, + }); + + // WHEN + cluster.addRotationSingleUser(); + + // THEN + expect(() => { + cluster.addRotationSingleUser(); + }).toThrowError(); +}); + +function testStack() { + const stack = new cdk.Stack(undefined, undefined, { env: { account: '12345', region: 'us-test-1' } }); + stack.node.setContext('availability-zones:12345:us-test-1', ['us-test-1a', 'us-test-1b']); + return stack; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-redshift/test/parameter-group.test.ts b/packages/@aws-cdk/aws-redshift/test/parameter-group.test.ts new file mode 100644 index 0000000000000..ca5923ee36ba6 --- /dev/null +++ b/packages/@aws-cdk/aws-redshift/test/parameter-group.test.ts @@ -0,0 +1,29 @@ +import { expect as cdkExpect, haveResource } from '@aws-cdk/assert'; +import * as cdk from '@aws-cdk/core'; +import { ClusterParameterGroup } from '../lib'; + +test('create a cluster parameter group', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new ClusterParameterGroup(stack, 'Params', { + description: 'desc', + parameters: { + param: 'value', + }, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::ClusterParameterGroup', { + Description: 'desc', + ParameterGroupFamily: 'redshift-1.0', + Parameters: [ + { + ParameterName: 'param', + ParameterValue: 'value', + }, + ], + })); + +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-assets/package.json b/packages/@aws-cdk/aws-s3-assets/package.json index ff1eb0933ce36..3aea5a8f58626 100644 --- a/packages/@aws-cdk/aws-s3-assets/package.json +++ b/packages/@aws-cdk/aws-s3-assets/package.json @@ -61,7 +61,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "@types/sinon": "^9.0.3", + "@types/sinon": "^9.0.4", "aws-cdk": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-s3/README.md b/packages/@aws-cdk/aws-s3/README.md index 6e8f002e287e0..d136089a22528 100644 --- a/packages/@aws-cdk/aws-s3/README.md +++ b/packages/@aws-cdk/aws-s3/README.md @@ -84,6 +84,13 @@ bucket.addToResourcePolicy(new iam.PolicyStatement({ })); ``` +The bucket policy can be directly accessed after creation to add statements or +adjust the removal policy. + +```ts +bucket.policy?.applyRemovalPolicy(RemovalPolicy.RETAIN); +``` + Most of the time, you won't have to manipulate the bucket policy directly. Instead, buckets have "grant" methods called to give prepackaged sets of permissions to other resources. For example: diff --git a/packages/@aws-cdk/aws-s3/lib/bucket-policy.ts b/packages/@aws-cdk/aws-s3/lib/bucket-policy.ts index a59c891e7ccbd..10f35b5c40e3d 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket-policy.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket-policy.ts @@ -1,5 +1,5 @@ import { PolicyDocument } from '@aws-cdk/aws-iam'; -import { Construct, Resource } from '@aws-cdk/core'; +import { Construct, RemovalPolicy, Resource } from '@aws-cdk/core'; import { IBucket } from './bucket'; import { CfnBucketPolicy } from './s3.generated'; @@ -8,6 +8,13 @@ export interface BucketPolicyProps { * The Amazon S3 bucket that the policy applies to. */ readonly bucket: IBucket; + + /** + * Policy to apply when the policy is removed from this stack. + * + * @default - RemovalPolicy.DESTROY. + */ + readonly removalPolicy?: RemovalPolicy; } /** @@ -22,6 +29,8 @@ export class BucketPolicy extends Resource { */ public readonly document = new PolicyDocument(); + private resource: CfnBucketPolicy; + constructor(scope: Construct, id: string, props: BucketPolicyProps) { super(scope, id); @@ -29,9 +38,22 @@ export class BucketPolicy extends Resource { throw new Error('Bucket doesn\'t have a bucketName defined'); } - new CfnBucketPolicy(this, 'Resource', { + this.resource = new CfnBucketPolicy(this, 'Resource', { bucket: props.bucket.bucketName, policyDocument: this.document, }); + + if (props.removalPolicy) { + this.resource.applyRemovalPolicy(props.removalPolicy); + } } + + /** + * Sets the removal policy for the BucketPolicy. + * @param removalPolicy the RemovalPolicy to set. + */ + public applyRemovalPolicy(removalPolicy: RemovalPolicy) { + this.resource.applyRemovalPolicy(removalPolicy); + } + } diff --git a/packages/@aws-cdk/aws-s3/test/test.bucket-policy.ts b/packages/@aws-cdk/aws-s3/test/test.bucket-policy.ts new file mode 100644 index 0000000000000..17121d3d6c31a --- /dev/null +++ b/packages/@aws-cdk/aws-s3/test/test.bucket-policy.ts @@ -0,0 +1,130 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import { PolicyStatement } from '@aws-cdk/aws-iam'; +import { RemovalPolicy, Stack } from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import * as s3 from '../lib'; + +// to make it easy to copy & paste from output: +// tslint:disable:object-literal-key-quotes + +export = { + 'default properties'(test: Test) { + const stack = new Stack(); + + const myBucket = new s3.Bucket(stack, 'MyBucket'); + const myBucketPolicy = new s3.BucketPolicy(stack, 'MyBucketPolicy', { + bucket: myBucket, + }); + myBucketPolicy.document.addStatements(new PolicyStatement({ + resources: [myBucket.bucketArn], + actions: ['s3:GetObject*'], + })); + + expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + Bucket: { + 'Ref': 'MyBucketF68F3FF0', + }, + PolicyDocument: { + 'Version': '2012-10-17', + 'Statement': [ + { + 'Action': 's3:GetObject*', + 'Effect': 'Allow', + 'Resource': { 'Fn::GetAtt': ['MyBucketF68F3FF0', 'Arn'] }, + }, + ], + }, + })); + + test.done(); + }, + + 'when specifying a removalPolicy at creation'(test: Test) { + const stack = new Stack(); + + const myBucket = new s3.Bucket(stack, 'MyBucket'); + const myBucketPolicy = new s3.BucketPolicy(stack, 'MyBucketPolicy', { + bucket: myBucket, + removalPolicy: RemovalPolicy.RETAIN, + }); + myBucketPolicy.document.addStatements(new PolicyStatement({ + resources: [myBucket.bucketArn], + actions: ['s3:GetObject*'], + })); + + expect(stack).toMatch({ + 'Resources': { + 'MyBucketF68F3FF0': { + 'Type': 'AWS::S3::Bucket', + 'DeletionPolicy': 'Retain', + 'UpdateReplacePolicy': 'Retain', + }, + 'MyBucketPolicy0AFEFDBE': { + 'Type': 'AWS::S3::BucketPolicy', + 'Properties': { + 'Bucket': { + 'Ref': 'MyBucketF68F3FF0', + }, + 'PolicyDocument': { + 'Statement': [ + { + 'Action': 's3:GetObject*', + 'Effect': 'Allow', + 'Resource': { 'Fn::GetAtt': ['MyBucketF68F3FF0', 'Arn'] }, + }, + ], + 'Version': '2012-10-17', + }, + }, + 'DeletionPolicy': 'Retain', + 'UpdateReplacePolicy': 'Retain', + }, + }, + }); + + test.done(); + }, + + 'when specifying a removalPolicy after creation'(test: Test) { + const stack = new Stack(); + + const myBucket = new s3.Bucket(stack, 'MyBucket'); + myBucket.addToResourcePolicy(new PolicyStatement({ + resources: [myBucket.bucketArn], + actions: ['s3:GetObject*'], + })); + myBucket.policy?.applyRemovalPolicy(RemovalPolicy.RETAIN); + + expect(stack).toMatch({ + 'Resources': { + 'MyBucketF68F3FF0': { + 'Type': 'AWS::S3::Bucket', + 'DeletionPolicy': 'Retain', + 'UpdateReplacePolicy': 'Retain', + }, + 'MyBucketPolicyE7FBAC7B': { + 'Type': 'AWS::S3::BucketPolicy', + 'Properties': { + 'Bucket': { + 'Ref': 'MyBucketF68F3FF0', + }, + 'PolicyDocument': { + 'Statement': [ + { + 'Action': 's3:GetObject*', + 'Effect': 'Allow', + 'Resource': { 'Fn::GetAtt': ['MyBucketF68F3FF0', 'Arn'] }, + }, + ], + 'Version': '2012-10-17', + }, + }, + 'DeletionPolicy': 'Retain', + 'UpdateReplacePolicy': 'Retain', + }, + }, + }); + + test.done(); + }, +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sqs/lib/validate-props.ts b/packages/@aws-cdk/aws-sqs/lib/validate-props.ts index 3d7781fabd957..8a1204e21f858 100644 --- a/packages/@aws-cdk/aws-sqs/lib/validate-props.ts +++ b/packages/@aws-cdk/aws-sqs/lib/validate-props.ts @@ -1,3 +1,4 @@ +import { Token } from '@aws-cdk/core'; import { QueueProps } from './index'; export function validateProps(props: QueueProps) { @@ -10,7 +11,7 @@ export function validateProps(props: QueueProps) { } function validateRange(label: string, value: number | undefined, minValue: number, maxValue: number, unit?: string) { - if (value === undefined) { return; } + if (value === undefined || Token.isUnresolved(value)) { return; } const unitSuffix = unit ? ` ${unit}` : ''; if (value < minValue) { throw new Error(`${label} must be ${minValue}${unitSuffix} or more, but ${value} was provided`); } if (value > maxValue) { throw new Error(`${label} must be ${maxValue}${unitSuffix} of less, but ${value} was provided`); } diff --git a/packages/@aws-cdk/aws-sqs/test/test.sqs.ts b/packages/@aws-cdk/aws-sqs/test/test.sqs.ts index baef7fa8bb2e4..15a67e269bf3a 100644 --- a/packages/@aws-cdk/aws-sqs/test/test.sqs.ts +++ b/packages/@aws-cdk/aws-sqs/test/test.sqs.ts @@ -1,7 +1,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; -import { Duration, Stack } from '@aws-cdk/core'; +import { CfnParameter, Duration, Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import * as sqs from '../lib'; @@ -54,6 +54,58 @@ export = { test.done(); }, + 'message retention period must be between 1 minute to 14 days'(test: Test) { + // GIVEN + const stack = new Stack(); + + // THEN + test.throws(() => new sqs.Queue(stack, 'MyQueue', { + retentionPeriod: Duration.seconds(30), + }), /message retention period must be 60 seconds or more/); + + test.throws(() => new sqs.Queue(stack, 'AnotherQueue', { + retentionPeriod: Duration.days(15), + }), /message retention period must be 1209600 seconds of less/); + + test.done(); + }, + + 'message retention period can be provided as a parameter'(test: Test) { + // GIVEN + const stack = new Stack(); + const parameter = new CfnParameter(stack, 'my-retention-period', { + type: 'Number', + default: 30, + }); + + // WHEN + new sqs.Queue(stack, 'MyQueue', { + retentionPeriod: Duration.seconds(parameter.valueAsNumber), + }); + + // THEN + expect(stack).toMatch({ + 'Parameters': { + 'myretentionperiod': { + 'Type': 'Number', + 'Default': 30, + }, + }, + 'Resources': { + 'MyQueueE6CA6235': { + 'Type': 'AWS::SQS::Queue', + 'Properties': { + 'MessageRetentionPeriod': { + 'Ref': 'myretentionperiod', + }, + }, + }, + }, + }); + + test.done(); + }, + 'addToPolicy will automatically create a policy for this queue'(test: Test) { const stack = new Stack(); const queue = new sqs.Queue(stack, 'MyQueue'); diff --git a/packages/@aws-cdk/cfnspec/CHANGELOG.md b/packages/@aws-cdk/cfnspec/CHANGELOG.md index cfdb595fd50eb..e161d97d380c6 100644 --- a/packages/@aws-cdk/cfnspec/CHANGELOG.md +++ b/packages/@aws-cdk/cfnspec/CHANGELOG.md @@ -1,3 +1,108 @@ +# CloudFormation Resource Specification v14.4.0 + +## New Resource Types + +* AWS::GlobalAccelerator::Accelerator +* AWS::GlobalAccelerator::EndpointGroup +* AWS::GlobalAccelerator::Listener +* AWS::ImageBuilder::Component +* AWS::ImageBuilder::DistributionConfiguration +* AWS::ImageBuilder::Image +* AWS::ImageBuilder::ImagePipeline +* AWS::ImageBuilder::ImageRecipe +* AWS::ImageBuilder::InfrastructureConfiguration +* AWS::Macie::CustomDataIdentifier +* AWS::Macie::FindingsFilter +* AWS::Macie::Session + +## Attribute Changes + +* AWS::Athena::NamedQuery NamedQueryId (__added__) +* AWS::SSM::Association AssociationId (__added__) + +## Property Changes + +* AWS::Cloud9::EnvironmentEC2 ConnectionType (__added__) +* AWS::CodeStarConnections::Connection Tags (__added__) +* AWS::DMS::Endpoint NeptuneSettings (__added__) +* AWS::DMS::ReplicationTask TaskData (__added__) +* AWS::ECS::Cluster ClusterSettings.DuplicatesAllowed (__deleted__) +* AWS::ECS::Cluster ClusterSettings.ItemType (__changed__) + * Old: ClusterSetting + * New: ClusterSettings +* AWS::ECS::Cluster Tags.DuplicatesAllowed (__deleted__) +* AWS::Neptune::DBCluster RestoreToTime (__added__) +* AWS::Neptune::DBCluster RestoreType (__added__) +* AWS::Neptune::DBCluster SourceDBClusterIdentifier (__added__) +* AWS::Neptune::DBCluster UseLatestRestorableTime (__added__) +* AWS::SSM::Association AutomationTargetParameterName (__added__) +* AWS::SSM::Association ComplianceSeverity (__added__) +* AWS::SSM::Association MaxConcurrency (__added__) +* AWS::SSM::Association MaxErrors (__added__) +* AWS::SSM::Association SyncCompliance (__added__) +* AWS::SSM::Association WaitForSuccessTimeoutSeconds (__added__) +* AWS::SSM::Association InstanceId.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::SSM::Association Name.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::SSM::Association Parameters.DuplicatesAllowed (__deleted__) +* AWS::SSM::Association Targets.DuplicatesAllowed (__deleted__) +* AWS::SSM::Association Targets.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::SSM::Parameter DataType (__added__) +* AWS::ServiceCatalog::CloudFormationProduct ReplaceProvisioningArtifacts (__added__) +* AWS::StepFunctions::StateMachine DefinitionS3Location (__added__) +* AWS::StepFunctions::StateMachine DefinitionSubstitutions (__added__) +* AWS::StepFunctions::StateMachine DefinitionString.Required (__changed__) + * Old: true + * New: false +* AWS::Synthetics::Canary RunConfig.Required (__changed__) + * Old: false + * New: true + +## Property Type Changes + +* AWS::EC2::LaunchTemplate.CapacityReservationPreference (__removed__) +* AWS::ECS::Cluster.ClusterSetting (__removed__) +* AWS::SSM::Association.ParameterValues (__removed__) +* AWS::DMS::Endpoint.NeptuneSettings (__added__) +* AWS::ECS::Cluster.ClusterSettings (__added__) +* AWS::StepFunctions::StateMachine.DefinitionSubstitutions (__added__) +* AWS::StepFunctions::StateMachine.S3Location (__added__) +* AWS::DLM::LifecyclePolicy.CreateRule CronExpression (__added__) +* AWS::DLM::LifecyclePolicy.CreateRule Interval.Required (__changed__) + * Old: true + * New: false +* AWS::DLM::LifecyclePolicy.CreateRule IntervalUnit.Required (__changed__) + * Old: true + * New: false +* AWS::DLM::LifecyclePolicy.CrossRegionCopyRetainRule Interval.Required (__changed__) + * Old: false + * New: true +* AWS::DLM::LifecyclePolicy.CrossRegionCopyRetainRule IntervalUnit.Required (__changed__) + * Old: false + * New: true +* AWS::DLM::LifecyclePolicy.CrossRegionCopyRule Encrypted.Required (__changed__) + * Old: false + * New: true +* AWS::DLM::LifecyclePolicy.CrossRegionCopyRule TargetRegion.Required (__changed__) + * Old: false + * New: true +* AWS::EC2::LaunchTemplate.CapacityReservationSpecification CapacityReservationPreference.Type (__deleted__) +* AWS::EC2::LaunchTemplate.CapacityReservationSpecification CapacityReservationPreference.PrimitiveType (__added__) +* AWS::SSM::Association.S3OutputLocation OutputS3Region (__added__) +* AWS::SSM::Association.Target Key.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::SSM::Association.Target Values.DuplicatesAllowed (__deleted__) +* AWS::SSM::Association.Target Values.UpdateType (__changed__) + * Old: Immutable + * New: Mutable + + # CloudFormation Resource Specification v14.1.0 ## New Resource Types diff --git a/packages/@aws-cdk/cfnspec/cfn.version b/packages/@aws-cdk/cfnspec/cfn.version index 7b3b6e02bb3e9..72f51351fcd88 100644 --- a/packages/@aws-cdk/cfnspec/cfn.version +++ b/packages/@aws-cdk/cfnspec/cfn.version @@ -1 +1 @@ -14.1.0 +14.4.0 diff --git a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json index d7e1771ee6a67..364df4c34f6dd 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json +++ b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json @@ -8696,16 +8696,22 @@ "AWS::DLM::LifecyclePolicy.CreateRule": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dlm-lifecyclepolicy-createrule.html", "Properties": { + "CronExpression": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dlm-lifecyclepolicy-createrule.html#cfn-dlm-lifecyclepolicy-createrule-cronexpression", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "Interval": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dlm-lifecyclepolicy-createrule.html#cfn-dlm-lifecyclepolicy-createrule-interval", "PrimitiveType": "Integer", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "IntervalUnit": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dlm-lifecyclepolicy-createrule.html#cfn-dlm-lifecyclepolicy-createrule-intervalunit", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "Times": { @@ -8723,13 +8729,13 @@ "Interval": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dlm-lifecyclepolicy-crossregioncopyretainrule.html#cfn-dlm-lifecyclepolicy-crossregioncopyretainrule-interval", "PrimitiveType": "Integer", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "IntervalUnit": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dlm-lifecyclepolicy-crossregioncopyretainrule.html#cfn-dlm-lifecyclepolicy-crossregioncopyretainrule-intervalunit", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" } } @@ -8752,7 +8758,7 @@ "Encrypted": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dlm-lifecyclepolicy-crossregioncopyrule.html#cfn-dlm-lifecyclepolicy-crossregioncopyrule-encrypted", "PrimitiveType": "Boolean", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "RetainRule": { @@ -8764,7 +8770,7 @@ "TargetRegion": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dlm-lifecyclepolicy-crossregioncopyrule.html#cfn-dlm-lifecyclepolicy-crossregioncopyrule-targetregion", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" } } @@ -9078,6 +9084,53 @@ } } }, + "AWS::DMS::Endpoint.NeptuneSettings": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-neptunesettings.html", + "Properties": { + "ErrorRetryDuration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-neptunesettings.html#cfn-dms-endpoint-neptunesettings-errorretryduration", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "IamAuthEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-neptunesettings.html#cfn-dms-endpoint-neptunesettings-iamauthenabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "MaxFileSize": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-neptunesettings.html#cfn-dms-endpoint-neptunesettings-maxfilesize", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "MaxRetryCount": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-neptunesettings.html#cfn-dms-endpoint-neptunesettings-maxretrycount", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "S3BucketFolder": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-neptunesettings.html#cfn-dms-endpoint-neptunesettings-s3bucketfolder", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "S3BucketName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-neptunesettings.html#cfn-dms-endpoint-neptunesettings-s3bucketname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ServiceAccessRoleArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-neptunesettings.html#cfn-dms-endpoint-neptunesettings-serviceaccessrolearn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::DMS::Endpoint.S3Settings": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html", "Properties": { @@ -10236,16 +10289,13 @@ } } }, - "AWS::EC2::LaunchTemplate.CapacityReservationPreference": { - "PrimitiveType": "String" - }, "AWS::EC2::LaunchTemplate.CapacityReservationSpecification": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-capacityreservationspecification.html", "Properties": { "CapacityReservationPreference": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-capacityreservationspecification.html#cfn-ec2-launchtemplate-launchtemplatedata-capacityreservationspecification-capacityreservationpreference", + "PrimitiveType": "String", "Required": false, - "Type": "CapacityReservationPreference", "UpdateType": "Mutable" }, "CapacityReservationTarget": { @@ -11693,19 +11743,19 @@ } } }, - "AWS::ECS::Cluster.ClusterSetting": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-clustersetting.html", + "AWS::ECS::Cluster.ClusterSettings": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-clustersettings.html", "Properties": { "Name": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-clustersetting.html#cfn-ecs-cluster-clustersetting-name", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-clustersettings.html#cfn-ecs-cluster-clustersettings-name", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "Value": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-clustersetting.html#cfn-ecs-cluster-clustersetting-value", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-clustersettings.html#cfn-ecs-cluster-clustersettings-value", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" } } @@ -16403,6 +16453,46 @@ } } }, + "AWS::GlobalAccelerator::EndpointGroup.EndpointConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-globalaccelerator-endpointgroup-endpointconfiguration.html", + "Properties": { + "ClientIPPreservationEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-globalaccelerator-endpointgroup-endpointconfiguration.html#cfn-globalaccelerator-endpointgroup-endpointconfiguration-clientippreservationenabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "EndpointId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-globalaccelerator-endpointgroup-endpointconfiguration.html#cfn-globalaccelerator-endpointgroup-endpointconfiguration-endpointid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Weight": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-globalaccelerator-endpointgroup-endpointconfiguration.html#cfn-globalaccelerator-endpointgroup-endpointconfiguration-weight", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::GlobalAccelerator::Listener.PortRange": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-globalaccelerator-listener-portrange.html", + "Properties": { + "FromPort": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-globalaccelerator-listener-portrange.html#cfn-globalaccelerator-listener-portrange-fromport", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Mutable" + }, + "ToPort": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-globalaccelerator-listener-portrange.html#cfn-globalaccelerator-listener-portrange-toport", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::Glue::Classifier.CsvClassifier": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-classifier-csvclassifier.html", "Properties": { @@ -18819,6 +18909,196 @@ } } }, + "AWS::ImageBuilder::DistributionConfiguration.Distribution": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-distributionconfiguration-distribution.html", + "Properties": { + "AmiDistributionConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-distributionconfiguration-distribution.html#cfn-imagebuilder-distributionconfiguration-distribution-amidistributionconfiguration", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Mutable" + }, + "LicenseConfigurationArns": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-distributionconfiguration-distribution.html#cfn-imagebuilder-distributionconfiguration-distribution-licenseconfigurationarns", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Region": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-distributionconfiguration-distribution.html#cfn-imagebuilder-distributionconfiguration-distribution-region", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::ImageBuilder::Image.ImageTestsConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-image-imagetestsconfiguration.html", + "Properties": { + "ImageTestsEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-image-imagetestsconfiguration.html#cfn-imagebuilder-image-imagetestsconfiguration-imagetestsenabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" + }, + "TimeoutMinutes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-image-imagetestsconfiguration.html#cfn-imagebuilder-image-imagetestsconfiguration-timeoutminutes", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Immutable" + } + } + }, + "AWS::ImageBuilder::ImagePipeline.ImageTestsConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagepipeline-imagetestsconfiguration.html", + "Properties": { + "ImageTestsEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagepipeline-imagetestsconfiguration.html#cfn-imagebuilder-imagepipeline-imagetestsconfiguration-imagetestsenabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "TimeoutMinutes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagepipeline-imagetestsconfiguration.html#cfn-imagebuilder-imagepipeline-imagetestsconfiguration-timeoutminutes", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::ImageBuilder::ImagePipeline.Schedule": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagepipeline-schedule.html", + "Properties": { + "PipelineExecutionStartCondition": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagepipeline-schedule.html#cfn-imagebuilder-imagepipeline-schedule-pipelineexecutionstartcondition", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ScheduleExpression": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagepipeline-schedule.html#cfn-imagebuilder-imagepipeline-schedule-scheduleexpression", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::ImageBuilder::ImageRecipe.ComponentConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-componentconfiguration.html", + "Properties": { + "ComponentArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-componentconfiguration.html#cfn-imagebuilder-imagerecipe-componentconfiguration-componentarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, + "AWS::ImageBuilder::ImageRecipe.EbsInstanceBlockDeviceSpecification": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification.html", + "Properties": { + "DeleteOnTermination": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification.html#cfn-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification-deleteontermination", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" + }, + "Encrypted": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification.html#cfn-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification-encrypted", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" + }, + "Iops": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification.html#cfn-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification-iops", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Immutable" + }, + "KmsKeyId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification.html#cfn-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification-kmskeyid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "SnapshotId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification.html#cfn-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification-snapshotid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "VolumeSize": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification.html#cfn-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification-volumesize", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Immutable" + }, + "VolumeType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification.html#cfn-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification-volumetype", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, + "AWS::ImageBuilder::ImageRecipe.InstanceBlockDeviceMapping": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-instanceblockdevicemapping.html", + "Properties": { + "DeviceName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-instanceblockdevicemapping.html#cfn-imagebuilder-imagerecipe-instanceblockdevicemapping-devicename", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Ebs": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-instanceblockdevicemapping.html#cfn-imagebuilder-imagerecipe-instanceblockdevicemapping-ebs", + "Required": false, + "Type": "EbsInstanceBlockDeviceSpecification", + "UpdateType": "Immutable" + }, + "NoDevice": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-instanceblockdevicemapping.html#cfn-imagebuilder-imagerecipe-instanceblockdevicemapping-nodevice", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "VirtualName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-instanceblockdevicemapping.html#cfn-imagebuilder-imagerecipe-instanceblockdevicemapping-virtualname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, + "AWS::ImageBuilder::InfrastructureConfiguration.Logging": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-infrastructureconfiguration-logging.html", + "Properties": { + "S3Logs": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-infrastructureconfiguration-logging.html#cfn-imagebuilder-infrastructureconfiguration-logging-s3logs", + "Required": false, + "Type": "S3Logs", + "UpdateType": "Mutable" + } + } + }, + "AWS::ImageBuilder::InfrastructureConfiguration.S3Logs": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-infrastructureconfiguration-s3logs.html", + "Properties": { + "S3BucketName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-infrastructureconfiguration-s3logs.html#cfn-imagebuilder-infrastructureconfiguration-s3logs-s3bucketname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "S3KeyPrefix": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-infrastructureconfiguration-s3logs.html#cfn-imagebuilder-infrastructureconfiguration-s3logs-s3keyprefix", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::IoT1Click::Project.DeviceTemplate": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot1click-project-devicetemplate.html", "Properties": { @@ -23716,6 +23996,20 @@ } } }, + "AWS::Macie::FindingsFilter.Criterion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-macie-findingsfilter-criterion.html" + }, + "AWS::Macie::FindingsFilter.FindingCriteria": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-macie-findingsfilter-findingcriteria.html", + "Properties": { + "Criterion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-macie-findingsfilter-findingcriteria.html#cfn-macie-findingsfilter-findingcriteria-criterion", + "Required": false, + "Type": "Criterion", + "UpdateType": "Mutable" + } + } + }, "AWS::ManagedBlockchain::Member.ApprovalThresholdPolicy": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-managedblockchain-member-approvalthresholdpolicy.html", "Properties": { @@ -28499,19 +28793,6 @@ } } }, - "AWS::SSM::Association.ParameterValues": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-association-parametervalues.html", - "Properties": { - "ParameterValues": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-association-parametervalues.html#cfn-ssm-association-parametervalues-parametervalues", - "DuplicatesAllowed": false, - "PrimitiveItemType": "String", - "Required": true, - "Type": "List", - "UpdateType": "Mutable" - } - } - }, "AWS::SSM::Association.S3OutputLocation": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-association-s3outputlocation.html", "Properties": { @@ -28526,6 +28807,12 @@ "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" + }, + "OutputS3Region": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-association-s3outputlocation.html#cfn-ssm-association-s3outputlocation-outputs3region", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" } } }, @@ -28536,15 +28823,14 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-association-target.html#cfn-ssm-association-target-key", "PrimitiveType": "String", "Required": true, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "Values": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-association-target.html#cfn-ssm-association-target-values", - "DuplicatesAllowed": false, "PrimitiveItemType": "String", "Required": true, "Type": "List", - "UpdateType": "Immutable" + "UpdateType": "Mutable" } } }, @@ -29422,6 +29708,9 @@ } } }, + "AWS::StepFunctions::StateMachine.DefinitionSubstitutions": { + "PrimitiveType": "Json" + }, "AWS::StepFunctions::StateMachine.LogDestination": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stepfunctions-statemachine-logdestination.html", "Properties": { @@ -29457,6 +29746,29 @@ } } }, + "AWS::StepFunctions::StateMachine.S3Location": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stepfunctions-statemachine-s3location.html", + "Properties": { + "Bucket": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stepfunctions-statemachine-s3location.html#cfn-stepfunctions-statemachine-s3location-bucket", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Key": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stepfunctions-statemachine-s3location.html#cfn-stepfunctions-statemachine-s3location-key", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Version": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stepfunctions-statemachine-s3location.html#cfn-stepfunctions-statemachine-s3location-version", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::StepFunctions::StateMachine.TagsEntry": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stepfunctions-statemachine-tagsentry.html", "Properties": { @@ -31602,7 +31914,7 @@ } } }, - "ResourceSpecificationVersion": "14.1.0", + "ResourceSpecificationVersion": "14.4.0", "ResourceTypes": { "AWS::ACMPCA::Certificate": { "Attributes": { @@ -34993,6 +35305,11 @@ } }, "AWS::Athena::NamedQuery": { + "Attributes": { + "NamedQueryId": { + "PrimitiveType": "String" + } + }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-athena-namedquery.html", "Properties": { "Database": { @@ -35967,6 +36284,12 @@ "Required": false, "UpdateType": "Immutable" }, + "ConnectionType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloud9-environmentec2.html#cfn-cloud9-environmentec2-connectiontype", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, "Description": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloud9-environmentec2.html#cfn-cloud9-environmentec2-description", "PrimitiveType": "String", @@ -37238,6 +37561,13 @@ "PrimitiveType": "String", "Required": true, "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codestarconnections-connection.html#cfn-codestarconnections-connection-tags", + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" } } }, @@ -38557,6 +38887,12 @@ "Type": "MongoDbSettings", "UpdateType": "Mutable" }, + "NeptuneSettings": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dms-endpoint.html#cfn-dms-endpoint-neptunesettings", + "Required": false, + "Type": "NeptuneSettings", + "UpdateType": "Mutable" + }, "Password": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dms-endpoint.html#cfn-dms-endpoint-password", "PrimitiveType": "String", @@ -38853,6 +39189,12 @@ "PrimitiveType": "String", "Required": true, "UpdateType": "Immutable" + }, + "TaskData": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dms-replicationtask.html#cfn-dms-replicationtask-taskdata", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" } } }, @@ -41921,15 +42263,13 @@ }, "ClusterSettings": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-cluster.html#cfn-ecs-cluster-clustersettings", - "DuplicatesAllowed": false, - "ItemType": "ClusterSetting", + "ItemType": "ClusterSettings", "Required": false, "Type": "List", "UpdateType": "Mutable" }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-cluster.html#cfn-ecs-cluster-tags", - "DuplicatesAllowed": true, "ItemType": "Tag", "Required": false, "Type": "List", @@ -44812,6 +45152,151 @@ } } }, + "AWS::GlobalAccelerator::Accelerator": { + "Attributes": { + "AcceleratorArn": { + "PrimitiveType": "String" + }, + "DnsName": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-accelerator.html", + "Properties": { + "Enabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-accelerator.html#cfn-globalaccelerator-accelerator-enabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "IpAddressType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-accelerator.html#cfn-globalaccelerator-accelerator-ipaddresstype", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "IpAddresses": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-accelerator.html#cfn-globalaccelerator-accelerator-ipaddresses", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-accelerator.html#cfn-globalaccelerator-accelerator-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-accelerator.html#cfn-globalaccelerator-accelerator-tags", + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::GlobalAccelerator::EndpointGroup": { + "Attributes": { + "EndpointGroupArn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-endpointgroup.html", + "Properties": { + "EndpointConfigurations": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-endpointgroup.html#cfn-globalaccelerator-endpointgroup-endpointconfigurations", + "ItemType": "EndpointConfiguration", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "EndpointGroupRegion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-endpointgroup.html#cfn-globalaccelerator-endpointgroup-endpointgroupregion", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "HealthCheckIntervalSeconds": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-endpointgroup.html#cfn-globalaccelerator-endpointgroup-healthcheckintervalseconds", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "HealthCheckPath": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-endpointgroup.html#cfn-globalaccelerator-endpointgroup-healthcheckpath", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "HealthCheckPort": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-endpointgroup.html#cfn-globalaccelerator-endpointgroup-healthcheckport", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "HealthCheckProtocol": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-endpointgroup.html#cfn-globalaccelerator-endpointgroup-healthcheckprotocol", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ListenerArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-endpointgroup.html#cfn-globalaccelerator-endpointgroup-listenerarn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "ThresholdCount": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-endpointgroup.html#cfn-globalaccelerator-endpointgroup-thresholdcount", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "TrafficDialPercentage": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-endpointgroup.html#cfn-globalaccelerator-endpointgroup-trafficdialpercentage", + "PrimitiveType": "Double", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::GlobalAccelerator::Listener": { + "Attributes": { + "ListenerArn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-listener.html", + "Properties": { + "AcceleratorArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-listener.html#cfn-globalaccelerator-listener-acceleratorarn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "ClientAffinity": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-listener.html#cfn-globalaccelerator-listener-clientaffinity", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "PortRanges": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-listener.html#cfn-globalaccelerator-listener-portranges", + "ItemType": "PortRange", + "Required": true, + "Type": "List", + "UpdateType": "Mutable" + }, + "Protocol": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-listener.html#cfn-globalaccelerator-listener-protocol", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::Glue::Classifier": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-glue-classifier.html", "Properties": { @@ -46471,6 +46956,360 @@ } } }, + "AWS::ImageBuilder::Component": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + }, + "Encrypted": { + "PrimitiveType": "Boolean" + }, + "Type": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-component.html", + "Properties": { + "ChangeDescription": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-component.html#cfn-imagebuilder-component-changedescription", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Data": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-component.html#cfn-imagebuilder-component-data", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-component.html#cfn-imagebuilder-component-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "KmsKeyId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-component.html#cfn-imagebuilder-component-kmskeyid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-component.html#cfn-imagebuilder-component-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Platform": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-component.html#cfn-imagebuilder-component-platform", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-component.html#cfn-imagebuilder-component-tags", + "PrimitiveItemType": "String", + "Required": false, + "Type": "Map", + "UpdateType": "Immutable" + }, + "Uri": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-component.html#cfn-imagebuilder-component-uri", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Version": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-component.html#cfn-imagebuilder-component-version", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::ImageBuilder::DistributionConfiguration": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-distributionconfiguration.html", + "Properties": { + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-distributionconfiguration.html#cfn-imagebuilder-distributionconfiguration-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Distributions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-distributionconfiguration.html#cfn-imagebuilder-distributionconfiguration-distributions", + "ItemType": "Distribution", + "Required": true, + "Type": "List", + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-distributionconfiguration.html#cfn-imagebuilder-distributionconfiguration-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-distributionconfiguration.html#cfn-imagebuilder-distributionconfiguration-tags", + "PrimitiveItemType": "String", + "Required": false, + "Type": "Map", + "UpdateType": "Mutable" + } + } + }, + "AWS::ImageBuilder::Image": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + }, + "ImageId": { + "PrimitiveType": "String" + }, + "OutputResources": { + "Type": "OutputResources" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-image.html", + "Properties": { + "DistributionConfigurationArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-image.html#cfn-imagebuilder-image-distributionconfigurationarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "ImageRecipeArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-image.html#cfn-imagebuilder-image-imagerecipearn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "ImageTestsConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-image.html#cfn-imagebuilder-image-imagetestsconfiguration", + "Required": false, + "Type": "ImageTestsConfiguration", + "UpdateType": "Immutable" + }, + "InfrastructureConfigurationArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-image.html#cfn-imagebuilder-image-infrastructureconfigurationarn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-image.html#cfn-imagebuilder-image-tags", + "PrimitiveItemType": "String", + "Required": false, + "Type": "Map", + "UpdateType": "Immutable" + } + } + }, + "AWS::ImageBuilder::ImagePipeline": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html", + "Properties": { + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "DistributionConfigurationArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-distributionconfigurationarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ImageRecipeArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-imagerecipearn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "ImageTestsConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-imagetestsconfiguration", + "Required": false, + "Type": "ImageTestsConfiguration", + "UpdateType": "Mutable" + }, + "InfrastructureConfigurationArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-infrastructureconfigurationarn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Schedule": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-schedule", + "Required": false, + "Type": "Schedule", + "UpdateType": "Mutable" + }, + "Status": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-status", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-tags", + "PrimitiveItemType": "String", + "Required": false, + "Type": "Map", + "UpdateType": "Mutable" + } + } + }, + "AWS::ImageBuilder::ImageRecipe": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagerecipe.html", + "Properties": { + "BlockDeviceMappings": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagerecipe.html#cfn-imagebuilder-imagerecipe-blockdevicemappings", + "ItemType": "InstanceBlockDeviceMapping", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, + "Components": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagerecipe.html#cfn-imagebuilder-imagerecipe-components", + "ItemType": "ComponentConfiguration", + "Required": true, + "Type": "List", + "UpdateType": "Immutable" + }, + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagerecipe.html#cfn-imagebuilder-imagerecipe-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagerecipe.html#cfn-imagebuilder-imagerecipe-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "ParentImage": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagerecipe.html#cfn-imagebuilder-imagerecipe-parentimage", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagerecipe.html#cfn-imagebuilder-imagerecipe-tags", + "PrimitiveItemType": "String", + "Required": false, + "Type": "Map", + "UpdateType": "Immutable" + }, + "Version": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagerecipe.html#cfn-imagebuilder-imagerecipe-version", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::ImageBuilder::InfrastructureConfiguration": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-infrastructureconfiguration.html", + "Properties": { + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-infrastructureconfiguration.html#cfn-imagebuilder-infrastructureconfiguration-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "InstanceProfileName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-infrastructureconfiguration.html#cfn-imagebuilder-infrastructureconfiguration-instanceprofilename", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "InstanceTypes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-infrastructureconfiguration.html#cfn-imagebuilder-infrastructureconfiguration-instancetypes", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "KeyPair": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-infrastructureconfiguration.html#cfn-imagebuilder-infrastructureconfiguration-keypair", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Logging": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-infrastructureconfiguration.html#cfn-imagebuilder-infrastructureconfiguration-logging", + "PrimitiveType": "Json", + "Required": false, + "Type": "Logging", + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-infrastructureconfiguration.html#cfn-imagebuilder-infrastructureconfiguration-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "SecurityGroupIds": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-infrastructureconfiguration.html#cfn-imagebuilder-infrastructureconfiguration-securitygroupids", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "SnsTopicArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-infrastructureconfiguration.html#cfn-imagebuilder-infrastructureconfiguration-snstopicarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "SubnetId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-infrastructureconfiguration.html#cfn-imagebuilder-infrastructureconfiguration-subnetid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-infrastructureconfiguration.html#cfn-imagebuilder-infrastructureconfiguration-tags", + "PrimitiveItemType": "String", + "Required": false, + "Type": "Map", + "UpdateType": "Mutable" + }, + "TerminateInstanceOnFailure": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-infrastructureconfiguration.html#cfn-imagebuilder-infrastructureconfiguration-terminateinstanceonfailure", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::Inspector::AssessmentTarget": { "Attributes": { "Arn": { @@ -48018,6 +48857,135 @@ } } }, + "AWS::Macie::CustomDataIdentifier": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + }, + "CreatedAt": { + "PrimitiveType": "String" + }, + "Deleted": { + "PrimitiveType": "Boolean" + }, + "Id": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-customdataidentifier.html", + "Properties": { + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-customdataidentifier.html#cfn-macie-customdataidentifier-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "IgnoreWords": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-customdataidentifier.html#cfn-macie-customdataidentifier-ignorewords", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, + "Keywords": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-customdataidentifier.html#cfn-macie-customdataidentifier-keywords", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, + "MaximumMatchDistance": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-customdataidentifier.html#cfn-macie-customdataidentifier-maximummatchdistance", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Immutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-customdataidentifier.html#cfn-macie-customdataidentifier-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Regex": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-customdataidentifier.html#cfn-macie-customdataidentifier-regex", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::Macie::FindingsFilter": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + }, + "FindingsFilterListItems": { + "ItemType": "FindingsFilterListItem", + "Type": "List" + }, + "Id": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-findingsfilter.html", + "Properties": { + "Action": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-findingsfilter.html#cfn-macie-findingsfilter-action", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-findingsfilter.html#cfn-macie-findingsfilter-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "FindingCriteria": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-findingsfilter.html#cfn-macie-findingsfilter-findingcriteria", + "Required": true, + "Type": "FindingCriteria", + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-findingsfilter.html#cfn-macie-findingsfilter-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Position": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-findingsfilter.html#cfn-macie-findingsfilter-position", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::Macie::Session": { + "Attributes": { + "AwsAccountId": { + "PrimitiveType": "String" + }, + "ServiceRole": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-session.html", + "Properties": { + "FindingPublishingFrequency": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-session.html#cfn-macie-session-findingpublishingfrequency", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Status": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-session.html#cfn-macie-session-status", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::ManagedBlockchain::Member": { "Attributes": { "MemberId": { @@ -48572,12 +49540,30 @@ "Required": false, "UpdateType": "Mutable" }, + "RestoreToTime": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-neptune-dbcluster.html#cfn-neptune-dbcluster-restoretotime", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "RestoreType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-neptune-dbcluster.html#cfn-neptune-dbcluster-restoretype", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, "SnapshotIdentifier": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-neptune-dbcluster.html#cfn-neptune-dbcluster-snapshotidentifier", "PrimitiveType": "String", "Required": false, "UpdateType": "Immutable" }, + "SourceDBClusterIdentifier": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-neptune-dbcluster.html#cfn-neptune-dbcluster-sourcedbclusteridentifier", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, "StorageEncrypted": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-neptune-dbcluster.html#cfn-neptune-dbcluster-storageencrypted", "PrimitiveType": "Boolean", @@ -48591,6 +49577,12 @@ "Type": "List", "UpdateType": "Mutable" }, + "UseLatestRestorableTime": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-neptune-dbcluster.html#cfn-neptune-dbcluster-uselatestrestorabletime", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" + }, "VpcSecurityGroupIds": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-neptune-dbcluster.html#cfn-neptune-dbcluster-vpcsecuritygroupids", "PrimitiveItemType": "String", @@ -52994,6 +53986,11 @@ } }, "AWS::SSM::Association": { + "Attributes": { + "AssociationId": { + "PrimitiveType": "String" + } + }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html", "Properties": { "AssociationName": { @@ -53002,6 +53999,18 @@ "Required": false, "UpdateType": "Mutable" }, + "AutomationTargetParameterName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-automationtargetparametername", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ComplianceSeverity": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-complianceseverity", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "DocumentVersion": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-documentversion", "PrimitiveType": "String", @@ -53012,13 +54021,25 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-instanceid", "PrimitiveType": "String", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" + }, + "MaxConcurrency": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-maxconcurrency", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "MaxErrors": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-maxerrors", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-name", "PrimitiveType": "String", "Required": true, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "OutputLocation": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-outputlocation", @@ -53028,7 +54049,6 @@ }, "Parameters": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-parameters", - "DuplicatesAllowed": false, "ItemType": "ParameterValues", "Required": false, "Type": "Map", @@ -53040,13 +54060,24 @@ "Required": false, "UpdateType": "Mutable" }, + "SyncCompliance": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-synccompliance", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "Targets": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-targets", - "DuplicatesAllowed": false, "ItemType": "Target", "Required": false, "Type": "List", - "UpdateType": "Immutable" + "UpdateType": "Mutable" + }, + "WaitForSuccessTimeoutSeconds": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-waitforsuccesstimeoutseconds", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" } } }, @@ -53290,6 +54321,12 @@ "Required": false, "UpdateType": "Mutable" }, + "DataType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-parameter.html#cfn-ssm-parameter-datatype", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "Description": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-parameter.html#cfn-ssm-parameter-description", "PrimitiveType": "String", @@ -53983,6 +55020,12 @@ "Type": "List", "UpdateType": "Mutable" }, + "ReplaceProvisioningArtifacts": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicecatalog-cloudformationproduct.html#cfn-servicecatalog-cloudformationproduct-replaceprovisioningartifacts", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, "SupportDescription": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicecatalog-cloudformationproduct.html#cfn-servicecatalog-cloudformationproduct-supportdescription", "PrimitiveType": "String", @@ -54649,10 +55692,22 @@ }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html", "Properties": { + "DefinitionS3Location": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html#cfn-stepfunctions-statemachine-definitions3location", + "Required": false, + "Type": "S3Location", + "UpdateType": "Mutable" + }, "DefinitionString": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html#cfn-stepfunctions-statemachine-definitionstring", "PrimitiveType": "String", - "Required": true, + "Required": false, + "UpdateType": "Mutable" + }, + "DefinitionSubstitutions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html#cfn-stepfunctions-statemachine-definitionsubstitutions", + "Required": false, + "Type": "DefinitionSubstitutions", "UpdateType": "Mutable" }, "LoggingConfiguration": { @@ -54731,7 +55786,7 @@ }, "RunConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-synthetics-canary.html#cfn-synthetics-canary-runconfig", - "Required": false, + "Required": true, "Type": "RunConfig", "UpdateType": "Mutable" }, diff --git a/packages/@aws-cdk/cfnspec/spec-source/540_SSM_Association_Parameters_patch.json b/packages/@aws-cdk/cfnspec/spec-source/540_SSM_Association_Parameters_patch.json new file mode 100644 index 0000000000000..0059cd2577767 --- /dev/null +++ b/packages/@aws-cdk/cfnspec/spec-source/540_SSM_Association_Parameters_patch.json @@ -0,0 +1,20 @@ +{ + "ResourceTypes": { + "AWS::SSM::Association": { + "patch": { + "description": "Removes 'ItemType' property since 'ParameterValues' is (currently) not defined in the spec and the documentation states it to be a list of String", + "operations": [ + { + "op": "remove", + "path": "/Properties/Parameters/ItemType" + }, + { + "op": "add", + "path": "/Properties/Parameters/PrimitiveItemType", + "value": "String" + } + ] + } + } + } +} diff --git a/packages/@aws-cdk/cfnspec/spec-source/550_ImageBuilder_Image_Attributes_OutputResources_patch.json b/packages/@aws-cdk/cfnspec/spec-source/550_ImageBuilder_Image_Attributes_OutputResources_patch.json new file mode 100644 index 0000000000000..858350a6c2bc5 --- /dev/null +++ b/packages/@aws-cdk/cfnspec/spec-source/550_ImageBuilder_Image_Attributes_OutputResources_patch.json @@ -0,0 +1,21 @@ +{ + "ResourceTypes": { + "AWS::ImageBuilder::Image": { + "patch": { + "description": "Replaces 'OutputResources' attribute type to be an array of Strings as it is (currently) not defined in the spec", + "operations": [ + { + "op": "replace", + "path": "/Attributes/OutputResources/Type", + "value": "List" + }, + { + "op": "add", + "path": "/Attributes/OutputResources/PrimitiveItemType", + "value": "String" + } + ] + } + } + } +} diff --git a/packages/@aws-cdk/cfnspec/spec-source/560_Macie_FindingsFilter_Attributes_FindingsFilterListItems_patch.json b/packages/@aws-cdk/cfnspec/spec-source/560_Macie_FindingsFilter_Attributes_FindingsFilterListItems_patch.json new file mode 100644 index 0000000000000..035ce76d073fb --- /dev/null +++ b/packages/@aws-cdk/cfnspec/spec-source/560_Macie_FindingsFilter_Attributes_FindingsFilterListItems_patch.json @@ -0,0 +1,20 @@ +{ + "ResourceTypes": { + "AWS::Macie::FindingsFilter": { + "patch": { + "description": "Replaces 'FindingsFilterListItems' attribute to be an array of JSON values as it is (currently) not defined in the spec", + "operations": [ + { + "op": "remove", + "path": "/Attributes/FindingsFilterListItems/ItemType" + }, + { + "op": "add", + "path": "/Attributes/FindingsFilterListItems/PrimitiveItemType", + "value": "Json" + } + ] + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/README.md b/packages/@aws-cdk/cloudformation-include/README.md index 9996c500feb7f..a64d7b988e9bb 100644 --- a/packages/@aws-cdk/cloudformation-include/README.md +++ b/packages/@aws-cdk/cloudformation-include/README.md @@ -88,6 +88,24 @@ const bucket = s3.Bucket.fromBucketName(this, 'L2Bucket', cfnBucket.ref); // bucket is of type s3.IBucket ``` +## Conditions + +If your template uses [CloudFormation Conditions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/conditions-section-structure.html), +you can retrieve them from your template: + +```typescript +import * as core from '@aws-cdk/core'; + +const condition: core.CfnCondition = cfnTemplate.getCondition('MyCondition'); +``` + +The `CfnCondition` object is mutable, +and any changes you make to it will be reflected in the resulting template: + +```typescript +condition.expression = core.Fn.conditionEquals(1, 2); +``` + ## Known limitations This module is still in its early, experimental stage, @@ -98,13 +116,13 @@ All items unchecked below are currently not supported. - [x] Resources - [ ] Parameters -- [ ] Conditions +- [x] Conditions - [ ] Outputs ### [Resource attributes](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-product-attribute-reference.html): - [x] Properties -- [ ] Condition +- [x] Condition - [x] DependsOn - [ ] CreationPolicy - [ ] UpdatePolicy @@ -119,7 +137,7 @@ All items unchecked below are currently not supported. - [x] Fn::Join - [x] Fn::If - [ ] Fn::And -- [ ] Fn::Equals +- [x] Fn::Equals - [ ] Fn::Not - [ ] Fn::Or - [ ] Fn::Base64 diff --git a/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts index d825a7ae24845..9b1c21e5a590a 100644 --- a/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts +++ b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts @@ -1,4 +1,5 @@ import * as core from '@aws-cdk/core'; +import * as cfn_parse from '@aws-cdk/core/lib/cfn-parse'; import * as cfn_type_to_l1_mapping from './cfn-type-to-l1-mapping'; import * as futils from './file-utils'; @@ -20,6 +21,7 @@ export interface CfnIncludeProps { * Any modifications made on the returned resource objects will be reflected in the resulting CDK template. */ export class CfnInclude extends core.CfnElement { + private readonly conditions: { [conditionName: string]: core.CfnCondition } = {}; private readonly resources: { [logicalId: string]: core.CfnResource } = {}; private readonly template: any; private readonly preserveLogicalIds: boolean; @@ -33,7 +35,12 @@ export class CfnInclude extends core.CfnElement { // ToDo implement preserveLogicalIds=false this.preserveLogicalIds = true; - // instantiate all resources as CDK L1 objects + // first, instantiate the conditions + for (const conditionName of Object.keys(this.template.Conditions || {})) { + this.createCondition(conditionName); + } + + // then, instantiate all resources as CDK L1 objects for (const logicalId of Object.keys(this.template.Resources || {})) { this.getOrCreateResource(logicalId); } @@ -63,14 +70,32 @@ export class CfnInclude extends core.CfnElement { return ret; } + /** + * Returns the CfnCondition object from the 'Conditions' + * section of the CloudFormation template with the give name. + * Any modifications performed on that object will be reflected in the resulting CDK template. + * + * If a Condition with the given name is not present in the template, + * throws an exception. + * + * @param conditionName the name of the Condition in the CloudFormation template file + */ + public getCondition(conditionName: string): core.CfnCondition { + const ret = this.conditions[conditionName]; + if (!ret) { + throw new Error(`Condition with name '${conditionName}' was not found in the template`); + } + return ret; + } + /** @internal */ public _toCloudFormation(): object { const ret: { [section: string]: any } = {}; for (const section of Object.keys(this.template)) { // render all sections of the template unchanged, - // except Resources, which will be taken care of by the created L1s - if (section !== 'Resources') { + // except Conditions and Resources, which will be taken care of by the created L1s + if (section !== 'Conditions' && section !== 'Resources') { ret[section] = this.template[section]; } } @@ -78,6 +103,18 @@ export class CfnInclude extends core.CfnElement { return ret; } + private createCondition(conditionName: string): void { + // ToDo condition expressions can refer to other conditions - + // will be important when implementing preserveLogicalIds=false + const expression = cfn_parse.FromCloudFormation.parseValue(this.template.Conditions[conditionName]); + const cfnCondition = new core.CfnCondition(this, conditionName, { + expression, + }); + // ToDo handle renaming of the logical IDs of the conditions + cfnCondition.overrideLogicalId(conditionName); + this.conditions[conditionName] = cfnCondition; + } + private getOrCreateResource(logicalId: string): core.CfnResource { const ret = this.resources[logicalId]; if (ret) { @@ -92,7 +129,7 @@ export class CfnInclude extends core.CfnElement { throw new Error(`Unrecognized CloudFormation resource type: '${resourceAttributes.Type}'`); } // fail early for resource attributes we don't support yet - const knownAttributes = ['Type', 'Properties', 'DependsOn', 'DeletionPolicy', 'UpdateReplacePolicy', 'Metadata']; + const knownAttributes = ['Type', 'Properties', 'Condition', 'DependsOn', 'DeletionPolicy', 'UpdateReplacePolicy', 'Metadata']; for (const attribute of Object.keys(resourceAttributes)) { if (!knownAttributes.includes(attribute)) { throw new Error(`The ${attribute} resource attribute is not supported by cloudformation-include yet. ` + @@ -105,6 +142,10 @@ export class CfnInclude extends core.CfnElement { const jsClassFromModule = module[className.join('.')]; const self = this; const finder: core.ICfnFinder = { + findCondition(conditionName: string): core.CfnCondition | undefined { + return self.conditions[conditionName]; + }, + findResource(lId: string): core.CfnResource | undefined { if (!(lId in (self.template.Resources || {}))) { return undefined; diff --git a/packages/@aws-cdk/cloudformation-include/package.json b/packages/@aws-cdk/cloudformation-include/package.json index 71119c75b1020..5389145c4941a 100644 --- a/packages/@aws-cdk/cloudformation-include/package.json +++ b/packages/@aws-cdk/cloudformation-include/package.json @@ -117,10 +117,12 @@ "@aws-cdk/aws-fms": "0.0.0", "@aws-cdk/aws-fsx": "0.0.0", "@aws-cdk/aws-gamelift": "0.0.0", + "@aws-cdk/aws-globalaccelerator": "0.0.0", "@aws-cdk/aws-glue": "0.0.0", "@aws-cdk/aws-greengrass": "0.0.0", "@aws-cdk/aws-guardduty": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-imagebuilder": "0.0.0", "@aws-cdk/aws-inspector": "0.0.0", "@aws-cdk/aws-iot": "0.0.0", "@aws-cdk/aws-iot1click": "0.0.0", @@ -134,6 +136,7 @@ "@aws-cdk/aws-lakeformation": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-macie": "0.0.0", "@aws-cdk/aws-managedblockchain": "0.0.0", "@aws-cdk/aws-mediaconvert": "0.0.0", "@aws-cdk/aws-medialive": "0.0.0", @@ -236,10 +239,12 @@ "@aws-cdk/aws-fms": "0.0.0", "@aws-cdk/aws-fsx": "0.0.0", "@aws-cdk/aws-gamelift": "0.0.0", + "@aws-cdk/aws-globalaccelerator": "0.0.0", "@aws-cdk/aws-glue": "0.0.0", "@aws-cdk/aws-greengrass": "0.0.0", "@aws-cdk/aws-guardduty": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-imagebuilder": "0.0.0", "@aws-cdk/aws-inspector": "0.0.0", "@aws-cdk/aws-iot": "0.0.0", "@aws-cdk/aws-iot1click": "0.0.0", @@ -253,6 +258,7 @@ "@aws-cdk/aws-lakeformation": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-macie": "0.0.0", "@aws-cdk/aws-managedblockchain": "0.0.0", "@aws-cdk/aws-mediaconvert": "0.0.0", "@aws-cdk/aws-medialive": "0.0.0", diff --git a/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts index 3e03ed5468de4..038ea1e9e6dde 100644 --- a/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts @@ -46,6 +46,12 @@ describe('CDK Include', () => { includeTestTemplate(stack, 'non-existent-depends-on.json'); }).toThrow(/Resource 'Bucket2' depends on 'Bucket1' that doesn't exist/); }); + + test("throws a validation exception for a template referencing a Condition resource attribute that doesn't exist", () => { + expect(() => { + includeTestTemplate(stack, 'non-existent-condition.json'); + }).toThrow(/Resource 'Bucket' uses Condition 'AlwaysFalseCond' that doesn't exist/); + }); }); function includeTestTemplate(scope: core.Construct, testTemplate: string): inc.CfnInclude { diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/non-existent-condition.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/non-existent-condition.json new file mode 100644 index 0000000000000..dbaef4fd3a5ed --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/non-existent-condition.json @@ -0,0 +1,8 @@ +{ + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Condition": "AlwaysFalseCond" + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts index 121e163fb0ef4..0291de98eba95 100644 --- a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts @@ -232,6 +232,38 @@ describe('CDK Include', () => { }, ResourcePart.CompleteDefinition); }); + test('correctly parses Conditions and the Condition resource attribute', () => { + const cfnTemplate = includeTestTemplate(stack, 'resource-attribute-condition.json'); + const alwaysFalseCondition = cfnTemplate.getCondition('AlwaysFalseCond'); + const cfnBucket = cfnTemplate.getResource('Bucket'); + + expect(cfnBucket.cfnOptions.condition).toBe(alwaysFalseCondition); + expect(stack).toMatchTemplate( + loadTestFileToJsObject('resource-attribute-condition.json'), + ); + }); + + test('reflects changes to a retrieved CfnCondition object in the resulting template', () => { + const cfnTemplate = includeTestTemplate(stack, 'resource-attribute-condition.json'); + const alwaysFalseCondition = cfnTemplate.getCondition('AlwaysFalseCond'); + + alwaysFalseCondition.expression = core.Fn.conditionEquals(1, 2); + + expect(stack).toMatchTemplate({ + "Conditions": { + "AlwaysFalseCond": { + "Fn::Equals": [1, 2], + }, + }, + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Condition": "AlwaysFalseCond", + }, + }, + }); + }); + test("throws an exception when encountering a Resource type it doesn't recognize", () => { expect(() => { includeTestTemplate(stack, 'non-existent-resource-type.json'); @@ -244,12 +276,6 @@ describe('CDK Include', () => { }).toThrow(/Unsupported CloudFormation function 'Fn::Base64'/); }); - test('throws an exception when encountering the Condition attribute in a resource', () => { - expect(() => { - includeTestTemplate(stack, 'resource-attribute-condition.json'); - }).toThrow(/The Condition resource attribute is not supported by cloudformation-include yet/); - }); - test('throws an exception when encountering the CreationPolicy attribute in a resource', () => { expect(() => { includeTestTemplate(stack, 'resource-attribute-creation-policy.json'); diff --git a/packages/@aws-cdk/core/lib/cfn-parse.ts b/packages/@aws-cdk/core/lib/cfn-parse.ts index a72f59240776c..67e2d2390ef62 100644 --- a/packages/@aws-cdk/core/lib/cfn-parse.ts +++ b/packages/@aws-cdk/core/lib/cfn-parse.ts @@ -181,9 +181,16 @@ function parseIfCfnIntrinsic(object: any): any { } case 'Fn::If': { // Fn::If takes a 3-element list as its argument + // ToDo the first argument is the name of the condition, + // so we will need to retrieve the actual object from the template + // when we handle preserveLogicalIds=false const value = parseCfnValueToCdkValue(object[key]); return Fn.conditionIf(value[0], value[1], value[2]); } + case 'Fn::Equals': { + const value = parseCfnValueToCdkValue(object[key]); + return Fn.conditionEquals(value[0], value[1]); + } default: throw new Error(`Unsupported CloudFormation function '${key}'`); } diff --git a/packages/@aws-cdk/core/lib/cfn-resource.ts b/packages/@aws-cdk/core/lib/cfn-resource.ts index c385d91b9e237..deeb92e0e2456 100644 --- a/packages/@aws-cdk/core/lib/cfn-resource.ts +++ b/packages/@aws-cdk/core/lib/cfn-resource.ts @@ -116,6 +116,10 @@ export class CfnResource extends CfnRefElement { deletionPolicy = CfnDeletionPolicy.RETAIN; break; + case RemovalPolicy.SNAPSHOT: + deletionPolicy = CfnDeletionPolicy.SNAPSHOT; + break; + default: throw new Error(`Invalid removal policy: ${policy}`); } diff --git a/packages/@aws-cdk/core/lib/from-cfn.ts b/packages/@aws-cdk/core/lib/from-cfn.ts index 25127ad1fefbc..9d3b1544526a2 100644 --- a/packages/@aws-cdk/core/lib/from-cfn.ts +++ b/packages/@aws-cdk/core/lib/from-cfn.ts @@ -1,3 +1,4 @@ +import { CfnCondition } from './cfn-condition'; import { CfnResource } from './cfn-resource'; /** @@ -7,6 +8,13 @@ import { CfnResource } from './cfn-resource'; * @experimental */ export interface ICfnFinder { + /** + * Return the Condition with the given name from the template. + * If there is no Condition with that name in the template, + * returns undefined. + */ + findCondition(conditionName: string): CfnCondition | undefined; + /** * Returns the resource with the given logical ID in the template. * If a resource with that logical ID was not found in the template, diff --git a/packages/@aws-cdk/core/lib/removal-policy.ts b/packages/@aws-cdk/core/lib/removal-policy.ts index e98a6546024c8..879a00f53b4f9 100644 --- a/packages/@aws-cdk/core/lib/removal-policy.ts +++ b/packages/@aws-cdk/core/lib/removal-policy.ts @@ -10,6 +10,17 @@ export enum RemovalPolicy { * in the account, but orphaned from the stack. */ RETAIN = 'retain', + + /** + * This retention policy deletes the resource, + * but saves a snapshot of its data before deleting, + * so that it can be re-created later. + * Only available for some stateful resources, + * like databases, EFS volumes, etc. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-deletionpolicy.html#aws-attribute-deletionpolicy-options + */ + SNAPSHOT = 'snapshot', } export interface RemovalPolicyOptions { diff --git a/packages/@aws-cdk/core/lib/stack.ts b/packages/@aws-cdk/core/lib/stack.ts index 7d5d41fe72feb..72980dfbfbfe2 100644 --- a/packages/@aws-cdk/core/lib/stack.ts +++ b/packages/@aws-cdk/core/lib/stack.ts @@ -839,24 +839,60 @@ export class Stack extends Construct implements ITaggable { } } -function merge(template: any, part: any) { - for (const section of Object.keys(part)) { - const src = part[section]; +function merge(template: any, fragment: any): void { + for (const section of Object.keys(fragment)) { + const src = fragment[section]; // create top-level section if it doesn't exist - let dest = template[section]; + const dest = template[section]; if (!dest) { - template[section] = dest = src; + template[section] = src; } else { - // add all entities from source section to destination section - for (const id of Object.keys(src)) { - if (id in dest) { - throw new Error(`section '${section}' already contains '${id}'`); - } - dest[id] = src[id]; + template[section] = mergeSection(section, dest, src); + } + } +} + +function mergeSection(section: string, val1: any, val2: any): any { + switch (section) { + case 'Description': + return `${val1}\n${val2}`; + case 'AWSTemplateFormatVersion': + if (val1 != null && val2 != null && val1 !== val2) { + throw new Error(`Conflicting CloudFormation template versions provided: '${val1}' and '${val2}`); } + return val1 ?? val2; + case 'Resources': + case 'Conditions': + case 'Parameters': + case 'Outputs': + case 'Mappings': + case 'Metadata': + case 'Transform': + return mergeObjectsWithoutDuplicates(section, val1, val2); + default: + throw new Error(`CDK doesn't know how to merge two instances of the CFN template section '${section}' - ` + + 'please remove one of them from your code'); + } +} + +function mergeObjectsWithoutDuplicates(section: string, dest: any, src: any): any { + if (typeof dest !== 'object') { + throw new Error(`Expecting ${JSON.stringify(dest)} to be an object`); + } + if (typeof src !== 'object') { + throw new Error(`Expecting ${JSON.stringify(src)} to be an object`); + } + + // add all entities from source section to destination section + for (const id of Object.keys(src)) { + if (id in dest) { + throw new Error(`section '${section}' already contains '${id}'`); } + dest[id] = src[id]; } + + return dest; } /** diff --git a/packages/@aws-cdk/core/package.json b/packages/@aws-cdk/core/package.json index 485821ebe8fb7..a0927130ac58f 100644 --- a/packages/@aws-cdk/core/package.json +++ b/packages/@aws-cdk/core/package.json @@ -151,7 +151,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/lodash": "^4.14.152", + "@types/lodash": "^4.14.153", "@types/node": "^10.17.21", "@types/nodeunit": "^0.0.31", "@types/minimatch": "^3.0.3", diff --git a/packages/@aws-cdk/core/test/test.include.ts b/packages/@aws-cdk/core/test/test.include.ts index afe306cc1ed35..159e3189852f9 100644 --- a/packages/@aws-cdk/core/test/test.include.ts +++ b/packages/@aws-cdk/core/test/test.include.ts @@ -50,6 +50,30 @@ export = { test.throws(() => toCloudFormation(stack)); test.done(); }, + + 'correctly merges template sections that contain strings'(test: Test) { + const stack = new Stack(); + + new CfnInclude(stack, 'T1', { + template: { + AWSTemplateFormatVersion: '2010-09-09', + Description: 'Test 1', + }, + }); + new CfnInclude(stack, 'T2', { + template: { + AWSTemplateFormatVersion: '2010-09-09', + Description: 'Test 2', + }, + }); + + test.deepEqual(toCloudFormation(stack), { + AWSTemplateFormatVersion: '2010-09-09', + Description: 'Test 1\nTest 2', + }); + + test.done(); + }, }; const template = { diff --git a/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts index 017ab6e06192c..d0a7994740a19 100644 --- a/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts +++ b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts @@ -205,7 +205,8 @@ export interface AwsCustomResourceProps { readonly onDelete?: AwsSdkCall; /** - * The policy to apply to the resource. + * The policy that will be added to the execution role of the Lambda + * function implementing this custom resource provider. * * The custom resource also implements `iam.IGrantable`, making it possible * to use the `grantXxx()` methods. diff --git a/packages/@aws-cdk/custom-resources/package.json b/packages/@aws-cdk/custom-resources/package.json index 65f8c90f0bcb9..8f799ac5676e2 100644 --- a/packages/@aws-cdk/custom-resources/package.json +++ b/packages/@aws-cdk/custom-resources/package.json @@ -73,7 +73,7 @@ "@aws-cdk/aws-ssm": "0.0.0", "@types/aws-lambda": "^8.10.39", "@types/fs-extra": "^8.1.0", - "@types/sinon": "^9.0.3", + "@types/sinon": "^9.0.4", "aws-sdk": "^2.681.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", diff --git a/packages/@monocdk-experiment/assert/package.json b/packages/@monocdk-experiment/assert/package.json index f100441f91e73..ebd3b55c4681b 100644 --- a/packages/@monocdk-experiment/assert/package.json +++ b/packages/@monocdk-experiment/assert/package.json @@ -37,6 +37,7 @@ "license": "Apache-2.0", "devDependencies": { "@types/jest": "^25.2.3", + "@types/node": "^10.17.24", "cdk-build-tools": "0.0.0", "jest": "^25.5.4", "pkglint": "0.0.0", diff --git a/packages/@monocdk-experiment/rewrite-imports/lib/rewrite.ts b/packages/@monocdk-experiment/rewrite-imports/lib/rewrite.ts index 46a6e11db9003..35b78943f6444 100644 --- a/packages/@monocdk-experiment/rewrite-imports/lib/rewrite.ts +++ b/packages/@monocdk-experiment/rewrite-imports/lib/rewrite.ts @@ -18,6 +18,7 @@ export function rewriteLine(line: string) { } } return line - .replace(/(["'])@aws-cdk\/core(["'])/g, '$1monocdk-experiment$2') // monocdk-experiment => monocdk-experiment - .replace(/(["'])@aws-cdk\/(.+)(["'])/g, '$1monocdk-experiment/$2$3'); // monocdk-experiment/foobar => monocdk-experiment/foobar; + .replace(/(["'])@aws-cdk\/assert(["'])/g, '$1@monocdk-experiment/assert$2') // @aws-cdk/assert => @monocdk-experiment/assert + .replace(/(["'])@aws-cdk\/core(["'])/g, '$1monocdk-experiment$2') // @aws-cdk/core => monocdk-experiment + .replace(/(["'])@aws-cdk\/(.+)(["'])/g, '$1monocdk-experiment/$2$3'); // @aws-cdk/* => monocdk-experiment/*; } diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index 91ade21726f1b..1d3b1ca3b85db 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -86,23 +86,32 @@ $ cdk list --app='node bin/main.js' --long ``` #### `cdk synthesize` -Synthesize the CDK app and outputs CloudFormation templates. If the application contains multiple stacks and no -stack name is provided in the command-line arguments, the `--output` option is mandatory and a CloudFormation template -will be generated in the output folder for each stack. +Synthesizes the CDK app and produces a cloud assembly to a designated output (defaults to `cdk.out`) -By default, templates are generated in YAML format. The `--json` option can be used to switch to JSON. +Typically you don't interact directly with cloud assemblies. They are files that include everything +needed to deploy your app to a cloud environment. For example, it includes an AWS CloudFormation +template for each stack in your app, and a copy of any file assets or Docker images that you reference +in your app. + +If your app contains a single stack or a stack is supplied as an argument to `cdk synth`, the CloudFormation template will also be displayed in the standard output (STDOUT) as `YAML`. + +If there are multiple stacks in your application, `cdk synth` will synthesize the cloud assembly to `cdk.out`. ```console -$ # Generate the template for StackName and output it to STDOUT -$ cdk synthesize --app='node bin/main.js' MyStackName +$ # Synthesize cloud assembly for StackName and output the CloudFormation template to STDOUT +$ cdk synth MyStackName -$ # Generate the template for MyStackName and save it to template.yml -$ cdk synth --app='node bin/main.js' MyStackName --output=template.yml +$ # Synthesize cloud assembly for all the stacks and save them into cdk.out/ +$ cdk synth -$ # Generate templates for all the stacks and save them into templates/ -$ cdk synth --app='node bin/main.js' --output=templates +$ # Synthesize cloud assembly for StackName, but don't include dependencies +$ cdk synth MyStackName --exclusively ``` +See the [AWS Documentation](https://docs.aws.amazon.com/cdk/latest/guide/apps.html#apps_cloud_assembly) to learn more about cloud assemblies. +See the [CDK reference documentation](https://docs.aws.amazon.com/cdk/api/latest/docs/cloud-assembly-schema-readme.html) for details on the cloud assembly specification + + #### `cdk diff` Computes differences between the infrastructure specified in the current state of the CDK app and the currently deployed application (or a user-specified CloudFormation template). This command returns non-zero if any differences are diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index 99b18c3136b9f..583e6dc3d6ee8 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -235,6 +235,17 @@ export async function deployStack(options: DeployStackOptions): Promise { }); test('Termination protection', async () => { - await cdkDeploy('termination-protection'); + const stackName = 'termination-protection'; + await cdkDeploy(stackName); // Try a destroy that should fail - await expect(cdkDestroy('termination-protection')).rejects.toThrow('exited with error'); + await expect(cdkDestroy(stackName)).rejects.toThrow('exited with error'); - await cloudFormation('updateTerminationProtection', { - EnableTerminationProtection: false, - StackName: fullStackName('termination-protection'), - }); + // Can update termination protection even though the change set doesn't contain changes + await cdkDeploy(stackName, { modEnv: { TERMINATION_PROTECTION: 'FALSE' } }); + await cdkDestroy(stackName); }); test('cdk synth', async () => { @@ -160,9 +160,16 @@ test('security related changes without a CLI are expected to fail', async () => // redirect /dev/null to stdin, which means there will not be tty attached // since this stack includes security-related changes, the deployment should // immediately fail because we can't confirm the changes - await expect(cdkDeploy('iam-test', { + const stackName = 'iam-test'; + await expect(cdkDeploy(stackName, { options: ['<', '/dev/null'], // H4x, this only works because I happen to know we pass shell: true. + neverRequireApproval: false, })).rejects.toThrow('exited with error'); + + // Ensure stack was not deployed + await expect(cloudFormation('describeStacks', { + StackName: fullStackName(stackName), + })).rejects.toThrow('does not exist'); }); test('deploy wildcard with outputs', async () => { @@ -430,15 +437,15 @@ test('IAM diff', async () => { // Roughly check for a table like this: // - // ┌───┬─────────────────┬────────┬────────────────┬────────────────────────────┬───────────┐ - // │ │ Resource │ Effect │ Action │ Principal │ Condition │ - // ├───┼─────────────────┼────────┼────────────────┼────────────────────────────┼───────────┤ - // │ + │ ${SomeRole.Arn} │ Allow │ sts:AssumeRole │ Service:ec2.amazon.aws.com │ │ - // └───┴─────────────────┴────────┴────────────────┴────────────────────────────┴───────────┘ + // ┌───┬─────────────────┬────────┬────────────────┬────────────────────────────-──┬───────────┐ + // │ │ Resource │ Effect │ Action │ Principal │ Condition │ + // ├───┼─────────────────┼────────┼────────────────┼───────────────────────────────┼───────────┤ + // │ + │ ${SomeRole.Arn} │ Allow │ sts:AssumeRole │ Service:ec2.${AWS::URLSuffix} │ │ + // └───┴─────────────────┴────────┴────────────────┴───────────────────────────────┴───────────┘ expect(output).toContain('${SomeRole.Arn}'); expect(output).toContain('sts:AssumeRole'); - expect(output).toContain('ec2.amazon.aws.com'); + expect(output).toContain('ec2.${AWS::URLSuffix}'); }); test('fast deploy', async () => { diff --git a/packages/decdk/package.json b/packages/decdk/package.json index bd0e646ed35c7..46565cefab7cd 100644 --- a/packages/decdk/package.json +++ b/packages/decdk/package.json @@ -90,6 +90,7 @@ "@aws-cdk/aws-elasticbeanstalk": "0.0.0", "@aws-cdk/aws-elasticloadbalancing": "0.0.0", "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", + "@aws-cdk/aws-elasticloadbalancingv2-actions": "0.0.0", "@aws-cdk/aws-elasticloadbalancingv2-targets": "0.0.0", "@aws-cdk/aws-elasticsearch": "0.0.0", "@aws-cdk/aws-emr": "0.0.0", @@ -99,10 +100,12 @@ "@aws-cdk/aws-fms": "0.0.0", "@aws-cdk/aws-fsx": "0.0.0", "@aws-cdk/aws-gamelift": "0.0.0", + "@aws-cdk/aws-globalaccelerator": "0.0.0", "@aws-cdk/aws-glue": "0.0.0", "@aws-cdk/aws-greengrass": "0.0.0", "@aws-cdk/aws-guardduty": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-imagebuilder": "0.0.0", "@aws-cdk/aws-inspector": "0.0.0", "@aws-cdk/aws-iot": "0.0.0", "@aws-cdk/aws-iot1click": "0.0.0", @@ -120,6 +123,7 @@ "@aws-cdk/aws-lambda-nodejs": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-logs-destinations": "0.0.0", + "@aws-cdk/aws-macie": "0.0.0", "@aws-cdk/aws-managedblockchain": "0.0.0", "@aws-cdk/aws-mediaconvert": "0.0.0", "@aws-cdk/aws-medialive": "0.0.0", @@ -177,7 +181,6 @@ "fs-extra": "^8.1.0", "jsii-reflect": "^1.5.0", "jsonschema": "^1.2.6", - "@aws-cdk/aws-elasticloadbalancingv2-actions": "0.0.0", "yaml": "1.9.2", "yargs": "^15.3.1" }, diff --git a/packages/monocdk-experiment/.eslintrc.js b/packages/monocdk-experiment/.eslintrc.js new file mode 100644 index 0000000000000..0c8afb4aeb0c3 --- /dev/null +++ b/packages/monocdk-experiment/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/monocdk-experiment/.gitignore b/packages/monocdk-experiment/.gitignore index 5603865e24a28..9b1b4c8c4a775 100644 --- a/packages/monocdk-experiment/.gitignore +++ b/packages/monocdk-experiment/.gitignore @@ -2,7 +2,16 @@ *.d.ts !deps.js !gen.js -staging/ +lib/ tsconfig.json .jsii *.tsbuildinfo + +dist +.LAST_PACKAGE +.LAST_BUILD +*.snk +!.eslintrc.js + +# Ignore barrel import entry points +/*.ts diff --git a/packages/monocdk-experiment/.npmignore b/packages/monocdk-experiment/.npmignore index 0b96cfab1fbc5..85a5b1ed52c6b 100644 --- a/packages/monocdk-experiment/.npmignore +++ b/packages/monocdk-experiment/.npmignore @@ -11,10 +11,13 @@ coverage .nycrc # Build gear +build-tools dist .LAST_BUILD .LAST_PACKAGE -.jsii tsconfig.json *.tsbuildinfo + +!.jsii +.eslintrc.js \ No newline at end of file diff --git a/packages/monocdk-experiment/README.md b/packages/monocdk-experiment/README.md index f7aa85a0ffa37..a82945b85886e 100644 --- a/packages/monocdk-experiment/README.md +++ b/packages/monocdk-experiment/README.md @@ -2,13 +2,49 @@ [![experimental](http://badges.github.io/stability-badges/dist/experimental.svg)](http://github.com/badges/stability-badges) -An __experiment__ to bundle all of the CDK into a single module. Please don't -use this module. +An __experiment__ to bundle all of the CDK into a single module. -## TODO +> :warning: Please don't use this module unless you are interested in providing +> feedback about this experience. -- [ ] Consider if we want core types to be available under the root namespace -- [ ] jsii support -- [x] Bundle all code outside of `lib` -- [ ] Run unit tests +## Usage + +### Installation +To try out `monocdk-experiment` replace all references to CDK Construct +Libraries (most `@aws-cdk/*` packages) in your `package.json` file with a single +entrey referring to `monocdk-experiment`. + +You also need to add a reference to the `constructs` library, according to the +kind of project you are developing: +- For libraries, model the dependency under `devDependencies` **and** `peerDependencies` +- For apps, model the dependency under `dependencies` only + +### Use in your code + +#### Classic import + +You can use a classic import to get access to each service namespaces: + +```ts +import { core, aws_s3 as s3 } from 'monocdk-experiment'; + +const app = new core.App(); +const stack = new core.Stack(app, 'MonoCDK-Stack'); + +new s3.Bucket(stack, 'TestBucket'); +``` + +#### Barrel import + +Alternatively, you can use "barrel" imports: + +```ts +import { App, Stack } from 'monocdk-experiment'; +import { Bucket } from 'monocdk-experiment/aws-s3'; + +const app = new App(); +const stack = new Stack(app, 'MonoCDK-Stack'); + +new Bucket(stack, 'TestBucket'); +``` diff --git a/packages/monocdk-experiment/build-tools/gen.ts b/packages/monocdk-experiment/build-tools/gen.ts new file mode 100644 index 0000000000000..96e7de8d874a1 --- /dev/null +++ b/packages/monocdk-experiment/build-tools/gen.ts @@ -0,0 +1,395 @@ +import * as console from 'console'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as process from 'process'; +import * as ts from 'typescript'; + +const LIB_ROOT = path.resolve(__dirname, '..', 'lib'); + +async function main() { + const libraries = await findLibrariesToPackage(); + const packageJson = await verifyDependencies(libraries); + await prepareSourceFiles(libraries, packageJson); +} + +main().then( + () => process.exit(0), + (err) => { + console.error('❌ An error occurred: ', err.stack); + process.exit(1); + }, +); + +interface LibraryReference { + readonly packageJson: PackageJson; + readonly root: string; + readonly shortName: string; +} + +interface PackageJson { + readonly bundleDependencies?: readonly string[]; + readonly bundledDependencies?: readonly string[]; + readonly dependencies?: { readonly [name: string]: string }; + readonly devDependencies?: { readonly [name: string]: string }; + readonly jsii: { + readonly targets?: { + readonly dotnet?: { + readonly namespace: string; + readonly [key: string]: unknown; + }, + readonly java?: { + readonly package: string; + readonly [key: string]: unknown; + }, + readonly python?: { + readonly module: string; + readonly [key: string]: unknown; + }, + readonly [language: string]: unknown, + }, + }; + readonly name: string; + readonly types: string; + readonly version: string; + readonly [key: string]: unknown; +} + +async function findLibrariesToPackage(): Promise { + console.log('🔍 Discovering libraries that need packaging...'); + + const result = new Array(); + + const librariesRoot = path.resolve(__dirname, '..', '..', '@aws-cdk'); + for (const dir of await fs.readdir(librariesRoot)) { + const packageJson = await fs.readJson(path.resolve(librariesRoot, dir, 'package.json')); + + if (packageJson.private) { + console.log(`\t⚠️ Skipping (private): ${packageJson.name}`); + continue; + } else if (packageJson.deprecated) { + console.log(`\t⚠️ Skipping (deprecated): ${packageJson.name}`); + continue; + } else if (packageJson.jsii == null ) { + console.log(`\t⚠️ Skipping (not jsii-enabled): ${packageJson.name}`); + continue; + } + + result.push({ + packageJson, + root: path.join(librariesRoot, dir), + shortName: packageJson.name.substr('@aws-cdk/'.length), + }); + } + + console.log(`\tℹ️ Found ${result.length} relevant packages!`); + + return result; +} + +async function verifyDependencies(libraries: readonly LibraryReference[]): Promise { + console.log('🧐 Verifying dependencies are complete...'); + const packageJsonPath = path.resolve(__dirname, '..', 'package.json'); + const packageJson = await fs.readJson(packageJsonPath); + + let changed = false; + const toBundle: Record = {}; + + for (const library of libraries) { + for (const depName of library.packageJson.bundleDependencies ?? library.packageJson.bundledDependencies ?? []) { + const requiredVersion = library.packageJson.devDependencies?.[depName] + ?? library.packageJson.dependencies?.[depName] + ?? '*'; + if (toBundle[depName] != null && toBundle[depName] !== requiredVersion) { + throw new Error(`Required to bundle different versions of ${depName}: ${toBundle[depName]} and ${requiredVersion}.`); + } + toBundle[depName] = requiredVersion; + } + + if (library.packageJson.name in packageJson.devDependencies) { + const existingVersion = packageJson.devDependencies[library.packageJson.name]; + if (existingVersion !== library.packageJson.version) { + console.log(`\t⚠️ Incorrect dependency: ${library.packageJson.name} (expected ${library.packageJson.version}, found ${packageJson.devDependencies[library.packageJson.name]})`); + packageJson.devDependencies[library.packageJson.name] = library.packageJson.version; + changed = true; + } + continue; + } + console.log(`\t⚠️ Missing dependency: ${library.packageJson.name}`); + changed = true; + packageJson.devDependencies = sortObject({ + ...packageJson.devDependencies ?? {}, + [library.packageJson.name]: library.packageJson.version, + }); + } + + const workspacePath = path.resolve(__dirname, '..', '..', '..', 'package.json'); + const workspace = await fs.readJson(workspacePath); + let workspaceChanged = false + + const spuriousBundledDeps = new Set(packageJson.bundledDependencies ?? []); + for (const [name, version] of Object.entries(toBundle)) { + spuriousBundledDeps.delete(name); + + const nohoist = `${packageJson.name}/${name}`; + if (!workspace.workspaces.nohoist?.includes(nohoist)) { + console.log(`\t⚠️ Missing yarn workspace nohoist: ${nohoist}`); + workspace.workspaces.nohoist = Array.from(new Set([ + ...workspace.workspaces.nohoist ?? [], + nohoist, + `${nohoist}/**` + ])).sort(); + workspaceChanged = true; + } + + if (!(packageJson.bundledDependencies?.includes(name))) { + console.log(`\t⚠️ Missing bundled dependency: ${name} at ${version}`); + packageJson.bundledDependencies = [ + ...packageJson.bundledDependencies ?? [], + name, + ].sort(); + changed = true; + } + + if (packageJson.dependencies?.[name] !== version) { + console.log(`\t⚠️ Missing or incorrect dependency: ${name} at ${version}`); + packageJson.dependencies = sortObject({ + ...packageJson.dependencies ?? {}, + [name]: version, + }); + changed = true; + } + } + packageJson.bundledDependencies = packageJson.bundledDependencies?.filter((dep: string) => !spuriousBundledDeps.has(dep)); + for (const toRemove of Array.from(spuriousBundledDeps)) { + delete packageJson.dependencies[toRemove]; + changed = true; + } + + if (workspaceChanged) { + await fs.writeFile(workspacePath, JSON.stringify(workspace, null, 2) + '\n', { encoding: 'utf-8' }); + console.log('\t❌ Updated the yarn workspace configuration. Re-run "yarn install", and commit the changes.'); + } + + if (changed) { + await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', { encoding: 'utf8' }); + + throw new Error('Fixed dependency inconsistencies. Commit the updated package.json file.'); + } + console.log('\t✅ Dependencies are correct!'); + return packageJson; +} + +async function prepareSourceFiles(libraries: readonly LibraryReference[], packageJson: PackageJson) { + console.log('📝 Preparing source files...'); + + await fs.remove(LIB_ROOT); + + const indexStatements = new Array(); + for (const library of libraries) { + const libDir = path.join(LIB_ROOT, library.shortName); + await transformPackage(library, packageJson.jsii.targets, libDir, libraries); + + if (library.shortName === 'core') { + indexStatements.push(`export * from './${library.shortName}';`); + } else { + indexStatements.push(`export * as ${library.shortName.replace(/-/g, '_')} from './${library.shortName}';`); + } + } + + await fs.writeFile(path.join(LIB_ROOT, 'index.ts'), indexStatements.join('\n'), { encoding: 'utf8' }); + + console.log('\t🍺 Success!'); +} + +async function transformPackage( + library: LibraryReference, + config: PackageJson['jsii']['targets'], + destination: string, + allLibraries: readonly LibraryReference[], +) { + await fs.mkdirp(destination); + + await copyOrTransformFiles(library.root, destination, allLibraries); + + await fs.writeFile( + path.join(destination, 'index.ts'), + `export * from './${library.packageJson.types.replace(/(\/index)?(\.d)?\.ts$/, '')}';\n`, + { encoding: 'utf8' }, + ); + + if (library.shortName !== 'core') { + await fs.writeJson( + path.join(destination, '.jsiirc.json'), + { + targets: transformTargets(config, library.packageJson.jsii.targets), + }, + { spaces: 2 }, + ); + + await fs.writeFile( + path.resolve(LIB_ROOT, '..', `${library.shortName}.ts`), + `export * from './lib/${library.shortName}';\n`, + { encoding: 'utf8' }, + ); + } +} + +function transformTargets(monoConfig: PackageJson['jsii']['targets'], targets: PackageJson['jsii']['targets']): PackageJson['jsii']['targets'] { + if (targets == null) { return targets; } + + const result: Record = {}; + for (const [language, config] of Object.entries(targets)) { + switch (language) { + case 'dotnet': + if (monoConfig?.dotnet != null) { + result[language] = { + namespace: (config as any).namespace, + }; + } + break; + case 'java': + if (monoConfig?.java != null) { + result[language] = { + package: (config as any).package, + }; + } + break; + case 'python': + if (monoConfig?.python != null) { + result[language] = { + module: `${monoConfig.python.module}.${(config as any).module.replace(/^aws_cdk\./, '')}`, + }; + } + break; + default: + throw new Error(`Unsupported language for submodule configuration translation: ${language}`); + } + } + + return result; +} + +async function copyOrTransformFiles(from: string, to: string, libraries: readonly LibraryReference[]) { + const promises = (await fs.readdir(from)).map(async name => { + if (shouldIgnoreFile(name)) { return; } + + const source = path.join(from, name); + const destination = path.join(to, name); + + const stat = await fs.stat(source); + if (stat.isDirectory()) { + await fs.mkdirp(destination); + return copyOrTransformFiles(source, destination, libraries); + } + if (name.endsWith('.ts')) { + return await fs.writeFile( + destination, + await rewriteImports(source, to, libraries), + { encoding: 'utf8' }, + ); + } else { + return await fs.copyFile(source, destination); + } + }); + + await Promise.all(promises); +} + +async function rewriteImports(fromFile: string, targetDir: string, libraries: readonly LibraryReference[]): Promise { + const sourceFile = ts.createSourceFile( + fromFile, + await fs.readFile(fromFile, { encoding: 'utf8' }), + ts.ScriptTarget.ES2018, + true, + ts.ScriptKind.TS, + ); + + const transformResult = ts.transform(sourceFile, [importRewriter]); + const transformedSource = transformResult.transformed[0] as ts.SourceFile; + + const printer = ts.createPrinter(); + return printer.printFile(transformedSource); + + function importRewriter(ctx: ts.TransformationContext) { + function visitor(node: ts.Node): ts.Node { + if (ts.isExternalModuleReference(node) && ts.isStringLiteral(node.expression)) { + const newTarget = rewrittenImport(node.expression.text); + if (newTarget != null) { + return addRewrittenNote( + ts.updateExternalModuleReference(node, newTarget), + node.expression, + ); + } + } else if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) { + const newTarget = rewrittenImport(node.moduleSpecifier.text); + if (newTarget != null) { + return addRewrittenNote( + ts.updateImportDeclaration( + node, + node.decorators, + node.modifiers, + node.importClause, + newTarget, + ), + node.moduleSpecifier, + ); + } + } + return ts.visitEachChild(node, visitor, ctx); + } + return visitor; + } + + function addRewrittenNote(node: ts.Node, original: ts.StringLiteral): ts.Node { + return ts.addSyntheticTrailingComment( + node, + ts.SyntaxKind.SingleLineCommentTrivia, + ` Automatically re-written from ${original.getText()}`, + false, // hasTrailingNewline + ); + } + + function rewrittenImport(moduleSpecifier: string): ts.StringLiteral | undefined { + const sourceLibrary = libraries.find( + lib => + moduleSpecifier === lib.packageJson.name || + moduleSpecifier.startsWith(`${lib.packageJson.name}/`) + ); + if (sourceLibrary == null) { return undefined; } + + const importedFile = moduleSpecifier === sourceLibrary.packageJson.name + ? path.join(LIB_ROOT, sourceLibrary.shortName) + : path.join(LIB_ROOT, sourceLibrary.shortName, moduleSpecifier.substr(sourceLibrary.packageJson.name.length + 1)); + return ts.createStringLiteral( + path.relative(targetDir, importedFile), + ); + } +} + +const IGNORED_FILE_NAMES = new Set([ + '.eslintrc.js', + '.gitignore', + '.jest.config.js', + '.jsii', + '.npmignore', + 'node_modules', + 'package.json', + 'test', + 'tsconfig.json', + 'tsconfig.tsbuildinfo', + 'LICENSE', + 'NOTICE', +]); +function shouldIgnoreFile(name: string): boolean { + return IGNORED_FILE_NAMES.has(name); +} + +function sortObject(obj: Record): Record { + const result: Record = {}; + + for (const [key, value] of Object.entries(obj).sort((l, r) => l[0].localeCompare(r[0]))) { + result[key] = value; + } + + return result; +} diff --git a/packages/monocdk-experiment/build.sh b/packages/monocdk-experiment/build.sh deleted file mode 100755 index c386b738c7026..0000000000000 --- a/packages/monocdk-experiment/build.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash -set -euo pipefail -scriptdir=$(cd $(dirname $0) && pwd) - -constructs_version="$(node -p "require('./package.json').devDependencies.constructs")" - -rm -fr dist/js - -echo "collecting all modules..." -outdir=$(node gen.js) - -cd ${outdir} - -echo "installing dependencies for bundling..." -npm install - -echo "compiling..." -${CDK_BUILD_JSII:-jsii} - -echo "packaging..." -${CDK_PACKAGE_JSII_PACMAK:-jsii-pacmak} -tarball=$PWD/dist/js/monocdk-experiment@*.tgz - -echo "verifying package..." -checkdir=$(mktemp -d) - -cd ${checkdir} - -npm init -y -npm install ${tarball} constructs@${constructs_version} -node -e "require('monocdk-experiment')" -unpacked=$(node -p 'path.dirname(require.resolve("monocdk-experiment/package.json"))') - -# saving publishable artifact -rm -fr ${scriptdir}/dist -mv ${outdir}/dist ${scriptdir}/dist - -# so this module will also work as a local dependency (e.g. for modules under @monocdk-experiment/*). -rm -fr ${scriptdir}/staging -mv ${unpacked} ${scriptdir}/staging - -# move .jsii to package root, where our build tools, etc... will look for it. -# this is needed because the generated code is hosted under staging/, but during -# it's creation, it was directly in the package root. -mv ${scriptdir}/staging/.jsii ${scriptdir} - -rm -fr ${outdir} ${checkdir} diff --git a/packages/monocdk-experiment/deps.js b/packages/monocdk-experiment/deps.js deleted file mode 100644 index 61215dd7c84fa..0000000000000 --- a/packages/monocdk-experiment/deps.js +++ /dev/null @@ -1,58 +0,0 @@ -// +------------------------------------------------------------------------------------------------ -// | this script runs during build to verify that this package depends on the entire aws construct -// | library. the script will fail (and update package.json) if this is not true. -// | -const fs = require('fs'); -const path = require('path'); - -const pkg = require('./package.json'); -const pkgDevDeps = pkg.devDependencies || { }; -pkg.devDependencies = pkgDevDeps; - -const root = path.resolve('..', '..', 'packages', '@aws-cdk'); -const modules = fs.readdirSync(root); -let errors = false; - -for (const dir of modules) { - const module = path.resolve(root, dir); - const meta = require(path.join(module, 'package.json')); - if (!meta.jsii) { - continue; - } - - const exists = pkgDevDeps[meta.name]; - - if (meta.deprecated) { - if (exists) { - console.error(`spurious dependency on deprecated: ${meta.name}`); - errors = true; - } - delete pkgDevDeps[meta.name]; - continue; - } - // skip private packages - if (meta.private) { - continue; - } - - if (!exists) { - console.error(`missing dependency: ${meta.name}`); - errors = true; - } - - const requirement = meta.version; - - if (exists && exists !== requirement) { - console.error(`invalid version requirement: expecting '${requirement}', got ${exists}`); - errors = true; - } - - pkgDevDeps[meta.name] = requirement; -} - -fs.writeFileSync(path.join(__dirname, 'package.json'), JSON.stringify(pkg, undefined, 2) + '\n'); - -if (errors) { - console.error('errors found. updated package.json. delete node_modules and rerun "lerna bootstrap"'); - process.exit(1); -} diff --git a/packages/monocdk-experiment/gen.js b/packages/monocdk-experiment/gen.js deleted file mode 100644 index ff187cf84241d..0000000000000 --- a/packages/monocdk-experiment/gen.js +++ /dev/null @@ -1,214 +0,0 @@ -// generates the mono-cdk module by copying @aws-cdk/lib/** to src/ -// and rewriting the interdepedent "import" statements. -const fs = require('fs-extra'); -const path = require('path'); -const glob = require('glob'); -const os = require('os'); - -const exclude_modules = [ - // 'aws-lambda-nodejs' // bundles "parcel" which is unacceptable for now -]; - -const include_non_jsii = [ - // 'assert', - // 'cloudformation-diff', -]; - -const include_dev_deps = [ - d => d === 'aws-sdk', - d => d.startsWith('@types/') -]; - -const exclude_files = [ - 'test', - 'scripts', - 'node_modules', - 'package.json', - 'tsconfig.json', - 'tsconfig.tsbuildinfo', - '.gitignore', - '.jsii', - 'LICENSE', - 'NOTICE' -]; - -async function main() { - const outdir = path.join(await fs.mkdtemp(path.join(os.tmpdir(), 'monocdk-')), 'package'); - - console.error(`generating monocdk at ${outdir}`); - const reexports = []; - - await fs.remove(outdir); - await fs.mkdir(outdir); - - const monocdkroot = __dirname; - const root = path.resolve(__dirname, '..', '@aws-cdk'); - const modules = await fs.readdir(root); - const manifest = await fs.readJson(path.join(monocdkroot, 'package.json')); - - // Adjust index location for initial compilation - manifest.main = manifest.main.replace(/^staging\//, ''); - manifest.types = manifest.types.replace(/^staging\//, ''); - - const nodeTypes = manifest.devDependencies['@types/node']; - if (!nodeTypes) { - throw new Error(`@types/node must be defined in devDependencies`); - } - const devDeps = manifest.devDependencies = { - '@types/node': nodeTypes, - 'constructs': manifest.devDependencies['constructs'] - }; - - if (manifest.dependencies) { - throw new Error(`package.json should not contain "dependencies"`); - } - - if (manifest.bundledDependencies) { - throw new Error(`packaghe.json should not contain "bundledDependencies"`); - } - - const pkgDeps = manifest.dependencies = { }; - const pkgBundled = manifest.bundledDependencies = [ ]; - - for (const dir of modules) { - if (exclude_modules.includes(dir)) { - console.error(`skipping module ${dir}`); - continue; - } - - const moduledir = path.resolve(root, dir); - - const meta = JSON.parse(await fs.readFile(path.join(moduledir, 'package.json'), 'utf-8')); - - if (meta.deprecated) { - console.error(`skipping deprecated ${meta.name}`); - continue; - } - - if (!meta.jsii && !include_non_jsii.includes(dir)) { - console.error(`skipping non-jsii module ${meta.name}`); - continue; - } - - const basename = path.basename(moduledir); - const files = await fs.readdir(moduledir); - const targetdir = path.join(outdir, basename); - for (const file of files) { - const source = path.join(moduledir, file); - - // skip excluded directories - if (exclude_files.includes(file)) { - continue; - } - - const target = path.join(targetdir, file); - await fs.copy(source, target); - } - - await fs.writeFile(path.join(targetdir, 'index.ts'), `export * from './lib'\n`); - - // export "core" types at the root. all the rest under a namespace. - if (basename === 'core') { - reexports.push(`export * from './core/lib';`); - } else { - const namespace = basename.replace(/-/g, '_'); - reexports.push(`export * as ${namespace} from './${basename}/lib';`); - } - - // add @types/ devDependencies from module - const shouldIncludeDevDep = d => include_dev_deps.find(pred => pred(d)); - - for (const [ devDep, devDepVersion ] of Object.entries(meta.devDependencies || {})) { - if (!shouldIncludeDevDep(devDep)) { - continue; - } - - const existingVer = devDeps[devDep]; - if (existingVer && existingVer !== devDepVersion) { - throw new Error(`mismatching versions for devDependency ${devDep}. ${meta.name} requires ${devDepVersion} but we already have ${existingVer}`); - } - - if (!existingVer) { - console.error(`adding dev dep ${devDep}${devDepVersion}`); - devDeps[devDep] = devDepVersion; - } - } - - // add bundled deps - const bundled = [ ...meta.bundleDependencies || [], ...meta.bundledDependencies || [] ]; - for (const d of bundled) { - const ver = meta.dependencies[d]; - - console.error(`adding bundled dep ${d} with version ${ver}`); - if (!pkgBundled.includes(d)) { - pkgBundled.push(d); - } - - if (!ver) { - throw new Error(`cannot determine version for bundled dep ${d} of module ${meta.name}`); - } - const existingVer = pkgDeps[d]; - if (!existingVer) { - pkgDeps[d] = ver; - } else { - if (existingVer !== ver) { - throw new Error(`version mismatch for bundled dep ${d}: ${meta.name} requires version ${ver} but we already have version ${existingVer}`); - } - } - } - } - - await fs.writeFile(path.join(outdir, 'index.ts'), reexports.join('\n')); - - console.error(`rewriting "import" statements...`); - const sourceFiles = await findSources(outdir); - for (const source of sourceFiles) { - await rewriteImports(outdir, source); - } - - // copy .npmignore, license stuff, readme, ... - const files = [ '.npmignore', 'README.md', 'LICENSE', 'NOTICE' ]; - for (const file of files) { - await fs.copy(path.join(monocdkroot, file), path.join(outdir, file)); - } - - console.error('writing package.json'); - await fs.writeJson(path.join(outdir, 'package.json'), manifest, { spaces: 2 }); - - console.log(outdir); -} - -async function findSources(srcdir) { - return new Promise((ok, ko) => glob('**/*.ts', { cwd: srcdir }, (err, results) => { - if (err) { return ko(err); } - return ok(results); - })); -} - -async function rewriteImports(srcdir, relativeSource) { - const absoluteSource = path.join(srcdir, relativeSource); - const source = await fs.readFile(absoluteSource, 'utf-8'); - - const match = /from ['"]@aws-cdk\/(.+)['"]/.exec(source); - if (!match) { - return; - } - - const left = source.substring(0, match.index); - const right = source.substring(match.index + match[0].length); - - const submodule = match[1]; - const moduleDir = path.join(srcdir, submodule); - const rel = path.relative(path.dirname(absoluteSource), moduleDir) - - const newSource = `${left}from '${rel}'${right}`; - await fs.writeFile(absoluteSource, newSource); - - // call recursively until exhausted - await rewriteImports(srcdir, relativeSource); -} - -main().catch(e => { - console.error(e); - process.exit(1); -}); diff --git a/packages/monocdk-experiment/package.json b/packages/monocdk-experiment/package.json index cbadf0077116f..e8bec8325baea 100644 --- a/packages/monocdk-experiment/package.json +++ b/packages/monocdk-experiment/package.json @@ -2,24 +2,62 @@ "name": "monocdk-experiment", "version": "0.0.0", "description": "An experiment to bundle the entire CDK into a single module", - "main": "staging/index.js", - "types": "staging/index.d.ts", + "main": "lib/index.js", + "types": "lib/index.d.ts", "repository": { "type": "git", - "url": "https://github.com/aws/aws-cdk.git" + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/monocdk-experiment" }, "stability": "experimental", "maturity": "developer-preview", "scripts": { - "build": "node deps.js && ./build.sh", + "gen": "npx ts-node build-tools/gen.ts", + "build": "cdk-build", + "lint": "cdk-lint", "test": "echo done", - "package": "echo done", + "package": "cdk-package", + "pkglint": "pkglint -f", "build+test": "npm run build && npm test", - "build+test+package": "npm run build+test && npm run package" + "build+test+package": "npm run build+test && npm run package", + "watch": "cdk-watch", + "compat": "cdk-compat" + }, + "awslint": { + "exclude": ["*:*"] + }, + "cdk-build": { + "eslint": { + "disable": true + }, + "tslint": { + "disable": true + }, + "pre": [ + "npm run gen" + ] + }, + "pkglint": { + "exclude": [ + "package-info/maturity", + "jsii/java", + "jsii/python" + ] }, "jsii": { + "excludeTypescript": [ + "build-tools/*" + ], "outdir": "dist", "targets": { + "dotnet": { + "namespace": "Amazon.CDK.MonoCDK.Experiment", + "packageId": "Amazon.CDK.MonoCDK.Experiment", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png", + "versionSuffix": "-devpreview", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk" + }, "java": { "package": "software.amazon.awscdk.monocdkexperiment", "maven": { @@ -28,13 +66,9 @@ "versionSuffix": ".DEVPREVIEW" } }, - "dotnet": { - "namespace": "Amazon.CDK.MonoCDK.Experiment", - "packageId": "Amazon.CDK.MonoCDK.Experiment", - "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png", - "versionSuffix": "-devpreview", - "signAssembly": true, - "assemblyOriginatorKeyFile": "../../key.snk" + "python": { + "distName": "monocdk.experiment", + "module": "monocdk_experiment" } } }, @@ -44,9 +78,22 @@ "organization": true }, "license": "Apache-2.0", - "devDependencies": { - "jsii": "^1.5.0", + "bundledDependencies": [ + "case", + "jsonschema", + "minimatch", + "semver", + "yaml" + ], + "dependencies": { + "case": "1.6.3", "constructs": "^3.0.2", + "jsonschema": "^1.2.5", + "minimatch": "^3.0.4", + "semver": "^7.2.2", + "yaml": "1.10.0" + }, + "devDependencies": { "@aws-cdk/alexa-ask": "0.0.0", "@aws-cdk/app-delivery": "0.0.0", "@aws-cdk/assets": "0.0.0", @@ -69,7 +116,10 @@ "@aws-cdk/aws-backup": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", "@aws-cdk/aws-budgets": "0.0.0", + "@aws-cdk/aws-cassandra": "0.0.0", + "@aws-cdk/aws-ce": "0.0.0", "@aws-cdk/aws-certificatemanager": "0.0.0", + "@aws-cdk/aws-chatbot": "0.0.0", "@aws-cdk/aws-cloud9": "0.0.0", "@aws-cdk/aws-cloudformation": "0.0.0", "@aws-cdk/aws-cloudfront": "0.0.0", @@ -79,14 +129,17 @@ "@aws-cdk/aws-codebuild": "0.0.0", "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-codedeploy": "0.0.0", + "@aws-cdk/aws-codeguruprofiler": "0.0.0", "@aws-cdk/aws-codepipeline": "0.0.0", "@aws-cdk/aws-codepipeline-actions": "0.0.0", "@aws-cdk/aws-codestar": "0.0.0", + "@aws-cdk/aws-codestarconnections": "0.0.0", "@aws-cdk/aws-codestarnotifications": "0.0.0", "@aws-cdk/aws-cognito": "0.0.0", "@aws-cdk/aws-config": "0.0.0", "@aws-cdk/aws-datapipeline": "0.0.0", "@aws-cdk/aws-dax": "0.0.0", + "@aws-cdk/aws-detective": "0.0.0", "@aws-cdk/aws-directoryservice": "0.0.0", "@aws-cdk/aws-dlm": "0.0.0", "@aws-cdk/aws-dms": "0.0.0", @@ -104,6 +157,7 @@ "@aws-cdk/aws-elasticbeanstalk": "0.0.0", "@aws-cdk/aws-elasticloadbalancing": "0.0.0", "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", + "@aws-cdk/aws-elasticloadbalancingv2-actions": "0.0.0", "@aws-cdk/aws-elasticloadbalancingv2-targets": "0.0.0", "@aws-cdk/aws-elasticsearch": "0.0.0", "@aws-cdk/aws-emr": "0.0.0", @@ -113,10 +167,12 @@ "@aws-cdk/aws-fms": "0.0.0", "@aws-cdk/aws-fsx": "0.0.0", "@aws-cdk/aws-gamelift": "0.0.0", + "@aws-cdk/aws-globalaccelerator": "0.0.0", "@aws-cdk/aws-glue": "0.0.0", "@aws-cdk/aws-greengrass": "0.0.0", "@aws-cdk/aws-guardduty": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-imagebuilder": "0.0.0", "@aws-cdk/aws-inspector": "0.0.0", "@aws-cdk/aws-iot": "0.0.0", "@aws-cdk/aws-iot1click": "0.0.0", @@ -134,12 +190,14 @@ "@aws-cdk/aws-lambda-nodejs": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-logs-destinations": "0.0.0", + "@aws-cdk/aws-macie": "0.0.0", "@aws-cdk/aws-managedblockchain": "0.0.0", "@aws-cdk/aws-mediaconvert": "0.0.0", "@aws-cdk/aws-medialive": "0.0.0", "@aws-cdk/aws-mediastore": "0.0.0", "@aws-cdk/aws-msk": "0.0.0", "@aws-cdk/aws-neptune": "0.0.0", + "@aws-cdk/aws-networkmanager": "0.0.0", "@aws-cdk/aws-opsworks": "0.0.0", "@aws-cdk/aws-opsworkscm": "0.0.0", "@aws-cdk/aws-pinpoint": "0.0.0", @@ -148,6 +206,7 @@ "@aws-cdk/aws-ram": "0.0.0", "@aws-cdk/aws-rds": "0.0.0", "@aws-cdk/aws-redshift": "0.0.0", + "@aws-cdk/aws-resourcegroups": "0.0.0", "@aws-cdk/aws-robomaker": "0.0.0", "@aws-cdk/aws-route53": "0.0.0", "@aws-cdk/aws-route53-patterns": "0.0.0", @@ -172,29 +231,26 @@ "@aws-cdk/aws-ssm": "0.0.0", "@aws-cdk/aws-stepfunctions": "0.0.0", "@aws-cdk/aws-stepfunctions-tasks": "0.0.0", + "@aws-cdk/aws-synthetics": "0.0.0", "@aws-cdk/aws-transfer": "0.0.0", "@aws-cdk/aws-waf": "0.0.0", "@aws-cdk/aws-wafregional": "0.0.0", "@aws-cdk/aws-wafv2": "0.0.0", "@aws-cdk/aws-workspaces": "0.0.0", "@aws-cdk/cdk-assets-schema": "0.0.0", + "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/cloudformation-include": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/custom-resources": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/region-info": "0.0.0", - "@types/node": "^10.17.21", - "@aws-cdk/cloud-assembly-schema": "0.0.0", - "@aws-cdk/aws-chatbot": "0.0.0", - "@aws-cdk/aws-codestarconnections": "0.0.0", - "@aws-cdk/aws-cassandra": "0.0.0", - "@aws-cdk/aws-codeguruprofiler": "0.0.0", - "@aws-cdk/aws-networkmanager": "0.0.0", - "@aws-cdk/aws-resourcegroups": "0.0.0", - "@aws-cdk/aws-detective": "0.0.0", - "@aws-cdk/aws-ce": "0.0.0", - "@aws-cdk/aws-synthetics": "0.0.0", - "@aws-cdk/aws-elasticloadbalancingv2-actions": "0.0.0" + "@types/fs-extra": "^8.1.1", + "@types/node": "^10.17.24", + "cdk-build-tools": "0.0.0", + "fs-extra": "^9.0.0", + "pkglint": "0.0.0", + "ts-node": "^8.10.2", + "typescript": "~3.8.3" }, "peerDependencies": { "constructs": "^3.0.2" @@ -202,5 +258,12 @@ "homepage": "https://github.com/aws/aws-cdk", "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "keywords": [ + "aws", + "cdk" + ], + "awscdkio": { + "announce": false } } diff --git a/tools/cdk-build-tools/package.json b/tools/cdk-build-tools/package.json index 0ddee7d957395..42f531f06c53c 100644 --- a/tools/cdk-build-tools/package.json +++ b/tools/cdk-build-tools/package.json @@ -39,7 +39,7 @@ "pkglint": "0.0.0" }, "dependencies": { - "@typescript-eslint/eslint-plugin": "^3.0.0", + "@typescript-eslint/eslint-plugin": "^3.1.0", "@typescript-eslint/parser": "^2.19.2", "awslint": "0.0.0", "colors": "^1.4.0", diff --git a/tools/cfn2ts/lib/codegen.ts b/tools/cfn2ts/lib/codegen.ts index 7efe4275f0dc9..8bd831d9de73e 100644 --- a/tools/cfn2ts/lib/codegen.ts +++ b/tools/cfn2ts/lib/codegen.ts @@ -266,10 +266,19 @@ export default class CodeGenerator { this.code.line('ret.node.addDependency(depResource);'); this.code.closeBlock(); + // handle Condition + this.code.line('// handle Condition'); + this.code.openBlock('if (resourceAttributes.Condition)'); + this.code.line('const condition = options.finder.findCondition(resourceAttributes.Condition);'); + this.code.openBlock('if (!condition)'); + this.code.line("throw new Error(`Resource '${id}' uses Condition '${resourceAttributes.Condition}' that doesn't exist`);"); + this.code.closeBlock(); + this.code.line('cfnOptions.condition = condition;'); + this.code.closeBlock(); + // ToDo handle: - // 1. Condition - // 2. CreationPolicy - // 3. UpdatePolicy + // 1. CreationPolicy + // 2. UpdatePolicy this.code.line('return ret;'); this.code.closeBlock(); diff --git a/yarn.lock b/yarn.lock index c6114d649a7c0..3fa3bff67a241 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1439,6 +1439,13 @@ dependencies: "@types/node" "*" +"@types/fs-extra@^8.1.1": + version "8.1.1" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.1.tgz#1e49f22d09aa46e19b51c0b013cb63d0d923a068" + integrity sha512-TcUlBem321DFQzBNuz8p0CLLKp0VvF/XH9E4KHNmgwyp4E3AfgI5cjiIVZWlbfThBop2qxFIh4+LeY6hVWWZ2w== + dependencies: + "@types/node" "*" + "@types/glob@*", "@types/glob@^7.1.1": version "7.1.1" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" @@ -1500,10 +1507,10 @@ dependencies: jszip "*" -"@types/lodash@^4.14.152": - version "4.14.152" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.152.tgz#7e7679250adce14e749304cdb570969f77ec997c" - integrity sha512-Vwf9YF2x1GE3WNeUMjT5bTHa2DqgUo87ocdgTScupY2JclZ5Nn7W2RLM/N0+oreexUk8uaVugR81NnTY/jNNXg== +"@types/lodash@^4.14.153": + version "4.14.153" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.153.tgz#5cb7dded0649f1df97938ac5ffc4f134e9e9df98" + integrity sha512-lYniGRiRfZf2gGAR9cfRC3Pi5+Q1ziJCKqPmjZocigrSJUVPWf7st1BtSJ8JOeK0FLXVndQ1IjUjTco9CXGo/Q== "@types/md5@^2.2.0": version "2.2.0" @@ -1534,7 +1541,7 @@ resolved "https://registry.yarnpkg.com/@types/mockery/-/mockery-1.4.29.tgz#9ba22df37f07e3780fff8531d1a38e633f9457a5" integrity sha1-m6It838H43gP/4Ux0aOOYz+UV6U= -"@types/node@*", "@types/node@>= 8", "@types/node@^13.9.1": +"@types/node@*", "@types/node@>= 8": version "13.13.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.0.tgz#30d2d09f623fe32cde9cb582c7a6eda2788ce4a8" integrity sha512-WE4IOAC6r/yBZss1oQGM5zs2D7RuKR6Q+w+X2SouPofnWn+LbCqClRyhO3ZE7Ix8nmFgo/oVuuE01cJT2XB13A== @@ -1544,6 +1551,16 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.21.tgz#c00e9603399126925806bed2d9a1e37da506965e" integrity sha512-PQKsydPxYxF1DsAFWmunaxd3sOi3iMt6Zmx/tgaagHYmwJ/9cRH91hQkeJZaUGWbvn0K5HlSVEXkn5U/llWPpQ== +"@types/node@^10.17.24": + version "10.17.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.24.tgz#c57511e3a19c4b5e9692bb2995c40a3a52167944" + integrity sha512-5SCfvCxV74kzR3uWgTYiGxrd69TbT1I6+cMx1A5kEly/IVveJBimtAMlXiEyVFn5DvUFewQWxOOiJhlxeQwxgA== + +"@types/node@^13.9.1": + version "13.13.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.9.tgz#79df4ae965fb76d31943b54a6419599307a21394" + integrity sha512-EPZBIGed5gNnfWCiwEIwTE2Jdg4813odnG8iNPMQGrqVxrI+wL68SPtPeCX+ZxGBaA6pKAVc6jaKgP/Q0QzfdQ== + "@types/nodeunit@^0.0.31": version "0.0.31" resolved "https://registry.yarnpkg.com/@types/nodeunit/-/nodeunit-0.0.31.tgz#67eb52ad22326c7d1d9febe99d553f33b166126d" @@ -1578,10 +1595,10 @@ dependencies: "@types/node" "*" -"@types/sinon@^9.0.3": - version "9.0.3" - resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.3.tgz#c803f2ebf96db44230ce4e632235c279830edd45" - integrity sha512-NWVG++603tEDwmz5k0DwFR1hqP3iBmq5GYi6d+0KCQMQsfDEULF1D7xqZ+iXRJHeGwLVhM+Rv73uzIYuIUVlJQ== +"@types/sinon@^9.0.4": + version "9.0.4" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.4.tgz#e934f904606632287a6e7f7ab0ce3f08a0dad4b1" + integrity sha512-sJmb32asJZY6Z2u09bl0G2wglSxDlROlAejCjsnor+LzBMz17gu8IU7vKC/vWDnv9zEq2wqADHVXFjf4eE8Gdw== dependencies: "@types/sinonjs__fake-timers" "*" @@ -1641,12 +1658,12 @@ resolved "https://registry.yarnpkg.com/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.3.tgz#38fb31d82ed07dea87df6bd565721d11979fd761" integrity sha512-mhdQq10tYpiNncMkg1vovCud5jQm+rWeRVz6fxjCJlY6uhDlAn9GnMSmBa2DQwqPf/jS5YR0K/xChDEh1jdOQg== -"@typescript-eslint/eslint-plugin@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.0.0.tgz#02f8ec6b5ce814bda80dfc22463f108bed1f699b" - integrity sha512-lcZ0M6jD4cqGccYOERKdMtg+VWpoq3NSnWVxpc/AwAy0zhkUYVioOUZmfNqiNH8/eBNGhCn6HXd6mKIGRgNc1Q== +"@typescript-eslint/eslint-plugin@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.1.0.tgz#4ac00ecca3bbea740c577f1843bc54fa69c3def2" + integrity sha512-D52KwdgkjYc+fmTZKW7CZpH5ZBJREJKZXRrveMiRCmlzZ+Rw9wRVJ1JAmHQ9b/+Ehy1ZeaylofDB9wwXUt83wg== dependencies: - "@typescript-eslint/experimental-utils" "3.0.0" + "@typescript-eslint/experimental-utils" "3.1.0" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" semver "^7.3.2" @@ -1662,13 +1679,13 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/experimental-utils@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.0.0.tgz#1ddf53eeb61ac8eaa9a77072722790ac4f641c03" - integrity sha512-BN0vmr9N79M9s2ctITtChRuP1+Dls0x/wlg0RXW1yQ7WJKPurg6X3Xirv61J2sjPif4F8SLsFMs5Nzte0WYoTQ== +"@typescript-eslint/experimental-utils@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.1.0.tgz#2d5dba7c2ac2a3da3bfa3f461ff64de38587a872" + integrity sha512-Zf8JVC2K1svqPIk1CB/ehCiWPaERJBBokbMfNTNRczCbQSlQXaXtO/7OfYz9wZaecNvdSvVADt6/XQuIxhC79w== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "3.0.0" + "@typescript-eslint/typescript-estree" "3.1.0" eslint-scope "^5.0.0" eslint-utils "^2.0.0" @@ -1695,10 +1712,10 @@ semver "^6.3.0" tsutils "^3.17.1" -"@typescript-eslint/typescript-estree@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.0.0.tgz#fa40e1b76ccff880130be054d9c398e96004bf42" - integrity sha512-nevQvHyNghsfLrrByzVIH4ZG3NROgJ8LZlfh3ddwPPH4CH7W4GAiSx5qu+xHuX5pWsq6q/eqMc1io840ZhAnUg== +"@typescript-eslint/typescript-estree@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.1.0.tgz#eaff52d31e615e05b894f8b9d2c3d8af152a5dd2" + integrity sha512-+4nfYauqeQvK55PgFrmBWFVYb6IskLyOosYEmhH3mSVhfBp9AIJnjExdgDmKWoOBHRcPM8Ihfm2BFpZf0euUZQ== dependencies: debug "^4.1.1" eslint-visitor-keys "^1.1.0" @@ -8655,7 +8672,7 @@ source-map-resolve@^0.5.0: source-map-url "^0.4.0" urix "^0.1.0" -source-map-support@^0.5.10, source-map-support@^0.5.19, source-map-support@^0.5.6: +source-map-support@^0.5.10, source-map-support@^0.5.17, source-map-support@^0.5.19, source-map-support@^0.5.6: version "0.5.19" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== @@ -9446,15 +9463,15 @@ ts-mock-imports@^1.2.6, ts-mock-imports@^1.3.0: resolved "https://registry.yarnpkg.com/ts-mock-imports/-/ts-mock-imports-1.3.0.tgz#ed9b743349f3c27346afe5b7454ffd2bcaa2302d" integrity sha512-cCrVcRYsp84eDvPict0ZZD/D7ppQ0/JSx4ve6aEU8DjlsaWRJWV6ADMovp2sCuh6pZcduLFoIYhKTDU2LARo7Q== -ts-node@^8.0.2: - version "8.8.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.8.2.tgz#0b39e690bee39ea5111513a9d2bcdc0bc121755f" - integrity sha512-duVj6BpSpUpD/oM4MfhO98ozgkp3Gt9qIp3jGxwU2DFvl/3IRaEAvbLa8G60uS7C77457e/m5TMowjedeRxI1Q== +ts-node@^8.0.2, ts-node@^8.10.2: + version "8.10.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d" + integrity sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA== dependencies: arg "^4.1.0" diff "^4.0.1" make-error "^1.1.1" - source-map-support "^0.5.6" + source-map-support "^0.5.17" yn "3.1.1" tsame@^2.0.1: