diff --git a/CHANGELOG.md b/CHANGELOG.md index 0386d2dbfb9a6..d060fb6f61a28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,50 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.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 ddd76ca329bbf..ad4328ebde45a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -77,7 +77,8 @@ you need to have the following SDKs and tools locally: - We recommend using a version in [Active LTS](https://nodejs.org/en/about/releases/) - ⚠️ versions `13.0.0` to `13.6.0` are not supported due to compatibility issues with our dependencies. - [Yarn >= 1.19.1](https://yarnpkg.com/lang/en/docs/install) -- [Java OpenJDK 8](http://openjdk.java.net/install/) +- [Java OpenJDK 8](https://docs.aws.amazon.com/corretto/latest/corretto-8-ug/downloads-list.html) +- [Apache Maven](http://maven.apache.org/install.html) - [.NET Core SDK 3.1](https://www.microsoft.com/net/download) - [Python 3.6.5](https://www.python.org/downloads/release/python-365/) - [Ruby 2.5.1](https://www.ruby-lang.org/en/news/2018/03/28/ruby-2-5-1-released/) @@ -91,6 +92,13 @@ $ yarn install $ yarn build ``` +If you get compiler errors when building, a common cause is globally installed tools like tslint and typescript. Try uninstalling them. + +``` +npm uninstall -g tslint +npm uninstall -g typescript +``` + Alternatively, the [Full Docker build](#full-docker-build) workflow can be used so that you don't have to worry about installing all those tools on your local machine and instead only depend on having a working Docker install. @@ -197,7 +205,7 @@ Examples: ### Step 4: Commit -Create a commit with the proposed change changes: +Create a commit with the proposed changes: * Commit title and message (and PR title and description) must adhere to [conventionalcommits](https://www.conventionalcommits.org). * The title must begin with `feat(module): title`, `fix(module): title`, `refactor(module): title` or diff --git a/allowed-breaking-changes.txt b/allowed-breaking-changes.txt index 94d3c3c05f46a..8b137891791fe 100644 --- a/allowed-breaking-changes.txt +++ b/allowed-breaking-changes.txt @@ -1,58 +1 @@ -incompatible-argument:@aws-cdk/aws-ecs.Ec2TaskDefinition. -incompatible-argument:@aws-cdk/aws-ecs.Ec2TaskDefinition.addVolume -incompatible-argument:@aws-cdk/aws-ecs.FargateTaskDefinition. -incompatible-argument:@aws-cdk/aws-ecs.FargateTaskDefinition.addVolume -incompatible-argument:@aws-cdk/aws-ecs.TaskDefinition. -incompatible-argument:@aws-cdk/aws-ecs.TaskDefinition.addVolume -change-return-type:@aws-cdk/core.Fn.getAtt -new-argument:@aws-cdk/aws-iam.ManagedPolicy. -new-argument:@aws-cdk/aws-iam.ManagedPolicy. -removed:@aws-cdk/aws-apigateway.AwsIntegration.props -removed:@aws-cdk/aws-apigateway.HttpIntegration.props -removed:@aws-cdk/aws-apigateway.Integration.props -removed:@aws-cdk/aws-apigateway.LambdaIntegration.props -removed:@aws-cdk/aws-apigateway.MockIntegration.props -removed:@aws-cdk/aws-ecs-patterns.ScheduledEc2TaskDefinitionOptions.schedule -removed:@aws-cdk/aws-ecs-patterns.ScheduledEc2TaskDefinitionOptions.cluster -removed:@aws-cdk/aws-ecs-patterns.ScheduledEc2TaskDefinitionOptions.desiredTaskCount -removed:@aws-cdk/aws-ecs-patterns.ScheduledEc2TaskDefinitionOptions.vpc -removed:@aws-cdk/aws-ecs-patterns.ScheduledFargateTaskDefinitionOptions.schedule -removed:@aws-cdk/aws-ecs-patterns.ScheduledFargateTaskDefinitionOptions.cluster -removed:@aws-cdk/aws-ecs-patterns.ScheduledFargateTaskDefinitionOptions.desiredTaskCount -removed:@aws-cdk/aws-ecs-patterns.ScheduledFargateTaskDefinitionOptions.vpc -incompatible-argument:@aws-cdk/aws-lambda.Function. -incompatible-argument:@aws-cdk/aws-lambda.SingletonFunction. -incompatible-argument:@aws-cdk/aws-lambda.Function.addEnvironment -changed-type:@aws-cdk/aws-dynamodb.Table.tableStreamArn -incompatible-argument:@aws-cdk/aws-apigateway.LambdaRestApi.addModel -incompatible-argument:@aws-cdk/aws-apigateway.Model. -incompatible-argument:@aws-cdk/aws-apigateway.RestApi.addModel -incompatible-argument:@aws-cdk/aws-apigateway.ProxyResource.addProxy -incompatible-argument:@aws-cdk/aws-apigateway.Resource.addProxy -incompatible-argument:@aws-cdk/aws-apigateway.ResourceBase.addProxy -incompatible-argument:@aws-cdk/aws-apigateway.IResource.addProxy -incompatible-argument:@aws-cdk/aws-apigateway.RequestAuthorizer. -incompatible-argument:@aws-cdk/aws-servicediscovery.Service.fromServiceAttributes -removed:@aws-cdk/core.ConstructNode.addReference -removed:@aws-cdk/core.ConstructNode.references -removed:@aws-cdk/core.OutgoingReference -change-return-type:@aws-cdk/aws-lambda-destinations.EventBridgeDestination.bind -change-return-type:@aws-cdk/aws-lambda-destinations.LambdaDestination.bind -change-return-type:@aws-cdk/aws-lambda-destinations.SnsDestination.bind -change-return-type:@aws-cdk/aws-lambda-destinations.SqsDestination.bind -removed:@aws-cdk/cdk-assets-schema.DockerImageDestination.imageUri -incompatible-argument:@aws-cdk/aws-iam.FederatedPrincipal. -incompatible-argument:@aws-cdk/aws-iam.PolicyStatement.addCondition -incompatible-argument:@aws-cdk/aws-iam.PolicyStatement.addConditions -incompatible-argument:@aws-cdk/aws-iam.PolicyStatement.addFederatedPrincipal -incompatible-argument:@aws-cdk/aws-iam.PrincipalPolicyFragment. -changed-type:@aws-cdk/aws-iam.FederatedPrincipal.conditions -changed-type:@aws-cdk/aws-iam.PrincipalPolicyFragment.conditions -changed-type:@aws-cdk/aws-iam.PrincipalWithConditions.conditions -removed:@aws-cdk/cdk-assets-schema.Placeholders -# Following two are because we're turning: properties: {string=>any} into a union of typed interfaces -# Needs to be removed after next release. -incompatible-argument:@aws-cdk/cloud-assembly-schema.Manifest.save -change-return-type:@aws-cdk/cloud-assembly-schema.Manifest.load -removed:@aws-cdk/core.DefaultStackSynthesizer.DEFAULT_DEPLOY_ACTION_ROLE_ARN -removed:@aws-cdk/core.DefaultStackSynthesizerProps.deployActionRoleArn + diff --git a/lerna.json b/lerna.json index baa3940d032fb..b533a6ac4d33c 100644 --- a/lerna.json +++ b/lerna.json @@ -10,5 +10,5 @@ "tools/*" ], "rejectCycles": "true", - "version": "1.41.0" + "version": "1.42.0" } 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/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-backup/lib/resource.ts b/packages/@aws-cdk/aws-backup/lib/resource.ts index 5f3073642c05b..c0cd0fd2b8878 100644 --- a/packages/@aws-cdk/aws-backup/lib/resource.ts +++ b/packages/@aws-cdk/aws-backup/lib/resource.ts @@ -64,14 +64,14 @@ export class BackupResource { /** * A DynamoDB table */ - public static fromDynamoDbTable(table: dynamodb.Table) { + public static fromDynamoDbTable(table: dynamodb.ITable) { return BackupResource.fromArn(table.tableArn); } /** * An EC2 instance */ - public static fromEc2Instance(instance: ec2.Instance) { + public static fromEc2Instance(instance: ec2.IInstance) { return BackupResource.fromArn(Stack.of(instance).formatArn({ service: 'ec2', resource: 'instance', @@ -82,7 +82,7 @@ export class BackupResource { /** * An EFS file system */ - public static fromEfsFileSystem(fileSystem: efs.FileSystem) { + public static fromEfsFileSystem(fileSystem: efs.IFileSystem) { return BackupResource.fromArn(Stack.of(fileSystem).formatArn({ service: 'elasticfilesystem', resource: 'file-system', @@ -93,7 +93,7 @@ export class BackupResource { /** * A RDS database instance */ - public static fromRdsDatabaseInstance(instance: rds.DatabaseInstance) { + public static fromRdsDatabaseInstance(instance: rds.IDatabaseInstance) { return BackupResource.fromArn(instance.instanceArn); } diff --git a/packages/@aws-cdk/aws-backup/test/selection.test.ts b/packages/@aws-cdk/aws-backup/test/selection.test.ts index 4d8e7652a6925..75d1f6e6eade8 100644 --- a/packages/@aws-cdk/aws-backup/test/selection.test.ts +++ b/packages/@aws-cdk/aws-backup/test/selection.test.ts @@ -290,3 +290,44 @@ test('fromEc2Instance', () => { }, }); }); + +test('fromDynamoDbTable', () => { + // GIVEN + const newTable = new dynamodb.Table(stack, 'New', { + partitionKey: { + name: 'id', + type: dynamodb.AttributeType.STRING, + }, + }); + const existingTable = dynamodb.Table.fromTableArn(stack, 'Existing', 'arn:aws:dynamodb:eu-west-1:123456789012:table/existing'); + + // WHEN + plan.addSelection('Selection', { + resources: [ + BackupResource.fromDynamoDbTable(newTable), + BackupResource.fromDynamoDbTable(existingTable), + ], + }); + + // THEN + expect(stack).toHaveResource('AWS::Backup::BackupSelection', { + BackupSelection: { + IamRoleArn: { + 'Fn::GetAtt': [ + 'PlanSelectionRole6D10F4B7', + 'Arn', + ], + }, + Resources: [ + { + 'Fn::GetAtt': [ + 'New8A81B073', + 'Arn', + ], + }, + 'arn:aws:dynamodb:eu-west-1:123456789012:table/existing', + ], + SelectionName: 'Selection', + }, + }); +}); diff --git a/packages/@aws-cdk/aws-cloudtrail/README.md b/packages/@aws-cdk/aws-cloudtrail/README.md index 313f425125fb3..a1619d4bf48fe 100644 --- a/packages/@aws-cdk/aws-cloudtrail/README.md +++ b/packages/@aws-cdk/aws-cloudtrail/README.md @@ -66,13 +66,12 @@ const trail = new cloudtrail.Trail(this, 'MyAmazingCloudTrail'); // 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/"]); +trail.logAllS3DataEvents(); -// Adds an event selector to the bucket foo, with a specific configuration -trail.addS3EventSelector(["arn:aws:s3:::foo/"], { - includeManagementEvents: false, - readWriteType: ReadWriteType.ALL, -}); +// Adds an event selector to the bucket foo +trail.addS3EventSelector([{ + bucket: fooBucket // 'fooBucket' is of type s3.IBucket +}]); ``` For using CloudTrail event selector to log events about Lambda @@ -90,7 +89,7 @@ const lambdaFunction = new lambda.Function(stack, 'AnAmazingFunction', { }); // Add an event selector to log data events for all functions in the account. -trail.addLambdaEventSelector(["arn:aws:lambda"]); +trail.logAllLambdaDataEvents(); // Add an event selector to log data events for the provided Lambda functions. trail.addLambdaEventSelector([lambdaFunction.functionArn]); diff --git a/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts b/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts index 471587a0696ef..9c38e6ca06814 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'; @@ -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; } /** @@ -252,7 +254,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 +318,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 +345,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 +397,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..7137a1ea4a7f0 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 }); + cloudTrail.logAllS3DataEvents({ 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'); - - 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,38 @@ describe('cloudtrail', () => { }); const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); - cloudTrail.addLambdaEventSelector([lambdaFunction.functionArn]); - - expect(stack).toHaveResource('AWS::CloudTrail::Trail'); - expect(stack).toHaveResource('AWS::Lambda::Function'); - expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); + cloudTrail.addLambdaEventSelector([lambdaFunction]); - 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']); - - expect(stack).toHaveResource('AWS::CloudTrail::Trail'); - expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); - expect(stack).not.toHaveResource('AWS::IAM::Role'); + cloudTrail.logAllLambdaDataEvents(); - 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']); + expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + EventSelectors: [ + { + DataResources: [{ + Type: 'AWS::Lambda::Function', + Values: [ 'arn:aws:lambda' ], + }], + }, + ], + }); }); }); }); 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-codeguruprofiler/README.md b/packages/@aws-cdk/aws-codeguruprofiler/README.md index 23b5ff77af24f..5fcb3137d296b 100644 --- a/packages/@aws-cdk/aws-codeguruprofiler/README.md +++ b/packages/@aws-cdk/aws-codeguruprofiler/README.md @@ -9,8 +9,26 @@ --- -This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. +Amazon CodeGuru Profiler collects runtime performance data from your live applications, and provides recommendations that can help you fine-tune your application performance. + +### Installation + +Import to your project: ```ts import * as codeguruprofiler from '@aws-cdk/aws-codeguruprofiler'; ``` + +### Basic usage + +Here's how to setup a profiling group and give your compute role permissions to publish to the profiling group to the profiling agent can publish profiling information: + +```ts +// The execution role of your application that publishes to the ProfilingGroup via CodeGuru Profiler Profiling Agent. (the following is merely an example) +const publishAppRole = new Role(stack, 'PublishAppRole', { + assumedBy: new AccountRootPrincipal(), +}); + +const profilingGroup = new ProfilingGroup(stack, 'MyProfilingGroup'); +profilingGroup.grantPublish(publishAppRole); +``` diff --git a/packages/@aws-cdk/aws-codeguruprofiler/lib/index.ts b/packages/@aws-cdk/aws-codeguruprofiler/lib/index.ts index 1dca345aee39a..6ee79ba3c2171 100644 --- a/packages/@aws-cdk/aws-codeguruprofiler/lib/index.ts +++ b/packages/@aws-cdk/aws-codeguruprofiler/lib/index.ts @@ -1,2 +1,3 @@ // AWS::CodeGuruProfiler CloudFormation Resources: export * from './codeguruprofiler.generated'; +export * from './profiling-group'; diff --git a/packages/@aws-cdk/aws-codeguruprofiler/lib/profiling-group.ts b/packages/@aws-cdk/aws-codeguruprofiler/lib/profiling-group.ts new file mode 100644 index 0000000000000..f4d356e093204 --- /dev/null +++ b/packages/@aws-cdk/aws-codeguruprofiler/lib/profiling-group.ts @@ -0,0 +1,180 @@ +import { Grant, IGrantable } from '@aws-cdk/aws-iam'; +import { Construct, IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; +import { CfnProfilingGroup } from './codeguruprofiler.generated'; + +/** + * IResource represents a Profiling Group. + */ +export interface IProfilingGroup extends IResource { + + /** + * A name for the profiling group. + * + * @attribute + */ + readonly profilingGroupName: string; + + /** + * Grant access to publish profiling information to the Profiling Group to the given identity. + * + * This will grant the following permissions: + * + * - codeguru-profiler:ConfigureAgent + * - codeguru-profiler:PostAgentProfile + * + * @param grantee Principal to grant publish rights to + */ + grantPublish(grantee: IGrantable): Grant; + + /** + * Grant access to read profiling information from the Profiling Group to the given identity. + * + * This will grant the following permissions: + * + * - codeguru-profiler:GetProfile + * - codeguru-profiler:DescribeProfilingGroup + * + * @param grantee Principal to grant read rights to + */ + grantRead(grantee: IGrantable): Grant; + +} + +abstract class ProfilingGroupBase extends Resource implements IProfilingGroup { + + public abstract readonly profilingGroupName: string; + + public abstract readonly profilingGroupArn: string; + + /** + * Grant access to publish profiling information to the Profiling Group to the given identity. + * + * This will grant the following permissions: + * + * - codeguru-profiler:ConfigureAgent + * - codeguru-profiler:PostAgentProfile + * + * @param grantee Principal to grant publish rights to + */ + public grantPublish(grantee: IGrantable) { + // https://docs.aws.amazon.com/codeguru/latest/profiler-ug/security-iam.html#security-iam-access-control + return Grant.addToPrincipal({ + grantee, + actions: ['codeguru-profiler:ConfigureAgent', 'codeguru-profiler:PostAgentProfile'], + resourceArns: [this.profilingGroupArn], + }); + } + + /** + * Grant access to read profiling information from the Profiling Group to the given identity. + * + * This will grant the following permissions: + * + * - codeguru-profiler:GetProfile + * - codeguru-profiler:DescribeProfilingGroup + * + * @param grantee Principal to grant read rights to + */ + public grantRead(grantee: IGrantable) { + // https://docs.aws.amazon.com/codeguru/latest/profiler-ug/security-iam.html#security-iam-access-control + return Grant.addToPrincipal({ + grantee, + actions: ['codeguru-profiler:GetProfile', 'codeguru-profiler:DescribeProfilingGroup'], + resourceArns: [this.profilingGroupArn], + }); + } + +} + +/** + * Properties for creating a new Profiling Group. + */ +export interface ProfilingGroupProps { + + /** + * A name for the profiling group. + * @default - automatically generated name. + */ + readonly profilingGroupName?: string; + +} + +/** + * A new Profiling Group. + */ +export class ProfilingGroup extends ProfilingGroupBase { + + /** + * Import an existing Profiling Group provided a Profiling Group Name. + * + * @param scope The parent creating construct + * @param id The construct's name + * @param profilingGroupName Profiling Group Name + */ + public static fromProfilingGroupName(scope: Construct, id: string, profilingGroupName: string): IProfilingGroup { + const stack = Stack.of(scope); + + return this.fromProfilingGroupArn(scope, id, stack.formatArn({ + service: 'codeguru-profiler', + resource: 'profilingGroup', + resourceName: profilingGroupName, + })); + } + + /** + * Import an existing Profiling Group provided an ARN. + * + * @param scope The parent creating construct + * @param id The construct's name + * @param profilingGroupArn Profiling Group ARN + */ + public static fromProfilingGroupArn(scope: Construct, id: string, profilingGroupArn: string): IProfilingGroup { + class Import extends ProfilingGroupBase { + public readonly profilingGroupName = Stack.of(scope).parseArn(profilingGroupArn).resource; + public readonly profilingGroupArn = profilingGroupArn; + } + + return new Import(scope, id); + } + + /** + * The name of the Profiling Group. + * + * @attribute + */ + public readonly profilingGroupName: string; + + /** + * The ARN of the Profiling Group. + * + * @attribute + */ + public readonly profilingGroupArn: string; + + constructor(scope: Construct, id: string, props: ProfilingGroupProps = {}) { + super(scope, id, { + physicalName: props.profilingGroupName ?? Lazy.stringValue({ produce: () => this.generateUniqueId() }), + }); + + const profilingGroup = new CfnProfilingGroup(this, 'ProfilingGroup', { + profilingGroupName: this.physicalName, + }); + + this.profilingGroupName = this.getResourceNameAttribute(profilingGroup.ref); + + this.profilingGroupArn = this.getResourceArnAttribute(profilingGroup.attrArn, { + service: 'codeguru-profiler', + resource: 'profilingGroup', + resourceName: this.physicalName, + }); + } + + private generateUniqueId(): string { + const name = this.node.uniqueId; + if (name.length > 240) { + return name.substring(0, 120) + name.substring(name.length - 120); + } + return name; + } + +} diff --git a/packages/@aws-cdk/aws-codeguruprofiler/package.json b/packages/@aws-cdk/aws-codeguruprofiler/package.json index 71efbaf81a1d0..a114721c9e514 100644 --- a/packages/@aws-cdk/aws-codeguruprofiler/package.json +++ b/packages/@aws-cdk/aws-codeguruprofiler/package.json @@ -67,14 +67,18 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0" }, "dependencies": { + "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/core": "0.0.0" }, "peerDependencies": { - "@aws-cdk/core": "0.0.0" + "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "constructs": "^3.0.2" }, "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" diff --git a/packages/@aws-cdk/aws-codeguruprofiler/test/integ.profiler-group.expected.json b/packages/@aws-cdk/aws-codeguruprofiler/test/integ.profiler-group.expected.json new file mode 100644 index 0000000000000..8ea1221f6bbe8 --- /dev/null +++ b/packages/@aws-cdk/aws-codeguruprofiler/test/integ.profiler-group.expected.json @@ -0,0 +1,132 @@ +{ + "Resources": { + "MyProfilingGroup829F0507": { + "Type": "AWS::CodeGuruProfiler::ProfilingGroup", + "Properties": { + "ProfilingGroupName": "ProfilerGroupIntegrationTestMyProfilingGroup81DA69A3" + } + }, + "PublishAppRole9FEBD682": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PublishAppRoleDefaultPolicyCA1E15C3": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codeguru-profiler:ConfigureAgent", + "codeguru-profiler:PostAgentProfile" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyProfilingGroup829F0507", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PublishAppRoleDefaultPolicyCA1E15C3", + "Roles": [ + { + "Ref": "PublishAppRole9FEBD682" + } + ] + } + }, + "ReadAppRole52FE6317": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "ReadAppRoleDefaultPolicy4BB8955C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codeguru-profiler:GetProfile", + "codeguru-profiler:DescribeProfilingGroup" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyProfilingGroup829F0507", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ReadAppRoleDefaultPolicy4BB8955C", + "Roles": [ + { + "Ref": "ReadAppRole52FE6317" + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codeguruprofiler/test/integ.profiler-group.ts b/packages/@aws-cdk/aws-codeguruprofiler/test/integ.profiler-group.ts new file mode 100644 index 0000000000000..d947e85e823a4 --- /dev/null +++ b/packages/@aws-cdk/aws-codeguruprofiler/test/integ.profiler-group.ts @@ -0,0 +1,28 @@ +import { AccountRootPrincipal, Role } from '@aws-cdk/aws-iam'; +import { App, Stack, StackProps } from '@aws-cdk/core'; +import { ProfilingGroup } from '../lib'; + +class ProfilerGroupIntegrationTest extends Stack { + constructor(scope: App, id: string, props?: StackProps) { + super(scope, id, props); + + const profilingGroup = new ProfilingGroup(this, 'MyProfilingGroup'); + + const publishAppRole = new Role(this, 'PublishAppRole', { + assumedBy: new AccountRootPrincipal(), + }); + profilingGroup.grantPublish(publishAppRole); + + const readAppRole = new Role(this, 'ReadAppRole', { + assumedBy: new AccountRootPrincipal(), + }); + profilingGroup.grantRead(readAppRole); + + } +} + +const app = new App(); + +new ProfilerGroupIntegrationTest(app, 'ProfilerGroupIntegrationTest'); + +app.synth(); diff --git a/packages/@aws-cdk/aws-codeguruprofiler/test/profiling-group.test.ts b/packages/@aws-cdk/aws-codeguruprofiler/test/profiling-group.test.ts new file mode 100644 index 0000000000000..0fbf063cccfaa --- /dev/null +++ b/packages/@aws-cdk/aws-codeguruprofiler/test/profiling-group.test.ts @@ -0,0 +1,393 @@ +import { expect } from '@aws-cdk/assert'; +import { AccountRootPrincipal, Role } from '@aws-cdk/aws-iam'; +import { Stack } from '@aws-cdk/core'; +import { ProfilingGroup } from '../lib'; + +// tslint:disable:object-literal-key-quotes + +describe('profiling group', () => { + + test('attach read permission to Profiling group via fromProfilingGroupArn', () => { + const stack = new Stack(); + // dummy role to test out read permissions on ProfilingGroup + const readAppRole = new Role(stack, 'ReadAppRole', { + assumedBy: new AccountRootPrincipal(), + }); + + const profilingGroup = ProfilingGroup.fromProfilingGroupArn(stack, 'MyProfilingGroup', 'arn:aws:codeguru-profiler:us-east-1:1234567890:profilingGroup/MyAwesomeProfilingGroup'); + profilingGroup.grantRead(readAppRole); + + expect(stack).toMatch({ + 'Resources': { + 'ReadAppRole52FE6317': { + 'Type': 'AWS::IAM::Role', + 'Properties': { + 'AssumeRolePolicyDocument': { + 'Statement': [ + { + 'Action': 'sts:AssumeRole', + 'Effect': 'Allow', + 'Principal': { + 'AWS': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':iam::', + { + 'Ref': 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + }, + ], + 'Version': '2012-10-17', + }, + }, + }, + 'ReadAppRoleDefaultPolicy4BB8955C': { + 'Type': 'AWS::IAM::Policy', + 'Properties': { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': [ + 'codeguru-profiler:GetProfile', + 'codeguru-profiler:DescribeProfilingGroup', + ], + 'Effect': 'Allow', + 'Resource': 'arn:aws:codeguru-profiler:us-east-1:1234567890:profilingGroup/MyAwesomeProfilingGroup', + }, + ], + 'Version': '2012-10-17', + }, + 'PolicyName': 'ReadAppRoleDefaultPolicy4BB8955C', + 'Roles': [ + { + 'Ref': 'ReadAppRole52FE6317', + }, + ], + }, + }, + }, + }); + }); + + test('attach publish permission to Profiling group via fromProfilingGroupName', () => { + const stack = new Stack(); + // dummy role to test out publish permissions on ProfilingGroup + const publishAppRole = new Role(stack, 'PublishAppRole', { + assumedBy: new AccountRootPrincipal(), + }); + + const profilingGroup = ProfilingGroup.fromProfilingGroupName(stack, 'MyProfilingGroup', 'MyAwesomeProfilingGroup'); + profilingGroup.grantPublish(publishAppRole); + + expect(stack).toMatch({ + 'Resources': { + 'PublishAppRole9FEBD682': { + 'Type': 'AWS::IAM::Role', + 'Properties': { + 'AssumeRolePolicyDocument': { + 'Statement': [ + { + 'Action': 'sts:AssumeRole', + 'Effect': 'Allow', + 'Principal': { + 'AWS': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':iam::', + { + 'Ref': 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + }, + ], + 'Version': '2012-10-17', + }, + }, + }, + 'PublishAppRoleDefaultPolicyCA1E15C3': { + 'Type': 'AWS::IAM::Policy', + 'Properties': { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': [ + 'codeguru-profiler:ConfigureAgent', + 'codeguru-profiler:PostAgentProfile', + ], + 'Effect': 'Allow', + 'Resource': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':codeguru-profiler:', + { + 'Ref': 'AWS::Region', + }, + ':', + { + 'Ref': 'AWS::AccountId', + }, + ':profilingGroup/MyAwesomeProfilingGroup', + ], + ], + }, + }, + ], + 'Version': '2012-10-17', + }, + 'PolicyName': 'PublishAppRoleDefaultPolicyCA1E15C3', + 'Roles': [ + { + 'Ref': 'PublishAppRole9FEBD682', + }, + ], + }, + }, + }, + }); + }); + + test('default profiling group', () => { + const stack = new Stack(); + new ProfilingGroup(stack, 'MyProfilingGroup', { + profilingGroupName: 'MyAwesomeProfilingGroup', + }); + + expect(stack).toMatch({ + 'Resources': { + 'MyProfilingGroup829F0507': { + 'Type': 'AWS::CodeGuruProfiler::ProfilingGroup', + 'Properties': { + 'ProfilingGroupName': 'MyAwesomeProfilingGroup', + }, + }, + }, + }); + }); + + test('default profiling group without name', () => { + const stack = new Stack(); + new ProfilingGroup(stack, 'MyProfilingGroup', { + }); + + expect(stack).toMatch({ + 'Resources': { + 'MyProfilingGroup829F0507': { + 'Type': 'AWS::CodeGuruProfiler::ProfilingGroup', + 'Properties': { + 'ProfilingGroupName': 'MyProfilingGroup', + }, + }, + }, + }); + }); + + test('default profiling group without name when name exceeding limit is generated', () => { + const stack = new Stack(); + new ProfilingGroup(stack, 'MyProfilingGroupWithAReallyLongProfilingGroupNameThatExceedsTheLimitOfProfilingGroupNameSize_InOrderToDoSoTheNameMustBeGreaterThanTwoHundredAndFiftyFiveCharacters_InSuchCasesWePickUpTheFirstOneTwentyCharactersFromTheBeginningAndTheEndAndConcatenateThemToGetTheIdentifier', { + }); + + expect(stack).toMatch({ + 'Resources': { + 'MyProfilingGroupWithAReallyLongProfilingGroupNameThatExceedsTheLimitOfProfilingGroupNameSizeInOrderToDoSoTheNameMustBeGreaterThanTwoHundredAndFiftyFiveCharactersInSuchCasesWePickUpTheFirstOneTwentyCharactersFromTheBeginningAndTheEndAndConca4B39908C': { + 'Type': 'AWS::CodeGuruProfiler::ProfilingGroup', + 'Properties': { + 'ProfilingGroupName': 'MyProfilingGroupWithAReallyLongProfilingGroupNameThatExceedsTheLimitOfProfilingGroupNameSizeInOrderToDoSoTheNameMustBeGrnTwoHundredAndFiftyFiveCharactersInSuchCasesWePickUpTheFirstOneTwentyCharactersFromTheBeginningAndTheEndAndConca2FE009B0', + }, + }, + }, + }); + }); + + test('grant publish permissions profiling group', () => { + const stack = new Stack(); + const profilingGroup = new ProfilingGroup(stack, 'MyProfilingGroup', { + profilingGroupName: 'MyAwesomeProfilingGroup', + }); + const publishAppRole = new Role(stack, 'PublishAppRole', { + assumedBy: new AccountRootPrincipal(), + }); + + profilingGroup.grantPublish(publishAppRole); + + expect(stack).toMatch({ + 'Resources': { + 'MyProfilingGroup829F0507': { + 'Type': 'AWS::CodeGuruProfiler::ProfilingGroup', + 'Properties': { + 'ProfilingGroupName': 'MyAwesomeProfilingGroup', + }, + }, + 'PublishAppRole9FEBD682': { + 'Type': 'AWS::IAM::Role', + 'Properties': { + 'AssumeRolePolicyDocument': { + 'Statement': [ + { + 'Action': 'sts:AssumeRole', + 'Effect': 'Allow', + 'Principal': { + 'AWS': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':iam::', + { + 'Ref': 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + }, + ], + 'Version': '2012-10-17', + }, + }, + }, + 'PublishAppRoleDefaultPolicyCA1E15C3': { + 'Type': 'AWS::IAM::Policy', + 'Properties': { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': [ + 'codeguru-profiler:ConfigureAgent', + 'codeguru-profiler:PostAgentProfile', + ], + 'Effect': 'Allow', + 'Resource': { + 'Fn::GetAtt': [ + 'MyProfilingGroup829F0507', + 'Arn', + ], + }, + }, + ], + 'Version': '2012-10-17', + }, + 'PolicyName': 'PublishAppRoleDefaultPolicyCA1E15C3', + 'Roles': [ + { + 'Ref': 'PublishAppRole9FEBD682', + }, + ], + }, + }, + }, + }); + }); + + test('grant read permissions profiling group', () => { + const stack = new Stack(); + const profilingGroup = new ProfilingGroup(stack, 'MyProfilingGroup', { + profilingGroupName: 'MyAwesomeProfilingGroup', + }); + const readAppRole = new Role(stack, 'ReadAppRole', { + assumedBy: new AccountRootPrincipal(), + }); + + profilingGroup.grantRead(readAppRole); + + expect(stack).toMatch({ + 'Resources': { + 'MyProfilingGroup829F0507': { + 'Type': 'AWS::CodeGuruProfiler::ProfilingGroup', + 'Properties': { + 'ProfilingGroupName': 'MyAwesomeProfilingGroup', + }, + }, + 'ReadAppRole52FE6317': { + 'Type': 'AWS::IAM::Role', + 'Properties': { + 'AssumeRolePolicyDocument': { + 'Statement': [ + { + 'Action': 'sts:AssumeRole', + 'Effect': 'Allow', + 'Principal': { + 'AWS': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':iam::', + { + 'Ref': 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + }, + ], + 'Version': '2012-10-17', + }, + }, + }, + 'ReadAppRoleDefaultPolicy4BB8955C': { + 'Type': 'AWS::IAM::Policy', + 'Properties': { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': [ + 'codeguru-profiler:GetProfile', + 'codeguru-profiler:DescribeProfilingGroup', + ], + 'Effect': 'Allow', + 'Resource': { + 'Fn::GetAtt': [ + 'MyProfilingGroup829F0507', + 'Arn', + ], + }, + }, + ], + 'Version': '2012-10-17', + }, + 'PolicyName': 'ReadAppRoleDefaultPolicy4BB8955C', + 'Roles': [ + { + 'Ref': 'ReadAppRole52FE6317', + }, + ], + }, + }, + }, + }); + }); + +}); 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/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/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/cross-region-support-stack.ts b/packages/@aws-cdk/aws-codepipeline/lib/cross-region-support-stack.ts index 47227f4fb689d..00d0c5ca29493 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/cross-region-support-stack.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/cross-region-support-stack.ts @@ -71,6 +71,8 @@ export interface CrossRegionSupportStackProps { * @example '012345678901' */ readonly account: string; + + readonly synthesizer: cdk.IStackSynthesizer | undefined; } /** @@ -90,6 +92,7 @@ export class CrossRegionSupportStack extends cdk.Stack { region: props.region, account: props.account, }, + synthesizer: props.synthesizer, }); const crossRegionSupportConstruct = new CrossRegionSupportConstruct(this, 'Default'); diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index 05b4c174f6aa6..b498c20945f83 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -2,7 +2,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 s3 from '@aws-cdk/aws-s3'; -import { App, Construct, Lazy, PhysicalName, RemovalPolicy, Resource, Stack, Token } from '@aws-cdk/core'; +import { + App, BootstraplessSynthesizer, Construct, DefaultStackSynthesizer, + IStackSynthesizer, Lazy, PhysicalName, RemovalPolicy, Resource, Stack, Token, +} from '@aws-cdk/core'; import { ActionCategory, IAction, IPipeline, IStage } from './action'; import { CfnPipeline } from './codepipeline.generated'; import { CrossRegionSupportConstruct, CrossRegionSupportStack } from './cross-region-support-stack'; @@ -483,6 +486,7 @@ export class Pipeline extends PipelineBase { pipelineStackName: pipelineStack.stackName, region: actionRegion, account: pipelineAccount, + synthesizer: this.getCrossRegionSupportSynthesizer(), }); } @@ -492,6 +496,23 @@ export class Pipeline extends PipelineBase { }; } + private getCrossRegionSupportSynthesizer(): IStackSynthesizer | undefined { + if (this.stack.synthesizer instanceof DefaultStackSynthesizer) { + // if we have the new synthesizer, + // we need a bootstrapless copy of it, + // because we don't want to require bootstrapping the environment + // of the pipeline account in this replication region + return new BootstraplessSynthesizer({ + deployRoleArn: this.stack.synthesizer.deployRoleArn, + cloudFormationExecutionRoleArn: this.stack.synthesizer.cloudFormationExecutionRoleArn, + }); + } else { + // any other synthesizer: just return undefined + // (ie., use the default based on the context settings) + return undefined; + } + } + private generateNameForDefaultBucketKeyAlias(): string { const prefix = 'alias/codepipeline-'; const maxAliasLength = 256; @@ -728,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; } @@ -853,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/package.json b/packages/@aws-cdk/aws-codepipeline/package.json index 36154be191da0..0a94e85b6a724 100644 --- a/packages/@aws-cdk/aws-codepipeline/package.json +++ b/packages/@aws-cdk/aws-codepipeline/package.json @@ -68,6 +68,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "@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/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-codepipeline/test/test.pipeline.ts b/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts index 153e24d882f8a..5d1c91edd51af 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts @@ -3,6 +3,7 @@ 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 cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Test } from 'nodeunit'; import * as codepipeline from '../lib'; import { FakeBuildAction } from './fake-build-action'; @@ -46,7 +47,7 @@ export = { }, 'that is cross-region': { - 'validates that source actions are in the same account as the pipeline'(test: Test) { + 'validates that source actions are in the same region as the pipeline'(test: Test) { const app = new cdk.App(); const stack = new cdk.Stack(app, 'PipelineStack', { env: { region: 'us-west-1', account: '123456789012' }}); const pipeline = new codepipeline.Pipeline(stack, 'Pipeline'); @@ -296,6 +297,46 @@ export = { test.done(); }, + + 'generates the support stack containing the replication Bucket without the need to bootstrap in that environment'(test: Test) { + const app = new cdk.App({ + treeMetadata: false, // we can't set the context otherwise, because App will have a child + }); + app.node.setContext(cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT, true); + + const pipelineStack = new cdk.Stack(app, 'PipelineStack', { + env: { region: 'us-west-2', account: '123456789012' }, + }); + const sourceOutput = new codepipeline.Artifact(); + new codepipeline.Pipeline(pipelineStack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [new FakeSourceAction({ + actionName: 'Source', + output: sourceOutput, + })], + }, + { + stageName: 'Build', + actions: [new FakeBuildAction({ + actionName: 'Build', + input: sourceOutput, + region: 'eu-south-1', + })], + }, + ], + }); + + const assembly = app.synth(); + const supportStackArtifact = assembly.getStackByName('PipelineStack-support-eu-south-1'); + test.equal(supportStackArtifact.assumeRoleArn, + 'arn:${AWS::Partition}:iam::123456789012:role/cdk-hnb659fds-deploy-role-123456789012-us-west-2'); + test.equal(supportStackArtifact.cloudFormationExecutionRoleArn, + 'arn:${AWS::Partition}:iam::123456789012:role/cdk-hnb659fds-cfn-exec-role-123456789012-us-west-2'); + + test.done(); + }, }, 'that is cross-account': { 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-attr.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts index db7fcaa8e163e..e2a76c64120ef 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts @@ -17,7 +17,7 @@ export interface RequiredAttributes { readonly birthdate?: boolean; /** - * Whether theb user's e-mail address, represented as an RFC 5322 [RFC5322] addr-spec, is a required attribute. + * Whether the user's e-mail address, represented as an RFC 5322 [RFC5322] addr-spec, is a required attribute. * @default false */ readonly email?: boolean; 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 186db2266c2fa..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; @@ -1114,9 +1134,9 @@ export class Table extends TableBase { * @param nonKeyAttributes a list of non-key attribute names */ private validateNonKeyAttributes(nonKeyAttributes: string[]) { - if (this.nonKeyAttributes.size + nonKeyAttributes.length > 20) { + if (this.nonKeyAttributes.size + nonKeyAttributes.length > 100) { // https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html#limits-secondary-indexes - throw new RangeError('a maximum number of nonKeyAttributes across all of secondary indexes is 20'); + throw new RangeError('a maximum number of nonKeyAttributes across all of secondary indexes is 100'); } // store all non-key attributes diff --git a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts index c422330e2c1ce..c0c0fe9633ac0 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts @@ -1114,7 +1114,7 @@ test('error when adding a global secondary index with projection type INCLUDE, b const table = new Table(stack, CONSTRUCT_NAME, { partitionKey: TABLE_PARTITION_KEY, sortKey: TABLE_SORT_KEY }); const gsiNonKeyAttributeGenerator = NON_KEY_ATTRIBUTE_GENERATOR(GSI_NON_KEY); const gsiNonKeyAttributes: string[] = []; - for (let i = 0; i < 21; i++) { + for (let i = 0; i < 101; i++) { gsiNonKeyAttributes.push(gsiNonKeyAttributeGenerator.next().value); } @@ -1124,7 +1124,7 @@ test('error when adding a global secondary index with projection type INCLUDE, b sortKey: GSI_SORT_KEY, projectionType: ProjectionType.INCLUDE, nonKeyAttributes: gsiNonKeyAttributes, - })).toThrow(/a maximum number of nonKeyAttributes across all of secondary indexes is 20/); + })).toThrow(/a maximum number of nonKeyAttributes across all of secondary indexes is 100/); }); test('error when adding a global secondary index with read or write capacity on a PAY_PER_REQUEST table', () => { @@ -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-eks/README.md b/packages/@aws-cdk/aws-eks/README.md index e6d94a2838257..d77259e7fb3fb 100644 --- a/packages/@aws-cdk/aws-eks/README.md +++ b/packages/@aws-cdk/aws-eks/README.md @@ -526,26 +526,33 @@ With services account you can provide Kubernetes Pods access to AWS resources. ```ts // add service account -const serviceAccount = cluster.addServiceAccount('MyServiceAccount'); +const sa = cluster.addServiceAccount('MyServiceAccount'); const bucket = new Bucket(this, 'Bucket'); bucket.grantReadWrite(serviceAccount); -cluster.addResource('mypod', { +const mypod = cluster.addResource('mypod', { apiVersion: 'v1', kind: 'Pod', metadata: { name: 'mypod' }, spec: { + serviceAccountName: sa.serviceAccountName containers: [ { name: 'hello', image: 'paulbouwer/hello-kubernetes:1.5', ports: [ { containerPort: 8080 } ], - serviceAccountName: serviceAccount.serviceAccountName + } ] } }); + +// create the resource after the service account +mypod.node.addDependency(sa); + +// print the IAM role arn for this service account +new cdk.CfnOutput(this, 'ServiceAccountIamRole', { value: sa.role.roleArn }) ``` ### Roadmap diff --git a/packages/@aws-cdk/aws-eks/lib/service-account.ts b/packages/@aws-cdk/aws-eks/lib/service-account.ts index 24865c4a36f4b..83da66fbfef73 100644 --- a/packages/@aws-cdk/aws-eks/lib/service-account.ts +++ b/packages/@aws-cdk/aws-eks/lib/service-account.ts @@ -78,7 +78,7 @@ export class ServiceAccount extends Construct implements IPrincipal { this.grantPrincipal = this.role.grantPrincipal; this.policyFragment = this.role.policyFragment; - cluster.addResource('ServiceAccount', { + cluster.addResource(`${id}ServiceAccountResource`, { apiVersion: 'v1', kind: 'ServiceAccount', metadata: { diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json index 29b38c2393bc1..164377d944797 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json @@ -2298,7 +2298,7 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, - "ClustermanifestServiceAccountD03C306D": { + "ClustermanifestMyServiceAccountServiceAccountResource0EC03615": { "Type": "Custom::AWSCDK-EKS-KubernetesResource", "Properties": { "ServiceToken": { diff --git a/packages/@aws-cdk/aws-eks/test/test.service-account.ts b/packages/@aws-cdk/aws-eks/test/test.service-account.ts index 71b04ee993d04..8c83c62da2810 100644 --- a/packages/@aws-cdk/aws-eks/test/test.service-account.ts +++ b/packages/@aws-cdk/aws-eks/test/test.service-account.ts @@ -65,5 +65,50 @@ export = { })); test.done(); }, + 'should have allow multiple services accounts'(test: Test) { + // GIVEN + const { stack, cluster } = testFixtureCluster(); + + // WHEN + cluster.addServiceAccount('MyServiceAccount'); + cluster.addServiceAccount('MyOtherServiceAccount'); + + // THEN + expect(stack).to(haveResource(eks.KubernetesResource.RESOURCE_TYPE, { + ServiceToken: { + 'Fn::GetAtt': [ + 'awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B', + 'Outputs.StackawscdkawseksKubectlProviderframeworkonEvent8897FD9BArn', + ], + }, + Manifest: { + 'Fn::Join': [ + '', + [ + '[{\"apiVersion\":\"v1\",\"kind\":\"ServiceAccount\",\"metadata\":{\"name\":\"stackclustermyotherserviceaccounta472761a\",\"namespace\":\"default\",\"labels\":{\"app.kubernetes.io/name\":\"stackclustermyotherserviceaccounta472761a\"},\"annotations\":{\"eks.amazonaws.com/role-arn\":\"', + { + 'Fn::GetAtt': [ + 'ClusterMyOtherServiceAccountRole764583C5', + 'Arn', + ], + }, + '\"}}}]', + ], + ], + }, + })); + test.done(); + }, + 'should have unique resource name'(test: Test) { + // GIVEN + const { cluster } = testFixtureCluster(); + + // WHEN + cluster.addServiceAccount('MyServiceAccount'); + + // THEN + test.throws(() => cluster.addServiceAccount('MyServiceAccount')); + test.done(); + }, }, }; diff --git a/packages/@aws-cdk/aws-events-targets/build-tools/gen.js b/packages/@aws-cdk/aws-events-targets/build-tools/gen.js new file mode 100644 index 0000000000000..406c72cd9b11e --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/build-tools/gen.js @@ -0,0 +1,40 @@ +#!/usr/bin/env node + +/** + * Writes lib/sdk-api-metadata.generated.ts from the metadata gathered from the + * aws-sdk package. + */ + +const fs = require('fs'); +const path = require('path'); + +const packageInfo = require('aws-sdk/package.json'); +const sdkMetadata = require('aws-sdk/apis/metadata.json'); + +fs.writeFileSync( + path.resolve(__dirname, '..', 'lib', 'sdk-api-metadata.generated.ts'), + [ + 'export interface AwsSdkMetadata {', + ' readonly [service: string]: {', + ' readonly name: string;', + ' readonly cors?: boolean;', + ' readonly dualstackAvailable?: boolean;', + ' readonly prefix?: string;', + ' readonly versions?: readonly string[];', + ' readonly xmlNoDefaultLists?: boolean;', + ' readonly [key: string]: unknown;', + ' };', + '}', + '', + // The generated code is probably not going to be super clean as far as linters are concerned... + '/* eslint-disable */', + '/* tslint:disable */', + '', + // Just mention where the data comes from, as a basic courtesy... + '/**', + ` * Extracted from ${packageInfo.name} version ${packageInfo.version} (${packageInfo.license}).`, + ' */', + // And finally, we export the data: + `export const metadata: AwsSdkMetadata = ${JSON.stringify(sdkMetadata, null, 2)};`, + ].join('\n'), +); diff --git a/packages/@aws-cdk/aws-events-targets/lib/aws-api.ts b/packages/@aws-cdk/aws-events-targets/lib/aws-api.ts index fdf0ac50eafa0..b47f23e6e9a2b 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/aws-api.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/aws-api.ts @@ -2,7 +2,7 @@ import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import * as path from 'path'; -import * as metadata from './sdk-api-metadata.json'; +import { metadata } from './sdk-api-metadata.generated'; import { addLambdaPermission } from './util'; /** diff --git a/packages/@aws-cdk/aws-events-targets/package.json b/packages/@aws-cdk/aws-events-targets/package.json index 5142667aed462..0216eabf50638 100644 --- a/packages/@aws-cdk/aws-events-targets/package.json +++ b/packages/@aws-cdk/aws-events-targets/package.json @@ -48,7 +48,7 @@ }, "cdk-build": { "pre": [ - "cp -f $(node -p 'require.resolve(\"aws-sdk/apis/metadata.json\")') lib/sdk-api-metadata.json && rm -f lib/sdk-api-metadata.d.ts" + "node ./build-tools/gen.js" ], "jest": true }, 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/lib/builder.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/builder.ts index 41ad7aa0df53a..dd8e3ba2f8565 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/builder.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/builder.ts @@ -111,11 +111,11 @@ 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}`] : []), - '-w', path.dirname(containerEntryPath), + '-w', path.dirname(containerEntryPath).replace(/\\/g, '/'), // Always use POSIX paths in the container 'parcel-bundler', ]; const parcelArgs = [ - 'parcel', 'build', containerEntryPath, + 'parcel', 'build', containerEntryPath.replace(/\\/g, '/'), // Always use POSIX paths in the container '--out-dir', containerOutDir, '--out-file', 'index.js', '--global', this.options.global, 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 e6e32655a187e..6c7f5e41ae3e0 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/builder.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/builder.test.ts @@ -20,6 +20,10 @@ jest.mock('child_process', () => ({ }), })); +beforeEach(() => { + jest.clearAllMocks(); +}); + test('calls docker with the correct args', () => { const builder = new Builder({ entry: '/project/folder/entry.ts', @@ -58,6 +62,24 @@ test('calls docker with the correct args', () => { ]); }); +test('with Windows paths', () => { + const builder = new Builder({ + entry: 'C:\\my-project\\lib\\entry.ts', + global: 'handler', + outDir: '/out-dir', + cacheDir: '/cache-dir', + nodeDockerTag: 'lts-alpine', + nodeVersion: '12', + projectRoot: 'C:\\my-project', + }); + builder.build(); + + // docker run + expect(spawnSync).toHaveBeenCalledWith('docker', expect.arrayContaining([ + 'parcel', 'build', expect.stringContaining('/lib/entry.ts'), + ])); +}); + test('throws in case of error', () => { const builder = new Builder({ entry: '/project/folder/error', diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index 292d921efb0fa..367e4dc8206d9 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -68,7 +68,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/aws-lambda": "^8.10.39", - "@types/lodash": "^4.14.152", + "@types/lodash": "^4.14.153", "@types/nodeunit": "^0.0.31", "@types/sinon": "^9.0.3", "aws-sdk": "^2.681.0", diff --git a/packages/@aws-cdk/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/README.md b/packages/@aws-cdk/aws-rds/README.md index 7a6e8e8ca9597..070ac5ca1698c 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -23,7 +23,7 @@ your instances will be launched privately or publicly: const cluster = new DatabaseCluster(this, 'Database', { engine: DatabaseClusterEngine.AURORA, masterUser: { - username: 'admin' + username: 'clusteradmin' }, instanceProps: { instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), 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-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index 92a473d886d10..c7cc3fe389099 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -509,14 +509,13 @@ Step Functions supports [AWS Glue](https://docs.aws.amazon.com/step-functions/la You can call the [`StartJobRun`](https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-jobs-runs.html#aws-glue-api-jobs-runs-StartJobRun) API from a `Task` state. ```ts -new sfn.Task(stack, 'Task', { - task: new tasks.RunGlueJobTask(jobName, { - arguments: { - key: 'value', - }, - timeout: cdk.Duration.minutes(30), - notifyDelayAfter: cdk.Duration.minutes(5), - }), +new GlueStartJobRun(stack, 'Task', { + jobName: 'my-glue-job', + arguments: { + key: 'value', + }, + timeout: cdk.Duration.minutes(30), + notifyDelayAfter: cdk.Duration.minutes(5), }); ``` diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/glue/run-glue-job-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/glue/run-glue-job-task.ts index 854df949c4dc9..fd4722835a52e 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/glue/run-glue-job-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/glue/run-glue-job-task.ts @@ -5,6 +5,8 @@ import { getResourceArn } from '../resource-arn-suffix'; /** * Properties for RunGlueJobTask + * + * @deprecated use `GlueStartJobRun` */ export interface RunGlueJobTaskProps { @@ -63,6 +65,8 @@ export interface RunGlueJobTaskProps { * https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-jobs-runs.html#aws-glue-api-jobs-runs-JobRun * * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-glue.html + * + * @deprecated use `GlueStartJobRun` */ export class RunGlueJobTask implements sfn.IStepFunctionsTask { private readonly integrationPattern: sfn.ServiceIntegrationPattern; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/glue/start-job-run.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/glue/start-job-run.ts new file mode 100644 index 0000000000000..9df1a6a5ed852 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/glue/start-job-run.ts @@ -0,0 +1,119 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Construct, Duration, Stack } from '@aws-cdk/core'; +import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; + +/** + * Properties for starting an AWS Glue job as a task + */ +export interface GlueStartJobRunProps extends sfn.TaskStateBaseProps { + + /** + * Glue job name + */ + readonly glueJobName: string; + + /** + * The job arguments specifically for this run. + * + * For this job run, they replace the default arguments set in the job + * definition itself. + * + * @default - Default arguments set in the job definition + */ + readonly arguments?: sfn.TaskInput; + + /** + * The name of the SecurityConfiguration structure to be used with this job run. + * + * This must match the Glue API + * @see https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-common.html#aws-glue-api-regex-oneLine + * + * @default - Default configuration set in the job definition + */ + readonly securityConfiguration?: string; + + /** + * After a job run starts, the number of minutes to wait before sending a job run delay notification. + * + * Must be at least 1 minute. + * + * @default - Default delay set in the job definition + */ + readonly notifyDelayAfter?: Duration; +} + +/** + * Starts an AWS Glue job in a Task state + * + * OUTPUT: the output of this task is a JobRun structure, for details consult + * https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-jobs-runs.html#aws-glue-api-jobs-runs-JobRun + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-glue.html + */ +export class GlueStartJobRun extends sfn.TaskStateBase { + private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ + sfn.IntegrationPattern.REQUEST_RESPONSE, + sfn.IntegrationPattern.RUN_JOB, + ]; + + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + private readonly integrationPattern: sfn.IntegrationPattern; + + constructor(scope: Construct, id: string, private readonly props: GlueStartJobRunProps) { + super(scope, id, props); + this.integrationPattern = props.integrationPattern ?? sfn.IntegrationPattern.REQUEST_RESPONSE; + + validatePatternSupported(this.integrationPattern, GlueStartJobRun.SUPPORTED_INTEGRATION_PATTERNS); + + this.taskPolicies = this.getPolicies(); + + this.taskMetrics = { + metricPrefixSingular: 'GlueJob', + metricPrefixPlural: 'GlueJobs', + metricDimensions: { GlueJobName: this.props.glueJobName }, + }; + } + + protected renderTask(): any { + const notificationProperty = this.props.notifyDelayAfter ? { NotifyDelayAfter: this.props.notifyDelayAfter.toMinutes() } : null; + return { + Resource: integrationResourceArn('glue', 'startJobRun', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + JobName: this.props.glueJobName, + Arguments: this.props.arguments?.value, + Timeout: this.props.timeout?.toMinutes(), + SecurityConfiguration: this.props.securityConfiguration, + NotificationProperty: notificationProperty, + }), + TimeoutSeconds: undefined, + }; + } + + private getPolicies(): iam.PolicyStatement[] { + let iamActions: string[] | undefined; + if (this.integrationPattern === sfn.IntegrationPattern.REQUEST_RESPONSE) { + iamActions = ['glue:StartJobRun']; + } else if (this.integrationPattern === sfn.IntegrationPattern.RUN_JOB) { + iamActions = [ + 'glue:StartJobRun', + 'glue:GetJobRun', + 'glue:GetJobRuns', + 'glue:BatchStopJobRun', + ]; + } + + return [new iam.PolicyStatement({ + resources: [ + Stack.of(this).formatArn({ + service: 'glue', + resource: 'job', + resourceName: this.props.glueJobName, + }), + ], + actions: iamActions, + })]; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts index 9759c4a621251..7b45086a4e48e 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts @@ -23,5 +23,6 @@ export * from './emr/emr-cancel-step'; export * from './emr/emr-modify-instance-fleet-by-name'; export * from './emr/emr-modify-instance-group-by-name'; export * from './glue/run-glue-job-task'; +export * from './glue/start-job-run'; export * from './batch/run-batch-job'; export * from './dynamodb/call-dynamodb'; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.start-job-run.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.start-job-run.expected.json new file mode 100644 index 0000000000000..1f916f6be06f1 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.start-job-run.expected.json @@ -0,0 +1,268 @@ +{ + "Parameters": { + "AssetParametersd030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0dS3BucketB8F6851B": { + "Type": "String", + "Description": "S3 bucket for asset \"d030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0d\"" + }, + "AssetParametersd030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0dS3VersionKey7BCC06FC": { + "Type": "String", + "Description": "S3 key for asset version \"d030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0d\"" + }, + "AssetParametersd030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0dArtifactHashEC764944": { + "Type": "String", + "Description": "Artifact hash for asset \"d030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0d\"" + } + }, + "Resources": { + "GlueJobRole1CD031E0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "glue.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSGlueServiceRole" + ] + ] + } + ] + } + }, + "GlueJobRoleDefaultPolicy3D94D6F1": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "AssetParametersd030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0dS3BucketB8F6851B" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "AssetParametersd030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0dS3BucketB8F6851B" + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "GlueJobRoleDefaultPolicy3D94D6F1", + "Roles": [ + { + "Ref": "GlueJobRole1CD031E0" + } + ] + } + }, + "GlueJob": { + "Type": "AWS::Glue::Job", + "Properties": { + "Command": { + "Name": "glueetl", + "PythonVersion": "3", + "ScriptLocation": { + "Fn::Join": [ + "", + [ + "s3://", + { + "Ref": "AssetParametersd030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0dS3BucketB8F6851B" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersd030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0dS3VersionKey7BCC06FC" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersd030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0dS3VersionKey7BCC06FC" + } + ] + } + ] + } + ] + ] + } + }, + "Role": { + "Fn::GetAtt": [ + "GlueJobRole1CD031E0", + "Arn" + ] + }, + "GlueVersion": "1.0", + "Name": "My Glue Job" + } + }, + "StateMachineRole543B9670": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachineRoleDefaultPolicyDA5F7DA8": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "glue:StartJobRun", + "glue:GetJobRun", + "glue:GetJobRuns", + "glue:BatchStopJobRun" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":glue:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":job/My Glue Job" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachineRoleDefaultPolicyDA5F7DA8", + "Roles": [ + { + "Ref": "StateMachineRole543B9670" + } + ] + } + }, + "StateMachine81935E76": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Start Task\",\"States\":{\"Start Task\":{\"Type\":\"Pass\",\"Next\":\"Glue Job Task\"},\"Glue Job Task\":{\"Next\":\"End Task\",\"Type\":\"Task\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::glue:startJobRun.sync\",\"Parameters\":{\"JobName\":\"My Glue Job\",\"Arguments\":{\"--enable-metrics\":\"true\"}}},\"End Task\":{\"Type\":\"Pass\",\"End\":true}}}" + ] + ] + }, + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRole543B9670", + "Arn" + ] + } + }, + "DependsOn": [ + "StateMachineRoleDefaultPolicyDA5F7DA8", + "StateMachineRole543B9670" + ] + } + }, + "Outputs": { + "StateMachineARNOutput": { + "Value": { + "Ref": "StateMachine81935E76" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.start-job-run.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.start-job-run.ts new file mode 100644 index 0000000000000..d63e2c5f586cc --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.start-job-run.ts @@ -0,0 +1,63 @@ +import * as glue from '@aws-cdk/aws-glue'; +import * as iam from '@aws-cdk/aws-iam'; +import * as assets from '@aws-cdk/aws-s3-assets'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import * as path from 'path'; +import { GlueStartJobRun } from '../../lib/glue/start-job-run'; + +/* + * Stack verification steps: + * * aws stepfunctions start-execution --state-machine-arn + * * aws stepfunctions describe-execution --execution-arn + * The "describe-execution" call should eventually return status "SUCCEEDED". + * NOTE: It will take up to 15 minutes for the step function to complete due to the cold start time + * for AWS Glue, which as of 02/2020, is around 10-15 minutes. + */ + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-stepfunctions-integ'); + +const codeAsset = new assets.Asset(stack, 'Glue Job Script', { + path: path.join(__dirname, 'my-glue-script/job.py'), +}); + +const jobRole = new iam.Role(stack, 'Glue Job Role', { + assumedBy: new iam.ServicePrincipal('glue'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSGlueServiceRole'), + ], +}); +codeAsset.grantRead(jobRole); + +const job = new glue.CfnJob(stack, 'Glue Job', { + name: 'My Glue Job', + glueVersion: '1.0', + command: { + name: 'glueetl', + pythonVersion: '3', + scriptLocation: `s3://${codeAsset.s3BucketName}/${codeAsset.s3ObjectKey}`, + }, + role: jobRole.roleArn, +}); + +const jobTask = new GlueStartJobRun(stack, 'Glue Job Task', { + glueJobName: job.name!, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + arguments: sfn.TaskInput.fromObject({ + '--enable-metrics': 'true', + }), +}); + +const startTask = new sfn.Pass(stack, 'Start Task'); +const endTask = new sfn.Pass(stack, 'End Task'); + +const stateMachine = new sfn.StateMachine(stack, 'State Machine', { + definition: sfn.Chain.start(startTask).next(jobTask).next(endTask), +}); + +new cdk.CfnOutput(stack, 'State Machine ARN Output', { + value: stateMachine.stateMachineArn, +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/start-job-run.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/start-job-run.test.ts new file mode 100644 index 0000000000000..e3773bd701966 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/start-job-run.test.ts @@ -0,0 +1,173 @@ +import '@aws-cdk/assert/jest'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Duration, Stack } from '@aws-cdk/core'; +import * as tasks from '../../lib'; +import { GlueStartJobRun } from '../../lib/glue/start-job-run'; + +const glueJobName = 'GlueJob'; +let stack: Stack; +beforeEach(() => { + stack = new Stack(); +}); + +test('Invoke glue job with just job ARN', () => { + const task = new GlueStartJobRun(stack, 'Task', { + glueJobName, + }); + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::glue:startJobRun', + ], + ], + }, + End: true, + Parameters: { + JobName: glueJobName, + }, + }); +}); + +test('Invoke glue job with full properties', () => { + const jobArguments = { + key: 'value', + }; + const timeoutMinutes = 1440; + const glueJobTimeout = Duration.minutes(timeoutMinutes); + const securityConfiguration = 'securityConfiguration'; + const notifyDelayAfterMinutes = 10; + const notifyDelayAfter = Duration.minutes(notifyDelayAfterMinutes); + const task = new GlueStartJobRun(stack, 'Task', { + glueJobName, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + arguments: sfn.TaskInput.fromObject(jobArguments), + timeout: glueJobTimeout, + securityConfiguration, + notifyDelayAfter, + }); + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::glue:startJobRun.sync', + ], + ], + }, + End: true, + Parameters: { + JobName: glueJobName, + Arguments: jobArguments, + Timeout: timeoutMinutes, + SecurityConfiguration: securityConfiguration, + NotificationProperty: { + NotifyDelayAfter: notifyDelayAfterMinutes, + }, + }, + }); +}); + +test('job arguments can reference state input', () => { + const task = new GlueStartJobRun(stack, 'Task', { + glueJobName, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + arguments: sfn.TaskInput.fromDataAt('$.input'), + }); + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::glue:startJobRun.sync', + ], + ], + }, + End: true, + Parameters: { + 'JobName': glueJobName, + 'Arguments.$': '$.input', + }, + }); +}); + +test('permitted role actions limited to start job run if service integration pattern is REQUEST_RESPONSE', () => { + const task = new GlueStartJobRun(stack, 'Task', { + glueJobName, + integrationPattern: sfn.IntegrationPattern.REQUEST_RESPONSE, + }); + + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [{ + Action: 'glue:StartJobRun', + }], + }, + }); +}); + +test('permitted role actions include start, get, and stop job run if service integration pattern is RUN_JOB', () => { + const task = new GlueStartJobRun(stack, 'Task', { + glueJobName, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + }); + + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [{ + Action: [ + 'glue:StartJobRun', + 'glue:GetJobRun', + 'glue:GetJobRuns', + 'glue:BatchStopJobRun', + ], + }], + }, + }); +}); + +test('Task throws if WAIT_FOR_TASK_TOKEN is supplied as service integration pattern', () => { + expect(() => { + new sfn.Task(stack, 'Task', { + task: new tasks.RunGlueJobTask(glueJobName, { + integrationPattern: sfn.ServiceIntegrationPattern.WAIT_FOR_TASK_TOKEN, + }), + }); + }).toThrow(/Invalid Service Integration Pattern: WAIT_FOR_TASK_TOKEN is not supported to call Glue./i); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions/README.md b/packages/@aws-cdk/aws-stepfunctions/README.md index 557527bd02f1c..b7560ad80883e 100644 --- a/packages/@aws-cdk/aws-stepfunctions/README.md +++ b/packages/@aws-cdk/aws-stepfunctions/README.md @@ -131,20 +131,45 @@ directly in the Amazon States language. ### Pass -A `Pass` state does no work, but it can optionally transform the execution's -JSON state. +A `Pass` state passes its input to its output, without performing work. +Pass states are useful when constructing and debugging state machines. + +The following example injects some fixed data into the state machine through +the `result` field. The `result` field will be added to the input and the result +will be passed as the state's output. ```ts // Makes the current JSON state { ..., "subObject": { "hello": "world" } } const pass = new stepfunctions.Pass(this, 'Add Hello World', { - result: { hello: "world" }, - resultPath: '$.subObject', + result: { hello: 'world' }, + resultPath: '$.subObject', }); // Set the next state pass.next(nextState); ``` +The `Pass` state also supports passing key-value pairs as input. Values can +be static, or selected from the input with a path. + +The following example filters the `greeting` field from the state input +and also injects a field called `otherData`. + +```ts +const pass = new stepfunctions.Pass(this, 'Filter input and inject data', { + parameters: { // input to the pass state + input: stepfunctions.DataAt('$.input.greeting') + otherData: 'some-extra-stuff' + }, +}); +``` + +The object specified in `parameters` will be the input of the `Pass` state. +Since neither `Result` nor `ResultPath` are supplied, the `Pass` state copies +its input through to its output. + +Learn more about the [Pass state](https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-pass-state.html) + ### Wait A `Wait` state waits for a given number of seconds, or until the current time diff --git a/packages/@aws-cdk/aws-stepfunctions/jest.config.js b/packages/@aws-cdk/aws-stepfunctions/jest.config.js index d984ff822379b..cd664e1d069e5 100644 --- a/packages/@aws-cdk/aws-stepfunctions/jest.config.js +++ b/packages/@aws-cdk/aws-stepfunctions/jest.config.js @@ -1,10 +1,2 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); -module.exports = { - ...baseConfig, - coverageThreshold: { - global: { - ...baseConfig.coverageThreshold.global, - branches: 75, - }, - }, -}; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts index e081b8fdc734b..982980456caaa 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts @@ -1,5 +1,6 @@ import * as cdk from '@aws-cdk/core'; import {Chain} from '../chain'; +import { FieldUtils } from '../fields'; import {IChainable, INextable} from '../types'; import { StateType } from './private/state-type'; import {renderJsonPath, State } from './state'; @@ -147,7 +148,17 @@ export class Pass extends State implements INextable { Result: this.result ? this.result.value : undefined, ResultPath: renderJsonPath(this.resultPath), ...this.renderInputOutput(), + ...this.renderParameters(), ...this.renderNextEnd(), }; } + + /** + * Render Parameters in ASL JSON format + */ + private renderParameters(): any { + return FieldUtils.renderObject({ + Parameters: this.parameters, + }); + } } diff --git a/packages/@aws-cdk/aws-stepfunctions/test/custom-state.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/custom-state.test.ts index 723d917986899..9e58281059e8b 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/custom-state.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/custom-state.test.ts @@ -1,12 +1,16 @@ import '@aws-cdk/assert/jest'; import * as cdk from '@aws-cdk/core'; -import * as stepfunctions from '../lib'; +import * as sfn from '../lib'; +import { render } from './private/render-util'; describe('Custom State', () => { - test('maintains the state Json provided during construction', () => { + let stack: cdk.Stack; + let stateJson: any; + + beforeEach(() => { // GIVEN - const stack = new cdk.Stack(); - const stateJson = { + stack = new cdk.Stack(); + stateJson = { Type: 'Task', Resource: 'arn:aws:states:::dynamodb:putItem', Parameters: { @@ -19,9 +23,11 @@ describe('Custom State', () => { }, ResultPath: null, }; + }); + test('maintains the state Json provided during construction', () => { // WHEN - const customState = new stepfunctions.CustomState(stack, 'Custom', { + const customState = new sfn.CustomState(stack, 'Custom', { stateJson, }); @@ -31,4 +37,38 @@ describe('Custom State', () => { End: true, }); }); -}); + + test('can add a next state to the chain', () => { + // WHEN + const definition = new sfn.CustomState(stack, 'Custom', { + stateJson, + }).next(new sfn.Pass(stack, 'MyPass')); + + // THEN + expect(render(stack, definition)).toStrictEqual( + { + StartAt: 'Custom', + States: { + Custom: { + Next: 'MyPass', + Type: 'Task', + Resource: 'arn:aws:states:::dynamodb:putItem', + Parameters: { + TableName: 'MyTable', + Item: { + id: { + S: 'MyEntry', + }, + }, + }, + ResultPath: null, + }, + MyPass: { + Type: 'Pass', + End: true, + }, + }, + }, + ); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/private/render-util.ts b/packages/@aws-cdk/aws-stepfunctions/test/private/render-util.ts new file mode 100644 index 0000000000000..ceb8998c7da92 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/private/render-util.ts @@ -0,0 +1,12 @@ +import * as cdk from '@aws-cdk/core'; +import * as sfn from '../../lib'; + +/** + * Renders a state machine definition + * + * @param stack stack for the state machine + * @param definition state machine definition + */ +export function render(stack: cdk.Stack, definition: sfn.IChainable) { + return stack.resolve(new sfn.StateGraph(definition.startState, 'Test Graph').toGraphJson()); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts index cc6fd2f7ec486..012a193087b9b 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts @@ -274,6 +274,26 @@ describe('State Machine Resources', () => { }); }), + test('parameters can be selected from the input with a path', () => { + // GIVEN + const stack = new cdk.Stack(); + const task = new stepfunctions.Pass(stack, 'Pass', { + parameters: { + input: stepfunctions.Data.stringAt('$.myField'), + }, + }); + + // WHEN + const taskState = task.toStateJson(); + + // THEN + expect(taskState).toEqual({ End: true, + Parameters: + { 'input.$': '$.myField'}, + Type: 'Pass', + }); + }), + test('State machines must depend on their roles', () => { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-stepfunctions/test/state-transition-metrics.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/state-transition-metrics.test.ts new file mode 100644 index 0000000000000..59a66b15a8445 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/state-transition-metrics.test.ts @@ -0,0 +1,54 @@ +import { Metric } from '@aws-cdk/aws-cloudwatch'; +import { StateTransitionMetric } from '../lib'; + +describe('State Transition Metrics', () => { + test('add a named state transition metric', () => { + // WHEN + const metric = StateTransitionMetric.metric('my-metric'); + + // THEN + verifyTransitionMetric(metric, 'my-metric', 'Average'); + }); + + test('metric for available state transitions.', () => { + // WHEN + const metric = StateTransitionMetric.metricProvisionedBucketSize(); + + // THEN + verifyTransitionMetric(metric, 'ProvisionedBucketSize', 'Average'); + }); + + test('metric for provisioned steady-state execution rate', () => { + // WHEN + const metric = StateTransitionMetric.metricProvisionedRefillRate(); + + // THEN + verifyTransitionMetric(metric, 'ProvisionedRefillRate', 'Average'); + }); + + test('metric for state-transitions per second', () => { + // WHEN + const metric = StateTransitionMetric.metricConsumedCapacity(); + + // THEN + verifyTransitionMetric(metric, 'ConsumedCapacity', 'Average'); + }); + + test('metric for the number of throttled state transitions', () => { + // WHEN + const metric = StateTransitionMetric.metricThrottledEvents(); + + // THEN + verifyTransitionMetric(metric, 'ThrottledEvents', 'Sum'); + }); +}); + +function verifyTransitionMetric(metric: Metric, metricName: string, statistic: string) { + expect(metric).toEqual({ + period: { amount: 5, unit: { label: 'minutes', inMillis: 60000 } }, + dimensions: { ServiceMetric: 'StateTransition' }, + namespace: 'AWS/States', + metricName, + statistic, + }); +} diff --git a/packages/@aws-cdk/aws-stepfunctions/test/task.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/task.test.ts new file mode 100644 index 0000000000000..623ee29689308 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/task.test.ts @@ -0,0 +1,127 @@ +import { Metric } from '@aws-cdk/aws-cloudwatch'; +import * as cdk from '@aws-cdk/core'; +import * as sfn from '../lib'; + +describe('Task state', () => { + + let stack: cdk.Stack; + let task: sfn.Task; + + beforeEach(() => { + // GIVEN + stack = new cdk.Stack(); + task = new sfn.Task(stack, 'my-task', { + task: new FakeTask(), + }); + }); + + test('get named metric for the task', () => { + // WHEN + const metric = task.metric('my-metric'); + + // THEN + verifyMetric(metric, 'my-metric', 'Sum'); + }); + + test('add metric for number of times the task failed', () => { + // WHEN + const metric = task.metricFailed(); + + // THEN + verifyMetric(metric, 'Failed', 'Sum'); + }); + + test('add metric for number of times the metrics heartbeat timed out', () => { + // WHEN + const metric = task.metricHeartbeatTimedOut(); + + // THEN + verifyMetric(metric, 'HeartbeatTimedOut', 'Sum'); + }); + + test('add metric for task state run time', () => { + // WHEN + const metric = task.metricRunTime(); + + // THEN + verifyMetric(metric, 'RunTime', 'Average'); + }); + + test('add metric for task schedule time', () => { + // WHEN + const metric = task.metricScheduleTime(); + + // THEN + verifyMetric(metric, 'ScheduleTime', 'Average'); + }); + + test('add metric for number of times the task is scheduled', () => { + // WHEN + const metric = task.metricScheduled(); + + // THEN + verifyMetric(metric, 'Scheduled', 'Sum'); + }); + + test('add metric for number of times the task was started', () => { + // WHEN + const metric = task.metricStarted(); + + // THEN + verifyMetric(metric, 'Started', 'Sum'); + }); + + test('add metric for number of times the task succeeded', () => { + // WHEN + const metric = task.metricSucceeded(); + + // THEN + verifyMetric(metric, 'Succeeded', 'Sum'); + }); + + test('add metric for time between task being scheduled to closing', () => { + // WHEN + const metric = task.metricTime(); + + // THEN + verifyMetric(metric, 'Time', 'Average'); + }); + + test('add metric for number of times the task times out', () => { + // WHEN + const metric = task.metricTimedOut(); + + // THEN + verifyMetric(metric, 'TimedOut', 'Sum'); + }); + +}); + +function verifyMetric(metric: Metric, metricName: string, statistic: string) { + expect(metric).toEqual({ + metricName, + namespace: 'AWS/States', + period: { + amount: 5, + unit: { + inMillis: 60000, + label: 'minutes', + }, + }, + statistic, + dimensions: { + Arn: 'resource', + }, + }); +} + +class FakeTask implements sfn.IStepFunctionsTask { + public bind(_task: sfn.Task): sfn.StepFunctionsTaskConfig { + return { + resourceArn: 'resource', + metricPrefixSingular: '', + metricPrefixPlural: '', + metricDimensions: { Arn: 'resource' }, + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/wait.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/wait.test.ts new file mode 100644 index 0000000000000..94c67543e2e60 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/wait.test.ts @@ -0,0 +1,79 @@ +import '@aws-cdk/assert/jest'; +import * as cdk from '@aws-cdk/core'; +import { Pass, Wait, WaitTime } from '../lib'; +import { render } from './private/render-util'; + +describe('Wait State', () => { + test('wait time from ISO8601 timestamp', () => { + // GIVEN + const timestamp = '2025-01-01T00:00:00Z'; + + // WHEN + const waitTime = WaitTime.timestamp(timestamp); + + // THEN + expect(waitTime).toEqual({ + json: { + Timestamp: '2025-01-01T00:00:00Z', + }, + }); + }); + + test('wait time from seconds path in state object', () => { + // GIVEN + const secondsPath = '$.waitSeconds'; + + // WHEN + const waitTime = WaitTime.secondsPath(secondsPath); + + // THEN + expect(waitTime).toEqual({ + json: { + SecondsPath: '$.waitSeconds', + }, + }); + }); + + test('wait time from timestamp path in state object', () => { + // GIVEN + const path = '$.timestampPath'; + + // WHEN + const waitTime = WaitTime.timestampPath(path); + + // THEN + expect(waitTime).toEqual({ + json: { + TimestampPath: '$.timestampPath', + }, + }); + }); + + test('supports adding a next state', () => { + // GIVEN + const stack = new cdk.Stack(); + const chain = new Wait(stack, 'myWaitState', { + time: WaitTime.duration(cdk.Duration.seconds(30)), + }); + + // WHEN + chain.next(new Pass(stack, 'final pass', {})); + + // THEN + expect(render(stack, chain)).toEqual({ + StartAt: 'myWaitState', + States: { + 'final pass': { + End: true, + Type: 'Pass', + }, + 'myWaitState': { + Next: 'final pass', + Seconds: 30, + Type: 'Wait', + }, + }, + }); + }); + +}); \ No newline at end of file diff --git a/packages/@aws-cdk/cdk-assets-schema/lib/private/schema-helpers.ts b/packages/@aws-cdk/cdk-assets-schema/lib/private/schema-helpers.ts index a124b366a22ad..9ae1d1fcdb39a 100644 --- a/packages/@aws-cdk/cdk-assets-schema/lib/private/schema-helpers.ts +++ b/packages/@aws-cdk/cdk-assets-schema/lib/private/schema-helpers.ts @@ -1,3 +1,5 @@ +import { FileAssetPackaging } from '../file-asset'; + /** * Validate that a given key is of a given type in an object * @@ -51,4 +53,13 @@ export function isObjectAnd(p: (x: object) => A): (x: unknown) => A { export function assertIsObject(x: unknown): asserts x is object { if (typeof x !== 'object' || x === null) { throw new Error(`Expected a map, got '${x}'`); } -} \ No newline at end of file +} + +export function isFileAssetPackaging(x: unknown): FileAssetPackaging { + const str = isString(x); + const validValues = Object.values(FileAssetPackaging) as string[]; // Explicit cast needed because this is a string-valued enum + if (!validValues.includes(str)) { + throw new Error(`Expected a FileAssetPackaging (one of ${validValues.map(v => `'${v}'`).join(', ')}), got '${str}'`); + } + return x as any; +} diff --git a/packages/@aws-cdk/cdk-assets-schema/lib/validate.ts b/packages/@aws-cdk/cdk-assets-schema/lib/validate.ts index 041124f0b2061..2660a6adae98f 100644 --- a/packages/@aws-cdk/cdk-assets-schema/lib/validate.ts +++ b/packages/@aws-cdk/cdk-assets-schema/lib/validate.ts @@ -3,7 +3,7 @@ import { DockerImageAsset } from './docker-image-asset'; import { FileAsset } from './file-asset'; import { ManifestFile } from './manifest-schema'; import { loadMyPackageJson } from './private/my-package-json'; -import { assertIsObject, expectKey, isMapOf, isObjectAnd, isString } from './private/schema-helpers'; +import { assertIsObject, expectKey, isFileAssetPackaging, isMapOf, isObjectAnd, isString } from './private/schema-helpers'; const PACKAGE_VERSION = loadMyPackageJson().version; @@ -63,7 +63,7 @@ function isFileAsset(entry: object): FileAsset { expectKey(entry, 'source', source => { assertIsObject(source); expectKey(source, 'path', isString); - expectKey(source, 'packaging', isString, true); + expectKey(source, 'packaging', isFileAssetPackaging, true); return source; }); diff --git a/packages/@aws-cdk/cdk-assets-schema/test/validate.test.ts b/packages/@aws-cdk/cdk-assets-schema/test/validate.test.ts index 0f4c22e482e61..145ae265aec5f 100644 --- a/packages/@aws-cdk/cdk-assets-schema/test/validate.test.ts +++ b/packages/@aws-cdk/cdk-assets-schema/test/validate.test.ts @@ -1,81 +1,137 @@ -import { AssetManifestSchema } from '../lib'; +import { AssetManifestSchema, FileAssetPackaging } from '../lib'; -test('Correctly validate Docker image asset', () => { - expect(() => { - AssetManifestSchema.validate({ - version: AssetManifestSchema.currentVersion(), - dockerImages: { - asset: { - source: { - directory: '.', - }, - destinations: { - dest: { - region: 'us-north-20', - repositoryName: 'REPO', - imageTag: 'TAG', +describe('Docker image asset', () => { + test('valid input', () => { + expect(() => { + AssetManifestSchema.validate({ + version: AssetManifestSchema.currentVersion(), + dockerImages: { + asset: { + source: { + directory: '.', + }, + destinations: { + dest: { + region: 'us-north-20', + repositoryName: 'REPO', + imageTag: 'TAG', + }, }, }, }, - }, - }); - }).not.toThrow(); -}); + }); + }).not.toThrow(); + }); -test('Throw on invalid Docker image asset', () => { - expect(() => { - AssetManifestSchema.validate({ - version: AssetManifestSchema.currentVersion(), - dockerImages: { - asset: { - source: { }, - destinations: { }, + test('invalid input', () => { + expect(() => { + AssetManifestSchema.validate({ + version: AssetManifestSchema.currentVersion(), + dockerImages: { + asset: { + source: {}, + destinations: {}, + }, }, - }, - }); - }).toThrow(/dockerImages: source: Expected key 'directory' missing/); + }); + }).toThrow(/dockerImages: source: Expected key 'directory' missing/); + }); }); -test('Correctly validate File asset', () => { - expect(() => { - AssetManifestSchema.validate({ - version: AssetManifestSchema.currentVersion(), - files: { - asset: { - source: { - path: 'a/b/c', - }, - destinations: { - dest: { - region: 'us-north-20', - bucketName: 'Bouquet', - objectKey: 'key', +describe('File asset', () => { + describe('valid input', () => { + test('without packaging', () => { + expect(() => { + AssetManifestSchema.validate({ + version: AssetManifestSchema.currentVersion(), + files: { + asset: { + source: { + path: 'a/b/c', + }, + destinations: { + dest: { + region: 'us-north-20', + bucketName: 'Bouquet', + objectKey: 'key', + }, + }, }, }, - }, - }, + }); + }).not.toThrow(); }); - }).not.toThrow(); -}); -test('Throw on invalid file asset', () => { - expect(() => { - AssetManifestSchema.validate({ - version: AssetManifestSchema.currentVersion(), - files: { - asset: { - source: { - path: 3, + for (const packaging of Object.values(FileAssetPackaging)) { + test(`with "${packaging}" packaging`, () => { + expect(() => { + AssetManifestSchema.validate({ + version: AssetManifestSchema.currentVersion(), + files: { + asset: { + source: { + path: 'a/b/c', + packaging, + }, + destinations: { + dest: { + region: 'us-north-20', + bucketName: 'Bouquet', + objectKey: 'key', + }, + }, + }, + }, + }); + }).not.toThrow(); + }); + } + }); + + describe('invalid input', () => { + test('bad "source.path" property', () => { + expect(() => { + AssetManifestSchema.validate({ + version: AssetManifestSchema.currentVersion(), + files: { + asset: { + source: { + path: 3, + }, + destinations: { + dest: { + region: 'us-north-20', + bucketName: 'Bouquet', + objectKey: 'key', + }, + }, + }, }, - destinations: { - dest: { - region: 'us-north-20', - bucketName: 'Bouquet', - objectKey: 'key', + }); + }).toThrow(/Expected a string, got '3'/); + }); + + test('bad "source.packaging" property', () => { + expect(() => { + AssetManifestSchema.validate({ + version: AssetManifestSchema.currentVersion(), + files: { + asset: { + source: { + path: 'a/b/c', + packaging: 'BLACK_HOLE', + }, + destinations: { + dest: { + region: 'us-north-20', + bucketName: 'Bouquet', + objectKey: 'key', + }, + }, }, }, - }, - }, + }); + }).toThrow(/Expected a FileAssetPackaging \(one of [^)]+\), got 'BLACK_HOLE'/); }); - }).toThrow(/Expected a string, got '3'/); + }); }); 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/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/core/lib/construct-compat.ts b/packages/@aws-cdk/core/lib/construct-compat.ts index 5a9315a0850ca..341943a748bca 100644 --- a/packages/@aws-cdk/core/lib/construct-compat.ts +++ b/packages/@aws-cdk/core/lib/construct-compat.ts @@ -91,7 +91,7 @@ export class Construct extends constructs.Construct implements IConstruct { * This method can be implemented by derived constructs in order to perform * validation logic. It is called on all constructs before synthesis. * - * @returns An array of validation error messages, or an empty array if there the construct is valid. + * @returns An array of validation error messages, or an empty array if the construct is valid. */ protected onValidate(): string[] { return this.validate(); @@ -132,7 +132,7 @@ export class Construct extends constructs.Construct implements IConstruct { * This method can be implemented by derived constructs in order to perform * validation logic. It is called on all constructs before synthesis. * - * @returns An array of validation error messages, or an empty array if there the construct is valid. + * @returns An array of validation error messages, or an empty array if the construct is valid. */ protected validate(): string[] { return []; diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts new file mode 100644 index 0000000000000..a1149f91e4990 --- /dev/null +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts @@ -0,0 +1,60 @@ +import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, FileAssetSource } from '../assets'; +import { ISynthesisSession } from '../construct-compat'; +import { addStackArtifactToAssembly, assertBound } from './_shared'; +import { DefaultStackSynthesizer } from './default-synthesizer'; + +/** + * Construction properties of {@link BootstraplessSynthesizer}. + */ +export interface BootstraplessSynthesizerProps { + /** + * The deploy Role ARN to use. + * + * @default - No deploy role (use CLI credentials) + * + */ + readonly deployRoleArn?: string; + + /** + * The CFN execution Role ARN to use. + * + * @default - No CloudFormation role (use CLI credentials) + */ + readonly cloudFormationExecutionRoleArn?: string; +} + +/** + * A special synthesizer that behaves similarly to DefaultStackSynthesizer, + * but doesn't require bootstrapping the environment it operates in. + * Because of that, stacks using it cannot have assets inside of them. + * Used by the CodePipeline construct for the support stacks needed for + * cross-region replication S3 buckets. + */ +export class BootstraplessSynthesizer extends DefaultStackSynthesizer { + constructor(props: BootstraplessSynthesizerProps) { + super({ + deployRoleArn: props.deployRoleArn, + cloudFormationExecutionRole: props.cloudFormationExecutionRoleArn, + }); + } + + public addFileAsset(_asset: FileAssetSource): FileAssetLocation { + throw new Error('Cannot add assets to a Stack that uses the BootstraplessSynthesizer'); + } + + public addDockerImageAsset(_asset: DockerImageAssetSource): DockerImageAssetLocation { + throw new Error('Cannot add assets to a Stack that uses the BootstraplessSynthesizer'); + } + + public synthesizeStackArtifacts(session: ISynthesisSession): void { + assertBound(this.stack); + + // do _not_ treat the template as an asset, + // because this synthesizer doesn't have a bootstrap bucket to put it in + addStackArtifactToAssembly(session, this.stack, { + assumeRoleArn: this.deployRoleArn, + cloudFormationExecutionRoleArn: this.cloudFormationExecutionRoleArn, + requiresBootstrapStackVersion: 1, + }, []); + } +} diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts index 13b65a5d8613c..ace086a9c4bd3 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts @@ -140,11 +140,11 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { */ public static readonly DEFAULT_FILE_ASSETS_BUCKET_NAME = 'cdk-${Qualifier}-assets-${AWS::AccountId}-${AWS::Region}'; - private stack?: Stack; + private _stack?: Stack; private bucketName?: string; private repositoryName?: string; - private deployRoleArn?: string; - private cloudFormationExecutionRoleArn?: string; + private _deployRoleArn?: string; + private _cloudFormationExecutionRoleArn?: string; private assetPublishingRoleArn?: string; private readonly files: NonNullable = {}; @@ -154,7 +154,7 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { } public bind(stack: Stack): void { - this.stack = stack; + this._stack = stack; const qualifier = this.props.qualifier ?? stack.node.tryGetContext(BOOTSTRAP_QUALIFIER_CONTEXT) ?? DefaultStackSynthesizer.DEFAULT_QUALIFIER; @@ -176,8 +176,8 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { // tslint:disable:max-line-length this.bucketName = specialize(this.props.fileAssetsBucketName ?? DefaultStackSynthesizer.DEFAULT_FILE_ASSETS_BUCKET_NAME); this.repositoryName = specialize(this.props.imageAssetsRepositoryName ?? DefaultStackSynthesizer.DEFAULT_IMAGE_ASSETS_REPOSITORY_NAME); - this.deployRoleArn = specialize(this.props.deployRoleArn ?? DefaultStackSynthesizer.DEFAULT_DEPLOY_ROLE_ARN); - this.cloudFormationExecutionRoleArn = specialize(this.props.cloudFormationExecutionRole ?? DefaultStackSynthesizer.DEFAULT_CLOUDFORMATION_ROLE_ARN); + this._deployRoleArn = specialize(this.props.deployRoleArn ?? DefaultStackSynthesizer.DEFAULT_DEPLOY_ROLE_ARN); + this._cloudFormationExecutionRoleArn = specialize(this.props.cloudFormationExecutionRole ?? DefaultStackSynthesizer.DEFAULT_CLOUDFORMATION_ROLE_ARN); this.assetPublishingRoleArn = specialize(this.props.assetPublishingRoleArn ?? DefaultStackSynthesizer.DEFAULT_ASSET_PUBLISHING_ROLE_ARN); // tslint:enable:max-line-length } @@ -259,13 +259,37 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { const artifactId = this.writeAssetManifest(session); addStackArtifactToAssembly(session, this.stack, { - assumeRoleArn: this.deployRoleArn, - cloudFormationExecutionRoleArn: this.cloudFormationExecutionRoleArn, + assumeRoleArn: this._deployRoleArn, + cloudFormationExecutionRoleArn: this._cloudFormationExecutionRoleArn, stackTemplateAssetObjectUrl: templateManifestUrl, requiresBootstrapStackVersion: 1, }, [artifactId]); } + /** + * Returns the ARN of the deploy Role. + */ + public get deployRoleArn(): string { + if (!this._deployRoleArn) { + throw new Error('deployRoleArn getter can only be called after the synthesizer has been bound to a Stack'); + } + return this._deployRoleArn; + } + + /** + * Returns the ARN of the CFN execution Role. + */ + public get cloudFormationExecutionRoleArn(): string { + if (!this._cloudFormationExecutionRoleArn) { + throw new Error('cloudFormationExecutionRoleArn getter can only be called after the synthesizer has been bound to a Stack'); + } + return this._cloudFormationExecutionRoleArn; + } + + protected get stack(): Stack | undefined { + return this._stack; + } + /** * Add the stack's template as one of the manifest assets * diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/index.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/index.ts index 5920f19bae2c9..b4ad67384729d 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/index.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/index.ts @@ -1,4 +1,5 @@ export * from './types'; export * from './default-synthesizer'; export * from './legacy'; -export * from './nested'; \ No newline at end of file +export * from './bootstrapless-synthesizer'; +export * from './nested'; 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/@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/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index c32f75234584e..37c5a0887d52c 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -21,6 +21,19 @@ import * as version from '../lib/version'; // tslint:disable:no-shadowed-variable max-line-length async function parseCommandLineArguments() { + // Use the following configuration for array arguments: + // + // { type: 'array', default: [], nargs: 1, requiresArg: true } + // + // The default behavior of yargs is to eat all strings following an array argument: + // + // ./prog --arg one two positional => will parse to { arg: ['one', 'two', 'positional'], _: [] } (so no positional arguments) + // ./prog --arg one two -- positional => does not help, for reasons that I can't understand. Still gets parsed incorrectly. + // + // By using the config above, every --arg will only consume one argument, so you can do the following: + // + // ./prog --arg one --arg two position => will parse to { arg: ['one', 'two'], _: ['positional'] }. + const initTemplateLanuages = await availableInitLanguages; return yargs .env('CDK') @@ -56,8 +69,8 @@ async function parseCommandLineArguments() { .option('qualifier', { type: 'string', desc: 'Unique string to distinguish multiple bootstrap stacks', default: undefined }) .option('tags', { type: 'array', alias: 't', desc: 'Tags to add for the stack (KEY=VALUE)', nargs: 1, requiresArg: true, default: [] }) .option('execute', {type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true}) - .option('trust', { type: 'array', desc: 'The (space-separated) list of AWS account IDs that should be trusted to perform deployments into this environment', default: [], hidden: true }) - .option('cloudformation-execution-policies', { type: 'array', desc: 'The (space-separated) list of Managed Policy ARNs that should be attached to the role performing deployments into this environment. Required if --trust was passed', default: [], hidden: true }) + .option('trust', { type: 'array', desc: 'The AWS account IDs that should be trusted to perform deployments into this environment (may be repeated)', default: [], nargs: 1, requiresArg: true, hidden: true }) + .option('cloudformation-execution-policies', { type: 'array', desc: 'The Managed Policy ARNs that should be attached to the role performing deployments into this environment. Required if --trust was passed (may be repeated)', default: [], nargs: 1, requiresArg: true, hidden: true }) .option('force', { alias: 'f', type: 'boolean', desc: 'Always bootstrap even if it would downgrade template version', default: false }), ) .command('deploy [STACKS..]', 'Deploys the stack(s) named STACKS into your AWS account', yargs => yargs @@ -296,6 +309,8 @@ initCommandLine() }) .catch(err => { error(err.message); - debug(err.stack); + if (err.stack) { + debug(err.stack); + } process.exitCode = 1; }); diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts b/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts index 98c2e03ac1a62..499a5af46beb0 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts @@ -127,7 +127,7 @@ export class SdkProvider { * If `region` is undefined, the default value will be used. */ public async withAssumedRole(roleArn: string, externalId: string | undefined, region: string | undefined) { - debug(`Assuming role '${roleArn}'`); + debug(`Assuming role '${roleArn}'.`); region = region ?? this.defaultRegion; const creds = new AWS.ChainableTemporaryCredentials({ diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk.ts b/packages/aws-cdk/lib/api/aws-auth/sdk.ts index d84b14627cba4..239f85fef51bc 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk.ts @@ -64,27 +64,27 @@ export class SDK implements ISDK { } public cloudFormation(): AWS.CloudFormation { - return new AWS.CloudFormation(this.config); + return wrapServiceErrorHandling(new AWS.CloudFormation(this.config)); } public ec2(): AWS.EC2 { - return new AWS.EC2(this.config); + return wrapServiceErrorHandling(new AWS.EC2(this.config)); } public ssm(): AWS.SSM { - return new AWS.SSM(this.config); + return wrapServiceErrorHandling(new AWS.SSM(this.config)); } public s3(): AWS.S3 { - return new AWS.S3(this.config); + return wrapServiceErrorHandling(new AWS.S3(this.config)); } public route53(): AWS.Route53 { - return new AWS.Route53(this.config); + return wrapServiceErrorHandling(new AWS.Route53(this.config)); } public ecr(): AWS.ECR { - return new AWS.ECR(this.config); + return wrapServiceErrorHandling(new AWS.ECR(this.config)); } public async currentAccount(): Promise { @@ -103,4 +103,113 @@ export class SDK implements ISDK { } } +/** + * Return a wrapping object for the underlying service object + * + * Responds to failures in the underlying service calls, in two different + * ways: + * + * - When errors are encountered, log the failing call and the error that + * it triggered (at debug level). This is necessary because the lack of + * stack traces in NodeJS otherwise makes it very hard to suss out where + * a certain AWS error occurred. + * - The JS SDK has a funny business of wrapping any credential-based error + * in a super-generic (and in our case wrong) exception. If we then use a + * 'ChainableTemporaryCredentials' and the target role doesn't exist, + * the error message that shows up by default is super misleading + * (https://github.com/aws/aws-sdk-js/issues/3272). We can fix this because + * the exception contains the "inner exception", so we unwrap and throw + * the correct error ("cannot assume role"). + * + * The wrapping business below is slightly more complicated than you'd think + * because we must hook into the `promise()` method of the object that's being + * returned from the methods of the object that we wrap, so there's two + * levels of wrapping going on, and also some exceptions to the wrapping magic. + */ +function wrapServiceErrorHandling(serviceObject: A): A { + const classObject = serviceObject.constructor.prototype; + + return new Proxy(serviceObject, { + get(obj: A, prop: string) { + const real = (obj as any)[prop]; + // Things we don't want to intercept: + // - Anything that's not a function. + // - 'constructor', s3.upload() will use this to do some magic and we need the underlying constructor. + // - Any method that's not on the service class (do not intercept 'makeRequest' and other helpers). + if (prop === 'constructor' || !classObject.hasOwnProperty(prop) || !isFunction(real)) { return real; } + + // NOTE: This must be a function() and not an () => { + // because I need 'this' to be dynamically bound and not statically bound. + // If your linter complains don't listen to it! + return function(this: any) { + // Call the underlying function. If it returns an object with a promise() + // method on it, wrap that 'promise' method. + const args = [].slice.call(arguments, 0); + const response = real.apply(this, args); + + // Don't intercept unless the return value is an object with a '.promise()' method. + if (typeof response !== 'object' || !response) { return response; } + if (!('promise' in response)) { return response; } + + // Return an object with the promise method replaced with a wrapper which will + // do additional things to errors. + return Object.assign(Object.create(response), { + promise() { + return response.promise().catch((e: Error) => { + e = makeDetailedException(e); + debug(`Call failed: ${prop}(${JSON.stringify(args[0])}) => ${e.message}`); + return Promise.reject(e); // Re-'throw' the new error + }); + }, + }); + }; + }, + }); +} + const CURRENT_ACCOUNT_KEY = Symbol('current_account_key'); + +function isFunction(x: any): x is (...args: any[]) => any { + return x && {}.toString.call(x) === '[object Function]'; +} + +/** + * Extract a more detailed error out of a generic error if we can + */ +function makeDetailedException(e: Error): Error { + // This is the super-generic "something's wrong" error that the JS SDK wraps other errors in. + // https://github.com/aws/aws-sdk-js/blob/f0ac2e53457c7512883d0677013eacaad6cd8a19/lib/event_listeners.js#L84 + if (typeof e.message === 'string' && e.message.startsWith('Missing credentials in config')) { + const original = (e as any).originalError; + if (original) { + // When the SDK does a 'util.copy', they lose the Error-ness of the inner error + // (they copy the Error's properties into a plain object) so make it an Error object again. + e = Object.assign(new Error(), original); + } + } + + // At this point, the error might still be a generic "ChainableTemporaryCredentials failed" + // error which wraps the REAL error (AssumeRole failed). We're going to replace the error + // message with one that's more likely to help users, and tell them the most probable + // fix (bootstrapping). The underlying service call failure will be appended below. + if (e.message === 'Could not load credentials from ChainableTemporaryCredentials') { + e.message = 'Could not assume role in target account (did you bootstrap the environment with the right \'--trust\'s?)'; + } + + // Replace the message on this error with a concatenation of all inner error messages. + // Must more clear what's going on that way. + e.message = allChainedExceptionMessages(e); + return e; +} + +/** + * Return the concatenated message of all exceptions in the AWS exception chain + */ +function allChainedExceptionMessages(e: Error | undefined) { + const ret = new Array(); + while (e) { + ret.push(e.message); + e = (e as any).originalError; + } + return ret.join(': '); +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index 952b9c9b2374d..99b18c3136b9f 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -173,6 +173,19 @@ export async function deployStack(options: DeployStackOptions): Promise { Changes: [], })), executeChangeSet: jest.fn((_o) => ({})), + deleteStack: jest.fn((_o) => ({})), getTemplate: jest.fn((_o) => ({ TemplateBody: JSON.stringify(DEFAULT_FAKE_TEMPLATE) })), updateTerminationProtection: jest.fn((_o) => ({ StackId: 'stack-id' })), }; @@ -190,6 +191,55 @@ test('deploy is skipped if template did not change', async () => { expect(cfnMocks.executeChangeSet).not.toBeCalled(); }); +test('if existing stack failed to create, it is deleted and recreated', async () => { + // GIVEN + givenStackExists( + { StackStatus: 'ROLLBACK_COMPLETE' }, // This is for the initial check + { StackStatus: 'DELETE_COMPLETE' }, // Poll the successful deletion + { StackStatus: 'CREATE_COMPLETE' }, // Poll the recreation + ); + givenTemplateIs({ + DifferentThan: 'TheDefault', + }); + + // WHEN + await deployStack({ + stack: FAKE_STACK, + sdk, + sdkProvider, + resolvedEnvironment: mockResolvedEnvironment(), + }); + + // THEN + expect(cfnMocks.deleteStack).toHaveBeenCalled(); + expect(cfnMocks.createChangeSet).toHaveBeenCalledWith(expect.objectContaining({ + ChangeSetType: 'CREATE', + })); +}); + +test('if existing stack failed to create, it is deleted and recreated even if the template did not change', async () => { + // GIVEN + givenStackExists( + { StackStatus: 'ROLLBACK_COMPLETE' }, // This is for the initial check + { StackStatus: 'DELETE_COMPLETE' }, // Poll the successful deletion + { StackStatus: 'CREATE_COMPLETE' }, // Poll the recreation + ); + + // WHEN + await deployStack({ + stack: FAKE_STACK, + sdk, + sdkProvider, + resolvedEnvironment: mockResolvedEnvironment(), + }); + + // THEN + expect(cfnMocks.deleteStack).toHaveBeenCalled(); + expect(cfnMocks.createChangeSet).toHaveBeenCalledWith(expect.objectContaining({ + ChangeSetType: 'CREATE', + })); +}); + test('deploy not skipped if template did not change and --force is applied', async () => { // GIVEN givenStackExists(); @@ -296,10 +346,7 @@ test('deploy not skipped if template did not change but one tag removed', async test('deploy not skipped if template changed', async () => { // GIVEN givenStackExists(); - cfnMocks.getTemplate!.mockReset(); - cfnMocks.getTemplate!.mockReturnValue({ - TemplateBody: JSON.stringify({ changed: 123 }), - }); + givenTemplateIs({ changed: 123 }); // WHEN await deployStack({ @@ -476,19 +523,37 @@ test('updateTerminationProtection called when termination protection is undefine /** * Set up the mocks so that it looks like the stack exists to start with + * + * The last element of this array will be continuously repeated. */ -function givenStackExists(overrides: Partial = {}) { +function givenStackExists(...overrides: Array>) { cfnMocks.describeStacks!.mockReset(); + + if (overrides.length === 0) { + overrides = [{}]; + } + + const baseResponse = { + StackName: 'mock-stack-name', + StackId: 'mock-stack-id', + CreationTime: new Date(), + StackStatus: 'CREATE_COMPLETE', + EnableTerminationProtection: false, + }; + + for (const override of overrides.slice(0, overrides.length - 1)) { + cfnMocks.describeStacks!.mockImplementationOnce(() => ({ + Stacks: [ {...baseResponse, ...override }], + })); + } cfnMocks.describeStacks!.mockImplementation(() => ({ - Stacks: [ - { - StackName: 'mock-stack-name', - StackId: 'mock-stack-id', - CreationTime: new Date(), - StackStatus: 'CREATE_COMPLETE', - EnableTerminationProtection: false, - ...overrides, - }, - ], + Stacks: [ {...baseResponse, ...overrides[overrides.length - 1] }], })); } + +function givenTemplateIs(template: any) { + cfnMocks.getTemplate!.mockReset(); + cfnMocks.getTemplate!.mockReturnValue({ + TemplateBody: JSON.stringify(template), + }); +} \ No newline at end of file diff --git a/packages/aws-cdk/test/api/sdk-provider.test.ts b/packages/aws-cdk/test/api/sdk-provider.test.ts index 21bb61dfbb9ed..6e6a4f91511c4 100644 --- a/packages/aws-cdk/test/api/sdk-provider.test.ts +++ b/packages/aws-cdk/test/api/sdk-provider.test.ts @@ -177,7 +177,6 @@ describe('CLI compatible credentials loading', () => { // WHEN const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions, - ec2creds: false, profile: 'assumable', httpOptions: { proxyAddress: 'http://DOESNTMATTER/', @@ -189,6 +188,32 @@ describe('CLI compatible credentials loading', () => { // THEN -- the fake proxy agent got called, we don't care about the result expect(called).toEqual(true); }); + + test('error we get from assuming a role is useful', async () => { + // GIVEN + // Because of the way ChainableTemporaryCredentials gets its STS client, it's not mockable + // using 'mock-aws-sdk'. So instead, we have to mess around with its internals. + function makeAssumeRoleFail(s: ISDK) { + (s as any).credentials.service.assumeRole = jest.fn().mockImplementation((_request, cb) => { + cb(new Error('Nope!')); + }); + } + + const provider = await SdkProvider.withAwsCliCompatibleDefaults({ + ...defaultCredOptions, + httpOptions: { + proxyAddress: 'http://localhost:8080/', + }, + }); + + // WHEN + const sdk = await provider.withAssumedRole('bla.role.arn', undefined, undefined); + makeAssumeRoleFail(sdk); + + // THEN - error message contains both a helpful hint and the underlying AssumeRole message + await expect(sdk.s3().listBuckets().promise()).rejects.toThrow('did you bootstrap'); + await expect(sdk.s3().listBuckets().promise()).rejects.toThrow('Nope!'); + }); }); describe('Plugins', () => { 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..acfe1041e24b5 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.1", + "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 86188e2ef0d19..09a93983b382d 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": "^2.34.0", + "@typescript-eslint/eslint-plugin": "^3.0.2", "@typescript-eslint/parser": "^2.19.2", "awslint": "0.0.0", "colors": "^1.4.0", diff --git a/yarn.lock b/yarn.lock index 8b2d06e54f111..657b0b9087378 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" @@ -1641,14 +1658,15 @@ 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@^2.34.0": - version "2.34.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz#6f8ce8a46c7dea4a6f1d171d2bb8fbae6dac2be9" - integrity sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ== +"@typescript-eslint/eslint-plugin@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.0.2.tgz#4a114a066e2f9659b25682ee59d4866e15a17ec3" + integrity sha512-ER3bSS/A/pKQT/hjMGCK8UQzlL0yLjuCZ/G8CDFJFVTfl3X65fvq2lNYqOG8JPTfrPa2RULCdwfOyFjZEMNExQ== dependencies: - "@typescript-eslint/experimental-utils" "2.34.0" + "@typescript-eslint/experimental-utils" "3.0.2" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" + semver "^7.3.2" tsutils "^3.17.1" "@typescript-eslint/experimental-utils@2.28.0": @@ -1661,13 +1679,13 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/experimental-utils@2.34.0": - version "2.34.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz#d3524b644cdb40eebceca67f8cf3e4cc9c8f980f" - integrity sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA== +"@typescript-eslint/experimental-utils@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.0.2.tgz#bb2131baede8df28ec5eacfa540308ca895e5fee" + integrity sha512-4Wc4EczvoY183SSEnKgqAfkj1eLtRgBQ04AAeG+m4RhTVyaazxc1uI8IHf0qLmu7xXe9j1nn+UoDJjbmGmuqXQ== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.34.0" + "@typescript-eslint/typescript-estree" "3.0.2" eslint-scope "^5.0.0" eslint-utils "^2.0.0" @@ -1694,10 +1712,10 @@ semver "^6.3.0" tsutils "^3.17.1" -"@typescript-eslint/typescript-estree@2.34.0": - version "2.34.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz#14aeb6353b39ef0732cc7f1b8285294937cf37d5" - integrity sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg== +"@typescript-eslint/typescript-estree@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.0.2.tgz#67a1ce4307ebaea43443fbf3f3be7e2627157293" + integrity sha512-cs84mxgC9zQ6viV8MEcigfIKQmKtBkZNDYf8Gru2M+MhnA6z9q0NFMZm2IEzKqAwN8lY5mFVd1Z8DiHj6zQ3Tw== dependencies: debug "^4.1.1" eslint-visitor-keys "^1.1.0" @@ -8654,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== @@ -9456,6 +9474,17 @@ ts-node@^8.0.2: source-map-support "^0.5.6" yn "3.1.1" +ts-node@^8.10.1: + version "8.10.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.1.tgz#77da0366ff8afbe733596361d2df9a60fc9c9bd3" + integrity sha512-bdNz1L4ekHiJul6SHtZWs1ujEKERJnHs4HxN7rjTyyVOFf3HaJ6sLqe6aPG62XTzAB/63pKRh5jTSWL0D7bsvw== + dependencies: + arg "^4.1.0" + diff "^4.0.1" + make-error "^1.1.1" + source-map-support "^0.5.17" + yn "3.1.1" + tsame@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/tsame/-/tsame-2.0.1.tgz#70410ddbefcd29c61e2d68549b3347b0444d613f" @@ -9729,10 +9758,10 @@ uuid@^3.0.1, uuid@^3.3.2, uuid@^3.3.3: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c" - integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== +uuid@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d" + integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg== v8-compile-cache@^2.0.3: version "2.1.0"