diff --git a/.github/workflows/auto-approve.yml b/.github/workflows/auto-approve.yml index 81bd372dfb3ed..cf2ed7e21ca69 100644 --- a/.github/workflows/auto-approve.yml +++ b/.github/workflows/auto-approve.yml @@ -6,9 +6,9 @@ on: pull_request jobs: auto-approve: if: > - contains(github.event.pull_request.labels.*.name, 'pr/auto-approve') && - (github.event.pull_request.user.login == 'aws-cdk-automation' - || github.event.pull_request.user.login == 'dependabot[bot]' + contains(github.event.pull_request.labels.*.name, 'pr/auto-approve') && + (github.event.pull_request.user.login == 'aws-cdk-automation' + || github.event.pull_request.user.login == 'dependabot[bot]' || github.event.pull_request.user.login == 'dependabot-preview[bot]') runs-on: ubuntu-latest steps: diff --git a/.github/workflows/issue-label-assign.yml b/.github/workflows/issue-label-assign.yml index 22f20538795c7..01afcd841138d 100644 --- a/.github/workflows/issue-label-assign.yml +++ b/.github/workflows/issue-label-assign.yml @@ -54,8 +54,8 @@ jobs: {"keywords":["(@aws-cdk/aws-cloudfront)","(aws-cloudfront)","(cloudfront)","(cloud front)"],"labels":["@aws-cdk/aws-cloudfront"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-cloudfront-origins)","(aws-cloudfront-origins)","(cloudfront-origins)","(cloudfront origins)"],"labels":["@aws-cdk/aws-cloudfront-origins"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-cloudtrail)","(aws-cloudtrail)","(cloudtrail)","(cloud trail)"],"labels":["@aws-cdk/aws-cloudtrail"],"assignees":["NetaNir"]}, - {"keywords":["(@aws-cdk/aws-cloudwatch)","(aws-cloudwatch)","(cloudwatch)","(cloud watch)"],"labels":["@aws-cdk/aws-cloudwatch"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-cloudwatch-actions)","(aws-cloudwatch-actions)","(cloudwatch-actions)","(cloudwatch actions)"],"labels":["@aws-cdk/aws-cloudwatch-actions"],"assignees":["rix0rrr"]}, + {"keywords":["(@aws-cdk/aws-cloudwatch)","(aws-cloudwatch)","(cloudwatch)","(cloud watch)"],"labels":["@aws-cdk/aws-cloudwatch"],"assignees":["NetaNir"]}, + {"keywords":["(@aws-cdk/aws-cloudwatch-actions)","(aws-cloudwatch-actions)","(cloudwatch-actions)","(cloudwatch actions)"],"labels":["@aws-cdk/aws-cloudwatch-actions"],"assignees":["NetaNir"]}, {"keywords":["(@aws-cdk/aws-codeartifact)","(aws-codeartifact)","(codeartifact)","(code artifact)","(code-artifact)"],"labels":["@aws-cdk/aws-codeartifact"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-codebuild)","(aws-codebuild)","(codebuild)","(code build)","(code-build)"],"labels":["@aws-cdk/aws-codebuild"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-codecommit)","(aws-codecommit)","(codecommit)","(code commit)", "(code-commit)"],"labels":["@aws-cdk/aws-codecommit"],"assignees":["skinny85"]}, @@ -76,8 +76,8 @@ jobs: {"keywords":["(@aws-cdk/aws-dlm)","(aws-dlm)","(dlm)"],"labels":["@aws-cdk/aws-dlm"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-dms)","(aws-dms)","(dms)"],"labels":["@aws-cdk/aws-dms"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-docdb)","(aws-docdb)","(docdb)","(doc db)","(doc-db)"],"labels":["@aws-cdk/aws-docdb"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-dynamodb)","(aws-dynamodb)","(dynamodb)","(dynamo db)","(dynamo-db)"],"labels":["@aws-cdk/aws-dynamodb"],"assignees":["skinny85"]}, - {"keywords":["(@aws-cdk/aws-dynamodb-global)","(aws-dynamodb-global)","(dynamodb-global)","(dynamodb global)"],"labels":["@aws-cdk/aws-dynamodb-global"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-dynamodb)","(aws-dynamodb)","(dynamodb)","(dynamo db)","(dynamo-db)"],"labels":["@aws-cdk/aws-dynamodb"],"assignees":["RomainMuller"]}, + {"keywords":["(@aws-cdk/aws-dynamodb-global)","(aws-dynamodb-global)","(dynamodb-global)","(dynamodb global)"],"labels":["@aws-cdk/aws-dynamodb-global"],"assignees":["RomainMuller"]}, {"keywords":["(@aws-cdk/aws-ec2)","(aws-ec2)","(ec2)","(vpc)"],"labels":["@aws-cdk/aws-ec2"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-ecr)","(aws-ecr)","(ecr)"],"labels":["@aws-cdk/aws-ecr"],"assignees":["MrArnoldPalmer"]}, {"keywords":["(@aws-cdk/aws-ecr-assets)","(aws-ecr-assets)","(ecr-assets)","(ecr assets)","(ecrassets)"],"labels":["@aws-cdk/aws-ecr-assets"],"assignees":["eladb"]}, diff --git a/CHANGELOG.md b/CHANGELOG.md index f8f6aec305888..7dd8c2c1991b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,34 @@ 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.91.0](https://github.com/aws/aws-cdk/compare/v1.90.1...v1.91.0) (2021-02-23) + + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **aws-appsync:** RdsDataSource now takes a ServerlessCluster instead of a DatabaseCluster +* **aws-appsync:** graphqlapi.addRdsDataSource now takes databaseName as its fourth argument + +### Features + +* **aws-appsync:** add databaseName to rdsDataSource ([#12575](https://github.com/aws/aws-cdk/issues/12575)) ([f92b65e](https://github.com/aws/aws-cdk/commit/f92b65e2a158f918d8f05132ed12a4bb85228997)), closes [#12572](https://github.com/aws/aws-cdk/issues/12572) +* **cfnspec:** cloudformation spec v28.0.0 ([#13101](https://github.com/aws/aws-cdk/issues/13101)) ([13c9859](https://github.com/aws/aws-cdk/commit/13c9859cc62b3d472ba1be84b12d478f61f02ec9)) +* **ecs-patterns:** Add support for assignPublicIp for QueueProcessingFargateService ([#13122](https://github.com/aws/aws-cdk/issues/13122)) ([3fb4600](https://github.com/aws/aws-cdk/commit/3fb46001a7345cbefa6df70893999bcb304ed40d)), closes [#12815](https://github.com/aws/aws-cdk/issues/12815) +* **stepfunctions-tasks:** add EKS call to SFN-tasks ([#12779](https://github.com/aws/aws-cdk/issues/12779)) ([296a10d](https://github.com/aws/aws-cdk/commit/296a10d76a9f6fc2a374d1a6461c460bcc3eeb79)) +* **synthetics:** Update CloudWatch Synthetics NodeJS runtimes ([#12907](https://github.com/aws/aws-cdk/issues/12907)) ([6aac3b6](https://github.com/aws/aws-cdk/commit/6aac3b6a9bb1586ee16e7a85ca657b544d0f8304)), closes [#12906](https://github.com/aws/aws-cdk/issues/12906) + + +### Bug Fixes + +* UserPool, Volume, ElasticSearch, FSx are now RETAIN by default ([#12920](https://github.com/aws/aws-cdk/issues/12920)) ([5a54741](https://github.com/aws/aws-cdk/commit/5a54741a414d3f8b7913163f4785759b984b41d8)), closes [#12563](https://github.com/aws/aws-cdk/issues/12563) +* **appsync:** revert to allow resolver creation from data source ([#12973](https://github.com/aws/aws-cdk/issues/12973)) ([d35f032](https://github.com/aws/aws-cdk/commit/d35f03226d6d7fb5be246b4d3584ee9205b0ef2d)), closes [#12635](https://github.com/aws/aws-cdk/issues/12635) [#11522](https://github.com/aws/aws-cdk/issues/11522) +* **aws-appsync:** use serverlessCluster on rdsDataSource ([#13206](https://github.com/aws/aws-cdk/issues/13206)) ([45cf387](https://github.com/aws/aws-cdk/commit/45cf3873fb48d4043e7a22284d36695ea6bde6ef)), closes [#12567](https://github.com/aws/aws-cdk/issues/12567) +* **cfn-diff:** handle Fn::If inside policies and statements ([#12975](https://github.com/aws/aws-cdk/issues/12975)) ([daf4e47](https://github.com/aws/aws-cdk/commit/daf4e47a790ab99639e471f6792f22e3e4f8ee73)), closes [#12887](https://github.com/aws/aws-cdk/issues/12887) +* **core:** ENOTDIR invalid cwd on "cdk deploy" ([#13145](https://github.com/aws/aws-cdk/issues/13145)) ([cd7a3ed](https://github.com/aws/aws-cdk/commit/cd7a3ed333570a3b26446e1e3a054ca886cd3906)), closes [#12258](https://github.com/aws/aws-cdk/issues/12258) [#13076](https://github.com/aws/aws-cdk/issues/13076) [#13131](https://github.com/aws/aws-cdk/issues/13131) +* **eks:** `KubectlProvider` creates un-necessary security group ([#13178](https://github.com/aws/aws-cdk/issues/13178)) ([c5e8b6d](https://github.com/aws/aws-cdk/commit/c5e8b6df1e5f0359d51d025edcc68508ab5daef1)) +* **eks:** Deployment fails for the first deployment in an account ([#13103](https://github.com/aws/aws-cdk/issues/13103)) ([e042879](https://github.com/aws/aws-cdk/commit/e042879851f8ddd558d20941019c9a6692a1c2bf)), closes [/github.com/aws/aws-cdk/issues/9027#issuecomment-780482124](https://github.com/aws//github.com/aws/aws-cdk/issues/9027/issues/issuecomment-780482124) +* **lambda-nodejs:** invalid sample in documentation ([#12404](https://github.com/aws/aws-cdk/issues/12404)) ([520c263](https://github.com/aws/aws-cdk/commit/520c263ca3c6b0ea7d9c09c23e509a3373ee2b8a)) + ## [1.90.1](https://github.com/aws/aws-cdk/compare/v1.90.0...v1.90.1) (2021-02-19) ### Bug Fixes @@ -73,7 +101,7 @@ All notable changes to this project will be documented in this file. See [standa ### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES -* **appmesh:** the properties virtualRouter and virtualNode of VirtualServiceProps have been replaced with the union-like class VirtualServiceProvider +* **appmesh:** the properties virtualRouter and virtualNode of VirtualServiceProps have been replaced with the union-like class VirtualServiceProvider * **appmesh**: the method `addVirtualService` has been removed from `IMesh` * **cloudfront:** experimental EdgeFunction stack names have changed from 'edge-lambda-stack-${region}' to 'edge-lambda-stack-${stackid}' to support multiple independent CloudFront distributions with EdgeFunctions. diff --git a/DESIGN_GUIDELINES.md b/DESIGN_GUIDELINES.md index e6cebe9f434c6..20fd5be016870 100644 --- a/DESIGN_GUIDELINES.md +++ b/DESIGN_GUIDELINES.md @@ -14,7 +14,7 @@ APIs and verifies that the APIs adhere to the guidelines. When a guideline is backed by a linter rule, the rule name will be referenced like this: _[awslint:resource-class-is-construct]_. -For the purpose of this document we will use "Foo" to denote the official name +For the purpose of this document, we will use "Foo" to denote the official name of the resource as defined in the AWS CloudFormation resource specification (i.e. "Bucket", "Queue", "Topic", etc). This notation allows deriving names from the official name. For example, `FooProps` would be `BucketProps`, `TopicProps`, @@ -98,8 +98,8 @@ or abstractions. However, you will notice that some sections explicitly call out guidelines that apply only to AWS resources (and in many cases enforced/implemented by the **Resource** base class). -AWS services are modeled around the concept of *resources*. Service normally -expose through their APIs one or more resources, which can be provisioned +AWS services are modeled around the concept of *resources*. Services normally +expose one or more resources through their APIs, which can be provisioned through the APIs control plane or through AWS CloudFormation. Every resource available in the AWS platform will have a corresponding resource @@ -397,12 +397,12 @@ For example, prefer “readCapacity” versus “readCapacityUnits”. We prefer the terminology used by the official AWS service documentation over new terminology, even if you think it's not ideal. It helps users diagnose issues and map the mental model of the construct to the service APIs, -documentation and examples. For example don't be tempted to change SQS's +documentation and examples. For example, don't be tempted to change SQS's **dataKeyReusePeriod** with **keyRotation** because it will be hard for people to diagnose problems. They won't be able to just search for “sqs dataKeyReuse” and find topics on it. -> We can relax this guidelines when this is about generic terms (like +> We can relax this guideline when this is about generic terms (like `httpStatus` instead of `statusCode`). The important semantics to preserve are for *service features*: I wouldn't want to rename "lambda layers" to "lambda dependencies" just because it makes more sense because then users won't be @@ -697,8 +697,8 @@ _[awslint:from-signature]_: #### “from” Methods Resource constructs should export static “from” methods for importing unowned -resources given one more of its physical attributes such as ARN, name, etc. All -constructs should have at least one "fromXxx" method _[awslint:from-method]_: +resources given one or more of its physical attributes such as ARN, name, etc. All +constructs should have at least one `fromXxx` method _[awslint:from-method]_: ```ts static fromFooArn(scope: Construct, id: string, bucketArn: string): IFoo; @@ -870,7 +870,7 @@ vpcSubnetSelection?: ec2.SubnetSelection; ### Grants -Grants are one of the most powerful concept in the AWS Construct Library. They +Grants are one of the most powerful concepts in the AWS Construct Library. They offer a higher level, intent-based, API for managing IAM permissions for AWS resources. @@ -974,7 +974,7 @@ class Function extends Resource implements IFunction { ### Events -Many AWS resource emit events to the CloudWatch event bus. Such resources should +Many AWS resources emit events to the CloudWatch event bus. Such resources should have a set of “onXxx” methods available on their construct interface _[awslint:events-in-interface]_. diff --git a/allowed-breaking-changes.txt b/allowed-breaking-changes.txt index 964bb4d5c7712..2ca2ca5b6067f 100644 --- a/allowed-breaking-changes.txt +++ b/allowed-breaking-changes.txt @@ -56,10 +56,3 @@ incompatible-argument:@aws-cdk/aws-ecs.TaskDefinition.addVolume # We made properties optional and it's really fine but our differ doesn't think so. weakened:@aws-cdk/cloud-assembly-schema.DockerImageSource weakened:@aws-cdk/cloud-assembly-schema.FileSource - -# https://github.com/aws/aws-cdk/pull/13145 -removed:@aws-cdk/core.AssetStaging.isArchive -removed:@aws-cdk/core.AssetStaging.packaging -removed:@aws-cdk/core.BundlingOutput -removed:@aws-cdk/core.BundlingOptions.outputType - diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.all-service-addons.expected.json b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.all-service-addons.expected.json index f9d42c3927b60..7ef708ccc31aa 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.all-service-addons.expected.json +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.all-service-addons.expected.json @@ -3491,4 +3491,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.assign-public-ip.expected.json b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.assign-public-ip.expected.json index 6b7a3f0e42086..0f176c27b6ab7 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.assign-public-ip.expected.json +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.assign-public-ip.expected.json @@ -1111,56 +1111,36 @@ ] }, "Create": { - "service": "SQS", - "action": "sendMessage", - "parameters": { - "QueueUrl": { - "Ref": "nameserviceTaskRecordManagerEventsQueueF805A6C1" - }, - "DelaySeconds": 10, - "MessageBody": "{ \"prime\": true }", - "MessageAttributes": { - "HostedZoneId": { - "DataType": "String", - "StringValue": { - "Ref": "zoneEB40FF1E" - } + "Fn::Join": [ + "", + [ + "{\"service\":\"SQS\",\"action\":\"sendMessage\",\"parameters\":{\"QueueUrl\":\"", + { + "Ref": "nameserviceTaskRecordManagerEventsQueueF805A6C1" }, - "RecordName": { - "DataType": "String", - "StringValue": "test-record" - } - } - }, - "physicalResourceId": { - "responsePath": "MessageId" - } + "\",\"DelaySeconds\":10,\"MessageBody\":\"{ \\\"prime\\\": true }\",\"MessageAttributes\":{\"HostedZoneId\":{\"DataType\":\"String\",\"StringValue\":\"", + { + "Ref": "zoneEB40FF1E" + }, + "\"},\"RecordName\":{\"DataType\":\"String\",\"StringValue\":\"test-record\"}}},\"physicalResourceId\":{\"responsePath\":\"MessageId\"}}" + ] + ] }, "Update": { - "service": "SQS", - "action": "sendMessage", - "parameters": { - "QueueUrl": { - "Ref": "nameserviceTaskRecordManagerEventsQueueF805A6C1" - }, - "DelaySeconds": 10, - "MessageBody": "{ \"prime\": true }", - "MessageAttributes": { - "HostedZoneId": { - "DataType": "String", - "StringValue": { - "Ref": "zoneEB40FF1E" - } + "Fn::Join": [ + "", + [ + "{\"service\":\"SQS\",\"action\":\"sendMessage\",\"parameters\":{\"QueueUrl\":\"", + { + "Ref": "nameserviceTaskRecordManagerEventsQueueF805A6C1" }, - "RecordName": { - "DataType": "String", - "StringValue": "test-record" - } - } - }, - "physicalResourceId": { - "responsePath": "MessageId" - } + "\",\"DelaySeconds\":10,\"MessageBody\":\"{ \\\"prime\\\": true }\",\"MessageAttributes\":{\"HostedZoneId\":{\"DataType\":\"String\",\"StringValue\":\"", + { + "Ref": "zoneEB40FF1E" + }, + "\"},\"RecordName\":{\"DataType\":\"String\",\"StringValue\":\"test-record\"}}},\"physicalResourceId\":{\"responsePath\":\"MessageId\"}}" + ] + ] }, "InstallLatestAwsSdk": true }, @@ -1210,7 +1190,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904" }, "S3Key": { "Fn::Join": [ @@ -1223,7 +1203,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -1236,7 +1216,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -1286,17 +1266,17 @@ "Type": "String", "Description": "Artifact hash for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904": { "Type": "String", - "Description": "S3 bucket for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 bucket for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF": { "Type": "String", - "Description": "S3 key for asset version \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 key for asset version \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94ArtifactHash782948FC": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343ArtifactHash0C561FF5": { "Type": "String", - "Description": "Artifact hash for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "Artifact hash for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" } }, "Outputs": { diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.multiple-environments.expected.json b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.multiple-environments.expected.json index 1d41a6913794e..2aab9da2612fa 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.multiple-environments.expected.json +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.multiple-environments.expected.json @@ -2235,4 +2235,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 759b013272bcf..926f45daf1436 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -157,6 +157,17 @@ book.addMethod('GET', getBookIntegration, { }); ``` +It is possible to also integrate with AWS services in a different region. The following code integrates with Amazon SQS in the +`eu-west-1` region. + +```ts +const getMessageIntegration = new apigateway.AwsIntegration({ + service: 'sqs', + path: 'queueName', + region: 'eu-west-1' +}); +``` + ## API Keys The following example shows how to use an API Key with a usage plan: diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts index 6374bb404902a..0e607e25a21cf 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts @@ -60,6 +60,13 @@ export interface AwsIntegrationProps { * Integration options, such as content handling, request/response mapping, etc. */ readonly options?: IntegrationOptions + + /** + * The region of the integrated AWS service. + * + * @default - same region as the stack + */ + readonly region?: string; } /** @@ -87,6 +94,7 @@ export class AwsIntegration extends Integration { resource: apiType, sep: '/', resourceName: apiValue, + region: props.region, }); }, }), diff --git a/packages/@aws-cdk/aws-apigateway/test/method.test.ts b/packages/@aws-cdk/aws-apigateway/test/method.test.ts index 36610126f3ec6..3fb5c580bc541 100644 --- a/packages/@aws-cdk/aws-apigateway/test/method.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/method.test.ts @@ -90,6 +90,35 @@ describe('method', () => { }); + test('integration can be set for a service in the provided region', () => { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigw.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); + + // WHEN + new apigw.Method(stack, 'my-method', { + httpMethod: 'POST', + resource: api.root, + integration: new apigw.AwsIntegration({ service: 'sqs', path: 'queueName', region: 'eu-west-1' }), + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Integration: { + IntegrationHttpMethod: 'POST', + Type: 'AWS', + Uri: { + 'Fn::Join': [ + '', + [ + 'arn:', { Ref: 'AWS::Partition' }, ':apigateway:eu-west-1:sqs:path/queueName', + ], + ], + }, + }, + }); + }); + test('integration with a custom http method can be set via a property', () => { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.core-custom-resources.expected.json b/packages/@aws-cdk/aws-cloudformation/test/integ.core-custom-resources.expected.json index 23c4884db164f..e5db15d48809b 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.core-custom-resources.expected.json +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.core-custom-resources.expected.json @@ -72,7 +72,8 @@ "Arn" ] }, - "Runtime": "nodejs12.x" + "Runtime": "nodejs12.x", + "Description": "veni vidi vici" }, "DependsOn": [ "CustomReflectCustomResourceProviderRoleB4B29AEC" diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.core-custom-resources.ts b/packages/@aws-cdk/aws-cloudformation/test/integ.core-custom-resources.ts index 1743444c1ad57..7f5e9d49a95db 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.core-custom-resources.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.core-custom-resources.ts @@ -24,6 +24,7 @@ class TestStack extends Stack { const serviceToken = CustomResourceProvider.getOrCreate(this, resourceType, { codeDirectory: `${__dirname}/core-custom-resource-provider-fixture`, runtime: CustomResourceProviderRuntime.NODEJS_12, + description: 'veni vidi vici', }); const cr = new CustomResource(this, 'MyResource', { diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/private/rendering.ts b/packages/@aws-cdk/aws-cloudwatch/lib/private/rendering.ts index e8dcb15cabc84..8553d9ad5c486 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/private/rendering.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/private/rendering.ts @@ -55,6 +55,7 @@ function metricGraphJson(metric: IMetric, yAxis?: string, id?: string) { withExpression(expr) { options.expression = expr.expression; + if (expr.period && expr.period !== 300) { options.period = expr.period; } }, }); diff --git a/packages/@aws-cdk/aws-cloudwatch/test/integ.math-alarm-and-dashboard.expected.json b/packages/@aws-cdk/aws-cloudwatch/test/integ.math-alarm-and-dashboard.expected.json index 7de0e4290cd65..8e9b235bb2b65 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/integ.math-alarm-and-dashboard.expected.json +++ b/packages/@aws-cdk/aws-cloudwatch/test/integ.math-alarm-and-dashboard.expected.json @@ -88,7 +88,7 @@ { "Ref": "AWS::Region" }, - "\",\"metrics\":[[{\"label\":\"Total Messages\",\"expression\":\"m1+m2\"}],[\"AWS/SQS\",\"ApproximateNumberOfMessagesVisible\",\"QueueName\",\"", + "\",\"metrics\":[[{\"label\":\"Total Messages\",\"expression\":\"m1+m2\",\"period\":60}],[\"AWS/SQS\",\"ApproximateNumberOfMessagesVisible\",\"QueueName\",\"", { "Fn::GetAtt": [ "queue", @@ -120,7 +120,7 @@ { "Ref": "AWS::Region" }, - "\",\"metrics\":[[{\"label\":\"Total Messages\",\"expression\":\"m1+m2\"}],[\"AWS/SQS\",\"ApproximateNumberOfMessagesVisible\",\"QueueName\",\"", + "\",\"metrics\":[[{\"label\":\"Total Messages\",\"expression\":\"m1+m2\",\"period\":60}],[\"AWS/SQS\",\"ApproximateNumberOfMessagesVisible\",\"QueueName\",\"", { "Fn::GetAtt": [ "queue", diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.metric-math.ts b/packages/@aws-cdk/aws-cloudwatch/test/test.metric-math.ts index e8288ce092b27..b5eecac9ec52d 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.metric-math.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/test.metric-math.ts @@ -219,6 +219,28 @@ export = { test.done(); }, + 'top level period in a MathExpression is respected in its metrics'(test: Test) { + const graph = new GraphWidget({ + left: [ + a, + new MathExpression({ + expression: 'a + b', + usingMetrics: { a, b }, + period: Duration.minutes(1), + }), + ], + }); + + // THEN + graphMetricsAre(test, graph, [ + ['Test', 'ACount'], + [{ label: 'a + b', expression: 'a + b', period: 60 }], + ['Test', 'ACount', { visible: false, id: 'a', period: 60 }], + ['Test', 'BCount', { visible: false, id: 'b', period: 60 }], + ]); + test.done(); + }, + 'MathExpression controls period of metrics transitively used in it'(test: Test) { // Same as the previous test, but recursively diff --git a/packages/@aws-cdk/aws-codedeploy/test/lambda/test.custom-deployment-config.ts b/packages/@aws-cdk/aws-codedeploy/test/lambda/test.custom-deployment-config.ts index 61917637c19d9..fdd8ba0c56be6 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/lambda/test.custom-deployment-config.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/lambda/test.custom-deployment-config.ts @@ -53,49 +53,9 @@ export = { 'Arn', ], }, - Create: { - action: 'createDeploymentConfig', - service: 'CodeDeploy', - parameters: { - computePlatform: 'Lambda', - deploymentConfigName: 'CustomConfig.LambdaCanary5Percent1Minutes', - trafficRoutingConfig: { - timeBasedCanary: { - canaryPercentage: '5', - canaryInterval: '1', - }, - type: 'TimeBasedCanary', - }, - }, - physicalResourceId: { - id: 'CustomConfig.LambdaCanary5Percent1Minutes', - }, - }, - Update: { - action: 'createDeploymentConfig', - service: 'CodeDeploy', - parameters: { - computePlatform: 'Lambda', - deploymentConfigName: 'CustomConfig.LambdaCanary5Percent1Minutes', - trafficRoutingConfig: { - timeBasedCanary: { - canaryPercentage: '5', - canaryInterval: '1', - }, - type: 'TimeBasedCanary', - }, - }, - physicalResourceId: { - id: 'CustomConfig.LambdaCanary5Percent1Minutes', - }, - }, - Delete: { - action: 'deleteDeploymentConfig', - service: 'CodeDeploy', - parameters: { - deploymentConfigName: 'CustomConfig.LambdaCanary5Percent1Minutes', - }, - }, + Create: '{"service":"CodeDeploy","action":"createDeploymentConfig","parameters":{"deploymentConfigName":"CustomConfig.LambdaCanary5Percent1Minutes","computePlatform":"Lambda","trafficRoutingConfig":{"type":"TimeBasedCanary","timeBasedCanary":{"canaryInterval":"1","canaryPercentage":"5"}}},"physicalResourceId":{"id":"CustomConfig.LambdaCanary5Percent1Minutes"}}', + Update: '{"service":"CodeDeploy","action":"createDeploymentConfig","parameters":{"deploymentConfigName":"CustomConfig.LambdaCanary5Percent1Minutes","computePlatform":"Lambda","trafficRoutingConfig":{"type":"TimeBasedCanary","timeBasedCanary":{"canaryInterval":"1","canaryPercentage":"5"}}},"physicalResourceId":{"id":"CustomConfig.LambdaCanary5Percent1Minutes"}}', + Delete: '{"service":"CodeDeploy","action":"deleteDeploymentConfig","parameters":{"deploymentConfigName":"CustomConfig.LambdaCanary5Percent1Minutes"}}', })); expect(stack).to(haveResource('AWS::IAM::Policy', { @@ -134,27 +94,9 @@ export = { // THEN expect(stack).to(haveResourceLike('Custom::AWS', { - Create: { - parameters: { - deploymentConfigName: 'MyDeploymentConfig', - }, - physicalResourceId: { - id: 'MyDeploymentConfig', - }, - }, - Update: { - parameters: { - deploymentConfigName: 'MyDeploymentConfig', - }, - physicalResourceId: { - id: 'MyDeploymentConfig', - }, - }, - Delete: { - parameters: { - deploymentConfigName: 'MyDeploymentConfig', - }, - }, + Create: '{"service":"CodeDeploy","action":"createDeploymentConfig","parameters":{"deploymentConfigName":"MyDeploymentConfig","computePlatform":"Lambda","trafficRoutingConfig":{"type":"TimeBasedCanary","timeBasedCanary":{"canaryInterval":"1","canaryPercentage":"5"}}},"physicalResourceId":{"id":"MyDeploymentConfig"}}', + Update: '{"service":"CodeDeploy","action":"createDeploymentConfig","parameters":{"deploymentConfigName":"MyDeploymentConfig","computePlatform":"Lambda","trafficRoutingConfig":{"type":"TimeBasedCanary","timeBasedCanary":{"canaryInterval":"1","canaryPercentage":"5"}}},"physicalResourceId":{"id":"MyDeploymentConfig"}}', + Delete: '{"service":"CodeDeploy","action":"deleteDeploymentConfig","parameters":{"deploymentConfigName":"MyDeploymentConfig"}}', })); test.done(); }, diff --git a/packages/@aws-cdk/aws-codepipeline-actions/package.json b/packages/@aws-cdk/aws-codepipeline-actions/package.json index 6087beb2745df..62a7868ece518 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/package.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/package.json @@ -68,6 +68,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-cloudtrail": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "@types/lodash": "^4.14.168", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/lambda/lambda-invoke-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/lambda/lambda-invoke-action.test.ts index e9674e4531695..64eecdb801b1c 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/lambda/lambda-invoke-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/lambda/lambda-invoke-action.test.ts @@ -4,12 +4,13 @@ import * as lambda from '@aws-cdk/aws-lambda'; import * as s3 from '@aws-cdk/aws-s3'; import * as sns from '@aws-cdk/aws-sns'; import { App, Aws, Lazy, SecretValue, Stack, Token } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -const s3GrantWriteCtx = { '@aws-cdk/aws-s3:grantWriteWithoutAcl': true }; +const s3GrantWriteCtx = { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true }; describe('', () => { describe('Lambda invoke Action', () => { diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/s3/s3-deploy-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/s3/s3-deploy-action.test.ts index c1e1c3c34c2c8..2e2b5cef98ace 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/s3/s3-deploy-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/s3/s3-deploy-action.test.ts @@ -2,6 +2,7 @@ import '@aws-cdk/assert/jest'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as s3 from '@aws-cdk/aws-s3'; import { App, Duration, SecretValue, Stack } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as cpactions from '../../lib'; @@ -48,7 +49,7 @@ describe('', () => { }); - testFutureBehavior('grant the pipeline correct access to the target bucket', { '@aws-cdk/aws-s3:grantWriteWithoutAcl': true }, App, (app) => { + testFutureBehavior('grant the pipeline correct access to the target bucket', { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true }, App, (app) => { const stack = new Stack(app); minimalPipeline(stack); diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-cfdist.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-cfdist.expected.json index e9a67d39c9235..abcb9265e2bda 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-cfdist.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-cfdist.expected.json @@ -71,32 +71,36 @@ ] }, "Create": { - "service": "CognitoIdentityServiceProvider", - "action": "describeUserPoolDomain", - "parameters": { - "Domain": { - "Ref": "UserPoolDomainD0EA232A" - } - }, - "physicalResourceId": { - "id": { - "Ref": "UserPoolDomainD0EA232A" - } - } + "Fn::Join": [ + "", + [ + "{\"service\":\"CognitoIdentityServiceProvider\",\"action\":\"describeUserPoolDomain\",\"parameters\":{\"Domain\":\"", + { + "Ref": "UserPoolDomainD0EA232A" + }, + "\"},\"physicalResourceId\":{\"id\":\"", + { + "Ref": "UserPoolDomainD0EA232A" + }, + "\"}}" + ] + ] }, "Update": { - "service": "CognitoIdentityServiceProvider", - "action": "describeUserPoolDomain", - "parameters": { - "Domain": { - "Ref": "UserPoolDomainD0EA232A" - } - }, - "physicalResourceId": { - "id": { - "Ref": "UserPoolDomainD0EA232A" - } - } + "Fn::Join": [ + "", + [ + "{\"service\":\"CognitoIdentityServiceProvider\",\"action\":\"describeUserPoolDomain\",\"parameters\":{\"Domain\":\"", + { + "Ref": "UserPoolDomainD0EA232A" + }, + "\"},\"physicalResourceId\":{\"id\":\"", + { + "Ref": "UserPoolDomainD0EA232A" + }, + "\"}}" + ] + ] }, "InstallLatestAwsSdk": true }, @@ -142,7 +146,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904" }, "S3Key": { "Fn::Join": [ @@ -155,7 +159,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -168,7 +172,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -209,17 +213,17 @@ } }, "Parameters": { - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904": { "Type": "String", - "Description": "S3 bucket for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 bucket for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF": { "Type": "String", - "Description": "S3 key for asset version \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 key for asset version \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94ArtifactHash782948FC": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343ArtifactHash0C561FF5": { "Type": "String", - "Description": "Artifact hash for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "Artifact hash for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-docdb/README.md b/packages/@aws-cdk/aws-docdb/README.md index 826033de4c134..530942578a090 100644 --- a/packages/@aws-cdk/aws-docdb/README.md +++ b/packages/@aws-cdk/aws-docdb/README.md @@ -30,7 +30,7 @@ your instances will be launched privately or publicly: ```ts const cluster = new DatabaseCluster(this, 'Database', { masterUser: { - username: 'admin' + username: 'myuser' // NOTE: 'admin' is reserved by DocumentDB }, instanceProps: { instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.LARGE), diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index 3b902e5cb8c75..49fff4b5c4f63 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -275,7 +275,7 @@ DatabaseSubnet3 |`ISOLATED`|`10.0.6.32/28`|#3|Only routes within the VPC ### Accessing the Internet Gateway -If you need access to the internet gateway, you can get it's ID like so: +If you need access to the internet gateway, you can get its ID like so: ```ts const igwId = vpc.internetGatewayId; diff --git a/packages/@aws-cdk/aws-ec2/lib/cfn-init-elements.ts b/packages/@aws-cdk/aws-ec2/lib/cfn-init-elements.ts index a190c17d7aa20..646d6b2dcbfa4 100644 --- a/packages/@aws-cdk/aws-ec2/lib/cfn-init-elements.ts +++ b/packages/@aws-cdk/aws-ec2/lib/cfn-init-elements.ts @@ -327,6 +327,9 @@ export abstract class InitFile extends InitElement { * Use a literal string as the file content */ public static fromString(fileName: string, content: string, options: InitFileOptions = {}): InitFile { + if (!content) { + throw new Error(`InitFile ${fileName}: cannot create empty file. Please supply at least one character of content.`); + } return new class extends InitFile { protected _doBind(bindOptions: InitBindOptions) { return { diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts index 197946716f969..2250bdd37c6c3 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts @@ -287,6 +287,7 @@ export class InterfaceVpcEndpointAwsService implements IInterfaceVpcEndpointServ public static readonly KMS = new InterfaceVpcEndpointAwsService('kms'); public static readonly CLOUDWATCH_LOGS = new InterfaceVpcEndpointAwsService('logs'); public static readonly CLOUDWATCH = new InterfaceVpcEndpointAwsService('monitoring'); + public static readonly RDS = new InterfaceVpcEndpointAwsService('rds'); public static readonly SAGEMAKER_API = new InterfaceVpcEndpointAwsService('sagemaker.api'); public static readonly SAGEMAKER_RUNTIME = new InterfaceVpcEndpointAwsService('sagemaker.runtime'); public static readonly SAGEMAKER_RUNTIME_FIPS = new InterfaceVpcEndpointAwsService('sagemaker.runtime-fips'); diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index 97f9edc2c3300..801040d4c7385 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -72,6 +72,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -232,6 +233,7 @@ "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.KINESIS_STREAMS", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.KINESIS_FIREHOSE", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.KMS", + "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.RDS", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.SAGEMAKER_API", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.SAGEMAKER_NOTEBOOK", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.SAGEMAKER_RUNTIME", diff --git a/packages/@aws-cdk/aws-ec2/test/cfn-init-element.test.ts b/packages/@aws-cdk/aws-ec2/test/cfn-init-element.test.ts index e794021c46b8f..75896912f3661 100644 --- a/packages/@aws-cdk/aws-ec2/test/cfn-init-element.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/cfn-init-element.test.ts @@ -206,6 +206,12 @@ describe('InitFile', () => { }); }); + test('empty content string throws error', () => { + expect(() => { + ec2.InitFile.fromString('/tmp/foo', ''); + }).toThrow('InitFile /tmp/foo: cannot create empty file. Please supply at least one character of content.'); + }); + test('symlink throws an error if mode is set incorrectly', () => { expect(() => { ec2.InitFile.symlink('/tmp/foo', '/tmp/bar', { diff --git a/packages/@aws-cdk/aws-ec2/test/volume.test.ts b/packages/@aws-cdk/aws-ec2/test/volume.test.ts index edd0323bc84ef..c390821c3a37a 100644 --- a/packages/@aws-cdk/aws-ec2/test/volume.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/volume.test.ts @@ -11,6 +11,7 @@ import { } from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { testFutureBehavior, testLegacyBehavior } from 'cdk-build-tools/lib/feature-flag'; import { AmazonLinuxGeneration, @@ -575,7 +576,7 @@ describe('volume', () => { }); - testFutureBehavior('with future flag aws-kms:defaultKeyPolicies', { '@aws-cdk/aws-kms:defaultKeyPolicies': true }, cdk.App, (app) => { + testFutureBehavior('with future flag aws-kms:defaultKeyPolicies', { [cxapi.KMS_DEFAULT_KEY_POLICIES]: true }, cdk.App, (app) => { // GIVEN const stack = new cdk.Stack(app); const role = new Role(stack, 'Role', { assumedBy: new AccountRootPrincipal() }); diff --git a/packages/@aws-cdk/aws-ecr-assets/test/image-asset.test.ts b/packages/@aws-cdk/aws-ecr-assets/test/image-asset.test.ts index 83f27e16a3857..17fd37be2e234 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/image-asset.test.ts +++ b/packages/@aws-cdk/aws-ecr-assets/test/image-asset.test.ts @@ -5,27 +5,19 @@ import * as iam from '@aws-cdk/aws-iam'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { App, DefaultStackSynthesizer, IgnoreMode, Lazy, LegacyStackSynthesizer, Stack, Stage } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; +import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; import { DockerImageAsset } from '../lib'; /* eslint-disable quote-props */ const DEMO_IMAGE_ASSET_HASH = 'b2c69bfbfe983b634456574587443159b3b7258849856a118ad3d2772238f1a5'; - -let app: App; -let stack: Stack; -beforeEach(() => { - app = new App({ - context: { - '@aws-cdk/aws-ecr-assets:dockerIgnoreSupport': true, - }, - }); - stack = new Stack(app, 'Stack'); -}); +const flags = { [cxapi.DOCKER_IGNORE_SUPPORT]: true }; describe('image asset', () => { - test('test instantiating Asset Image', () => { + testFutureBehavior('test instantiating Asset Image', flags, App, (app) => { // WHEN + const stack = new Stack(app); new DockerImageAsset(stack, 'Image', { directory: path.join(__dirname, 'demo-image'), }); @@ -47,8 +39,9 @@ describe('image asset', () => { }); - test('with build args', () => { + testFutureBehavior('with build args', flags, App, (app) => { // WHEN + const stack = new Stack(app); new DockerImageAsset(stack, 'Image', { directory: path.join(__dirname, 'demo-image'), buildArgs: { @@ -62,8 +55,9 @@ describe('image asset', () => { }); - test('with target', () => { + testFutureBehavior('with target', flags, App, (app) => { // WHEN + const stack = new Stack(app); new DockerImageAsset(stack, 'Image', { directory: path.join(__dirname, 'demo-image'), buildArgs: { @@ -78,8 +72,9 @@ describe('image asset', () => { }); - test('with file', () => { + testFutureBehavior('with file', flags, App, (app) => { // GIVEN + const stack = new Stack(app); const directoryPath = path.join(__dirname, 'demo-image-custom-docker-file'); // WHEN new DockerImageAsset(stack, 'Image', { @@ -93,8 +88,9 @@ describe('image asset', () => { }); - test('asset.repository.grantPull can be used to grant a principal permissions to use the image', () => { + testFutureBehavior('asset.repository.grantPull can be used to grant a principal permissions to use the image', flags, App, (app) => { // GIVEN + const stack = new Stack(app); const user = new iam.User(stack, 'MyUser'); const asset = new DockerImageAsset(stack, 'Image', { directory: path.join(__dirname, 'demo-image'), @@ -155,6 +151,7 @@ describe('image asset', () => { }); test('fails if the directory does not exist', () => { + const stack = new Stack(); // THEN expect(() => { new DockerImageAsset(stack, 'MyAsset', { @@ -165,6 +162,7 @@ describe('image asset', () => { }); test('fails if the directory does not contain a Dockerfile', () => { + const stack = new Stack(); // THEN expect(() => { new DockerImageAsset(stack, 'Asset', { @@ -175,6 +173,7 @@ describe('image asset', () => { }); test('fails if the file does not exist', () => { + const stack = new Stack(); // THEN expect(() => { new DockerImageAsset(stack, 'Asset', { @@ -185,7 +184,8 @@ describe('image asset', () => { }); - test('docker directory is staged if asset staging is enabled', () => { + testFutureBehavior('docker directory is staged if asset staging is enabled', flags, App, (app) => { + const stack = new Stack(app); const image = new DockerImageAsset(stack, 'MyAsset', { directory: path.join(__dirname, 'demo-image'), }); @@ -197,15 +197,16 @@ describe('image asset', () => { }); - test('docker directory is staged without files specified in .dockerignore', () => { - testDockerDirectoryIsStagedWithoutFilesSpecifiedInDockerignore(); + testFutureBehavior('docker directory is staged without files specified in .dockerignore', flags, App, (app) => { + testDockerDirectoryIsStagedWithoutFilesSpecifiedInDockerignore(app); }); - test('docker directory is staged without files specified in .dockerignore with IgnoreMode.GLOB', () => { - testDockerDirectoryIsStagedWithoutFilesSpecifiedInDockerignore(IgnoreMode.GLOB); + testFutureBehavior('docker directory is staged without files specified in .dockerignore with IgnoreMode.GLOB', flags, App, (app) => { + testDockerDirectoryIsStagedWithoutFilesSpecifiedInDockerignore(app, IgnoreMode.GLOB); }); - test('docker directory is staged with whitelisted files specified in .dockerignore', () => { + testFutureBehavior('docker directory is staged with whitelisted files specified in .dockerignore', flags, App, (app) => { + const stack = new Stack(app); const image = new DockerImageAsset(stack, 'MyAsset', { directory: path.join(__dirname, 'whitelisted-image'), }); @@ -227,16 +228,17 @@ describe('image asset', () => { }); - test('docker directory is staged without files specified in exclude option', () => { - testDockerDirectoryIsStagedWithoutFilesSpecifiedInExcludeOption(); + testFutureBehavior('docker directory is staged without files specified in exclude option', flags, App, (app) => { + testDockerDirectoryIsStagedWithoutFilesSpecifiedInExcludeOption(app); }); - test('docker directory is staged without files specified in exclude option with IgnoreMode.GLOB', () => { - testDockerDirectoryIsStagedWithoutFilesSpecifiedInExcludeOption(IgnoreMode.GLOB); + testFutureBehavior('docker directory is staged without files specified in exclude option with IgnoreMode.GLOB', flags, App, (app) => { + testDockerDirectoryIsStagedWithoutFilesSpecifiedInExcludeOption(app, IgnoreMode.GLOB); }); test('fails if using tokens in build args keys or values', () => { // GIVEN + const stack = new Stack(); const token = Lazy.string({ produce: () => 'foo' }); const expected = /Cannot use tokens in keys or values of "buildArgs" since they are needed before deployment/; @@ -256,6 +258,7 @@ describe('image asset', () => { test('fails if using token as repositoryName', () => { // GIVEN + const stack = new Stack(); const token = Lazy.string({ produce: () => 'foo' }); // THEN @@ -267,8 +270,9 @@ describe('image asset', () => { }); - test('docker build options are included in the asset id', () => { + testFutureBehavior('docker build options are included in the asset id', flags, App, (app) => { // GIVEN + const stack = new Stack(app); const directory = path.join(__dirname, 'demo-image-custom-docker-file'); const asset1 = new DockerImageAsset(stack, 'Asset1', { directory }); @@ -290,7 +294,8 @@ describe('image asset', () => { }); }); -function testDockerDirectoryIsStagedWithoutFilesSpecifiedInDockerignore(ignoreMode?: IgnoreMode) { +function testDockerDirectoryIsStagedWithoutFilesSpecifiedInDockerignore(app: App, ignoreMode?: IgnoreMode) { + const stack = new Stack(app); const image = new DockerImageAsset(stack, 'MyAsset', { ignoreMode, directory: path.join(__dirname, 'dockerignore-image'), @@ -309,7 +314,8 @@ function testDockerDirectoryIsStagedWithoutFilesSpecifiedInDockerignore(ignoreMo } -function testDockerDirectoryIsStagedWithoutFilesSpecifiedInExcludeOption(ignoreMode?: IgnoreMode) { +function testDockerDirectoryIsStagedWithoutFilesSpecifiedInExcludeOption(app: App, ignoreMode?: IgnoreMode) { + const stack = new Stack(app); const image = new DockerImageAsset(stack, 'MyAsset', { directory: path.join(__dirname, 'dockerignore-image'), exclude: ['subdirectory'], @@ -328,7 +334,7 @@ function testDockerDirectoryIsStagedWithoutFilesSpecifiedInExcludeOption(ignoreM } -test('nested assemblies share assets: legacy synth edition', () => { +testFutureBehavior('nested assemblies share assets: legacy synth edition', flags, App, (app) => { // GIVEN const stack1 = new Stack(new Stage(app, 'Stage1'), 'Stack', { synthesizer: new LegacyStackSynthesizer() }); const stack2 = new Stack(new Stage(app, 'Stage2'), 'Stack', { synthesizer: new LegacyStackSynthesizer() }); @@ -354,7 +360,7 @@ test('nested assemblies share assets: legacy synth edition', () => { } }); -test('nested assemblies share assets: default synth edition', () => { +testFutureBehavior('nested assemblies share assets: default synth edition', flags, App, (app) => { // GIVEN const stack1 = new Stack(new Stage(app, 'Stage1'), 'Stack', { synthesizer: new DefaultStackSynthesizer() }); const stack2 = new Stack(new Stage(app, 'Stage2'), 'Stack', { synthesizer: new DefaultStackSynthesizer() }); diff --git a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.ts b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.ts index 21161938c786f..d6c4c2aac75b7 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.ts +++ b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.ts @@ -1,11 +1,12 @@ import * as path from 'path'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import * as assets from '../lib'; const app = new cdk.App({ context: { - '@aws-cdk/aws-ecr-assets:dockerIgnoreSupport': true, + [cxapi.DOCKER_IGNORE_SUPPORT]: true, }, }); const stack = new cdk.Stack(app, 'integ-assets-docker'); diff --git a/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.ts b/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.ts index 9a6fe48b172e0..394fedc07ddf6 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.ts +++ b/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.ts @@ -1,6 +1,7 @@ import * as path from 'path'; import * as iam from '@aws-cdk/aws-iam'; import { App, CfnOutput, NestedStack, NestedStackProps, Stack, StackProps } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import * as ecr_assets from '../lib'; @@ -29,7 +30,7 @@ class TheParentStack extends Stack { const app = new App({ context: { - '@aws-cdk/aws-ecr-assets:dockerIgnoreSupport': true, + [cxapi.DOCKER_IGNORE_SUPPORT]: true, }, }); new TheParentStack(app, 'nested-stacks-docker'); diff --git a/packages/@aws-cdk/aws-ecr/lib/repository.ts b/packages/@aws-cdk/aws-ecr/lib/repository.ts index 83c05ba1ed308..3787903dedc3e 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository.ts @@ -39,6 +39,15 @@ export interface IRepository extends IResource { */ repositoryUriForTag(tag?: string): string; + /** + * Returns the URI of the repository for a certain tag. Can be used in `docker push/pull`. + * + * ACCOUNT.dkr.ecr.REGION.amazonaws.com/REPOSITORY[@DIGEST] + * + * @param digest Image digest to use (tools usually default to the image with the "latest" tag if omitted) + */ + repositoryUriForDigest(digest?: string): string; + /** * Add a policy statement to the repository's resource policy */ @@ -136,8 +145,29 @@ export abstract class RepositoryBase extends Resource implements IRepository { */ public repositoryUriForTag(tag?: string): string { const tagSuffix = tag ? `:${tag}` : ''; + return this.repositoryUriWithSuffix(tagSuffix); + } + + /** + * Returns the URL of the repository. Can be used in `docker push/pull`. + * + * ACCOUNT.dkr.ecr.REGION.amazonaws.com/REPOSITORY[@DIGEST] + * + * @param digest Optional image digest + */ + public repositoryUriForDigest(digest?: string): string { + const digestSuffix = digest ? `@${digest}` : ''; + return this.repositoryUriWithSuffix(digestSuffix); + } + + /** + * Returns the repository URI, with an appended suffix, if provided. + * @param suffix An image tag or an image digest. + * @private + */ + private repositoryUriWithSuffix(suffix?: string): string { const parts = this.stack.parseArn(this.repositoryArn); - return `${parts.account}.dkr.ecr.${parts.region}.${this.stack.urlSuffix}/${this.repositoryName}${tagSuffix}`; + return `${parts.account}.dkr.ecr.${parts.region}.${this.stack.urlSuffix}/${this.repositoryName}${suffix}`; } /** diff --git a/packages/@aws-cdk/aws-ecs-patterns/README.md b/packages/@aws-cdk/aws-ecs-patterns/README.md index 12f56a2b70102..a1072d47fe700 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/README.md +++ b/packages/@aws-cdk/aws-ecs-patterns/README.md @@ -466,3 +466,41 @@ const scheduledFargateTask = new ScheduledFargateTask(stack, 'ScheduledFargateTa platformVersion: ecs.FargatePlatformVersion.VERSION1_4, }); ``` + +### Use the REMOVE_DEFAULT_DESIRED_COUNT feature flag + +The REMOVE_DEFAULT_DESIRED_COUNT feature flag is used to override the default desiredCount that is autogenerated by the CDK. This will set the desiredCount of any service created by any of the following constructs to be undefined. + +* ApplicationLoadBalancedEc2Service +* ApplicationLoadBalancedFargateService +* NetworkLoadBalancedEc2Service +* NetworkLoadBalancedFargateService +* QueueProcessingEc2Service +* QueueProcessingFargateService + +If a desiredCount is not passed in as input to the above constructs, CloudFormation will either create a new service to start up with a desiredCount of 1, or update an existing service to start up with the same desiredCount as prior to the update. + +To enable the feature flag, ensure that the REMOVE_DEFAULT_DESIRED_COUNT flag within an application stack context is set to true, like so: + +```ts +stack.node.setContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT, true); +``` + +The following is an example of an application with the REMOVE_DEFAULT_DESIRED_COUNT feature flag enabled: + +```ts +const app = new App(); + +const stack = new Stack(app, 'aws-ecs-patterns-queue'); +stack.node.setContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT, true); + +const vpc = new ec2.Vpc(stack, 'VPC', { + maxAzs: 2, +}); + +new QueueProcessingFargateService(stack, 'QueueProcessingService', { + vpc, + memoryLimitMiB: 512, + image: new ecs.AssetImage(path.join(__dirname, '..', 'sqs-reader')), +}); +``` diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts index 74411bb217558..dde07a16e114c 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts @@ -78,7 +78,9 @@ export interface ApplicationLoadBalancedServiceBaseProps { * The desired number of instantiations of the task definition to keep running on the service. * The minimum value is 1 * - * @default 1 + * @default - If the feature flag, ECS_REMOVE_DEFAULT_DESIRED_COUNT is false, the default is 1; + * if true, the default is 1 for all new services and uses the existing services desired count + * when updating an existing service. */ readonly desiredCount?: number; @@ -311,12 +313,19 @@ export interface ApplicationLoadBalancedTaskImageOptions { * The base class for ApplicationLoadBalancedEc2Service and ApplicationLoadBalancedFargateService services. */ export abstract class ApplicationLoadBalancedServiceBase extends CoreConstruct { - /** * The desired number of instantiations of the task definition to keep running on the service. + * @deprecated - Use `internalDesiredCount` instead. */ public readonly desiredCount: number; + /** + * The desired number of instantiations of the task definition to keep running on the service. + * The default is 1 for all new services and uses the existing services desired count + * when updating an existing service if one is not provided. + */ + public readonly internalDesiredCount?: number; + /** * The Application Load Balancer for the service. */ @@ -368,7 +377,9 @@ export abstract class ApplicationLoadBalancedServiceBase extends CoreConstruct { if (props.desiredCount !== undefined && props.desiredCount < 1) { throw new Error('You must specify a desiredCount greater than 0'); } + this.desiredCount = props.desiredCount || 1; + this.internalDesiredCount = props.desiredCount; const internetFacing = props.publicLoadBalancer ?? true; diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts index f3eb132e934ae..ff3fc675fcd69 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts @@ -47,7 +47,9 @@ export interface ApplicationMultipleTargetGroupsServiceBaseProps { /** * The desired number of instantiations of the task definition to keep running on the service. * - * @default 1 + * @default - If the feature flag, ECS_REMOVE_DEFAULT_DESIRED_COUNT is false, the default is 1; + * if true, the default is 1 for all new services and uses the existing services desired count + * when updating an existing service. */ readonly desiredCount?: number; @@ -329,12 +331,19 @@ export interface ApplicationListenerProps { * The base class for ApplicationMultipleTargetGroupsEc2Service and ApplicationMultipleTargetGroupsFargateService classes. */ export abstract class ApplicationMultipleTargetGroupsServiceBase extends CoreConstruct { - /** * The desired number of instantiations of the task definition to keep running on the service. + * @deprecated - Use `internalDesiredCount` instead. */ public readonly desiredCount: number; + /** + * The desired number of instantiations of the task definition to keep running on the service. + * The default is 1 for all new services and uses the existing services desired count + * when updating an existing service, if one is not provided. + */ + public readonly internalDesiredCount?: number; + /** * The default Application Load Balancer for the service (first added load balancer). */ @@ -365,7 +374,10 @@ export abstract class ApplicationMultipleTargetGroupsServiceBase extends CoreCon this.validateInput(props); this.cluster = props.cluster || this.getDefaultCluster(this, props.vpc); + this.desiredCount = props.desiredCount || 1; + this.internalDesiredCount = props.desiredCount; + if (props.taskImageOptions) { this.logDriver = this.createLogDriver(props.taskImageOptions.enableLogging, props.taskImageOptions.logDriver); } diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts index 656cc19d19d43..949b052aebbad 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts @@ -67,7 +67,9 @@ export interface NetworkLoadBalancedServiceBaseProps { * The desired number of instantiations of the task definition to keep running on the service. * The minimum value is 1 * - * @default 1 + * @default - If the feature flag, ECS_REMOVE_DEFAULT_DESIRED_COUNT is false, the default is 1; + * if true, the default is 1 for all new services and uses the existing services desired count + * when updating an existing service. */ readonly desiredCount?: number; @@ -263,9 +265,17 @@ export interface NetworkLoadBalancedTaskImageOptions { export abstract class NetworkLoadBalancedServiceBase extends CoreConstruct { /** * The desired number of instantiations of the task definition to keep running on the service. + * @deprecated - Use `internalDesiredCount` instead. */ public readonly desiredCount: number; + /** + * The desired number of instantiations of the task definition to keep running on the service. + * The default is 1 for all new services and uses the existing services desired count + * when updating an existing service, if one is not provided. + */ + public readonly internalDesiredCount?: number; + /** * The Network Load Balancer for the service. */ @@ -306,7 +316,9 @@ export abstract class NetworkLoadBalancedServiceBase extends CoreConstruct { if (props.desiredCount !== undefined && props.desiredCount < 1) { throw new Error('You must specify a desiredCount greater than 0'); } + this.desiredCount = props.desiredCount || 1; + this.internalDesiredCount = props.desiredCount; const internetFacing = props.publicLoadBalancer ?? true; diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-multiple-target-groups-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-multiple-target-groups-service-base.ts index d34a6b548076d..60fd9904b8078 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-multiple-target-groups-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-multiple-target-groups-service-base.ts @@ -45,7 +45,9 @@ export interface NetworkMultipleTargetGroupsServiceBaseProps { * The desired number of instantiations of the task definition to keep running on the service. * The minimum value is 1 * - * @default 1 + * @default - If the feature flag, ECS_REMOVE_DEFAULT_DESIRED_COUNT is false, the default is 1; + * if true, the default is 1 for all new services and uses the existing services desired count + * when updating an existing service. */ readonly desiredCount?: number; @@ -264,9 +266,17 @@ export interface NetworkTargetProps { export abstract class NetworkMultipleTargetGroupsServiceBase extends CoreConstruct { /** * The desired number of instantiations of the task definition to keep running on the service. + * @deprecated - Use `internalDesiredCount` instead. */ public readonly desiredCount: number; + /** + * The desired number of instantiations of the task definition to keep running on the service. + * The default is 1 for all new services and uses the existing services desired count + * when updating an existing service, if one is not provided. + */ + public readonly internalDesiredCount?: number; + /** * The Network Load Balancer for the service. */ @@ -297,7 +307,10 @@ export abstract class NetworkMultipleTargetGroupsServiceBase extends CoreConstru this.validateInput(props); this.cluster = props.cluster || this.getDefaultCluster(this, props.vpc); + this.desiredCount = props.desiredCount || 1; + this.internalDesiredCount = props.desiredCount; + if (props.taskImageOptions) { this.logDriver = this.createLogDriver(props.taskImageOptions.enableLogging, props.taskImageOptions.logDriver); } diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts index 3248514931f4d..2f72c6345c469 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts @@ -3,6 +3,7 @@ import { IVpc } from '@aws-cdk/aws-ec2'; import { AwsLogDriver, BaseService, Cluster, ContainerImage, DeploymentController, ICluster, LogDriver, PropagatedTagSource, Secret } from '@aws-cdk/aws-ecs'; import { IQueue, Queue } from '@aws-cdk/aws-sqs'; import { CfnOutput, Duration, Stack } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. @@ -53,7 +54,10 @@ export interface QueueProcessingServiceBaseProps { /** * The desired number of instantiations of the task definition to keep running on the service. * - * @default 1 + * @default - If the feature flag, ECS_REMOVE_DEFAULT_DESIRED_COUNT is false, the default is 1; + * if true, the minScalingCapacity is 1 for all new services and uses the existing services desired count + * when updating an existing service. + * @deprecated - Use `minScalingCapacity` or a literal object instead. */ readonly desiredTaskCount?: number; @@ -109,10 +113,17 @@ export interface QueueProcessingServiceBaseProps { /** * Maximum capacity to scale to. * - * @default (desiredTaskCount * 2) + * @default - If the feature flag, ECS_REMOVE_DEFAULT_DESIRED_COUNT is false, the default is (desiredTaskCount * 2); if true, the default is 2. */ readonly maxScalingCapacity?: number + /** + * Minimum capacity to scale to. + * + * @default - If the feature flag, ECS_REMOVE_DEFAULT_DESIRED_COUNT is false, the default is the desiredTaskCount; if true, the default is 1. + */ + readonly minScalingCapacity?: number + /** * The intervals for scaling based on the SQS queue's ApproximateNumberOfMessagesVisible metric. * @@ -214,6 +225,7 @@ export abstract class QueueProcessingServiceBase extends CoreConstruct { /** * The minimum number of tasks to run. + * @deprecated - Use `minCapacity` instead. */ public readonly desiredCount: number; @@ -222,6 +234,11 @@ export abstract class QueueProcessingServiceBase extends CoreConstruct { */ public readonly maxCapacity: number; + /** + * The minimum number of instances for autoscaling to scale down to. + */ + public readonly minCapacity: number; + /** * The scaling interval for autoscaling based off an SQS Queue size. */ @@ -272,9 +289,21 @@ export abstract class QueueProcessingServiceBase extends CoreConstruct { this.environment = { ...(props.environment || {}), QUEUE_NAME: this.sqsQueue.queueName }; this.secrets = props.secrets; - // Determine the desired task count (minimum) and maximum scaling capacity this.desiredCount = props.desiredTaskCount ?? 1; - this.maxCapacity = props.maxScalingCapacity || (2 * this.desiredCount); + + // Determine the desired task count (minimum) and maximum scaling capacity + if (!this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT)) { + this.minCapacity = props.minScalingCapacity || this.desiredCount; + this.maxCapacity = props.maxScalingCapacity || (2 * this.desiredCount); + } else { + if (props.desiredTaskCount != null) { + this.minCapacity = props.minScalingCapacity || this.desiredCount; + this.maxCapacity = props.maxScalingCapacity || (2 * this.desiredCount); + } else { + this.minCapacity = props.minScalingCapacity || 1; + this.maxCapacity = props.maxScalingCapacity || 2; + } + } if (!this.desiredCount && !this.maxCapacity) { throw new Error('maxScalingCapacity must be set and greater than 0 if desiredCount is 0'); @@ -290,7 +319,7 @@ export abstract class QueueProcessingServiceBase extends CoreConstruct { * @param service the ECS/Fargate service for which to apply the autoscaling rules to */ protected configureAutoscalingForService(service: BaseService) { - const scalingTarget = service.autoScaleTaskCount({ maxCapacity: this.maxCapacity, minCapacity: this.desiredCount }); + const scalingTarget = service.autoScaleTaskCount({ maxCapacity: this.maxCapacity, minCapacity: this.minCapacity }); scalingTarget.scaleOnCpuUtilization('CpuScaling', { targetUtilizationPercent: 50, }); diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts index b9bdcf2d100ad..2915fce6a48ff 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts @@ -1,4 +1,5 @@ import { Ec2Service, Ec2TaskDefinition } from '@aws-cdk/aws-ecs'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { ApplicationLoadBalancedServiceBase, ApplicationLoadBalancedServiceBaseProps } from '../base/application-load-balanced-service-base'; @@ -117,9 +118,11 @@ export class ApplicationLoadBalancedEc2Service extends ApplicationLoadBalancedSe throw new Error('You must specify one of: taskDefinition or image'); } + const desiredCount = this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? this.internalDesiredCount : this.desiredCount; + this.service = new Ec2Service(this, 'Service', { cluster: this.cluster, - desiredCount: this.desiredCount, + desiredCount: desiredCount, taskDefinition: this.taskDefinition, assignPublicIp: false, serviceName: props.serviceName, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-multiple-target-groups-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-multiple-target-groups-ecs-service.ts index 6ed6b6b71802f..90f4afdd5fa8f 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-multiple-target-groups-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-multiple-target-groups-ecs-service.ts @@ -1,5 +1,6 @@ import { Ec2Service, Ec2TaskDefinition } from '@aws-cdk/aws-ecs'; import { ApplicationTargetGroup } from '@aws-cdk/aws-elasticloadbalancingv2'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { ApplicationMultipleTargetGroupsServiceBase, @@ -136,9 +137,11 @@ export class ApplicationMultipleTargetGroupsEc2Service extends ApplicationMultip } private createEc2Service(props: ApplicationMultipleTargetGroupsEc2ServiceProps): Ec2Service { + const desiredCount = this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? this.internalDesiredCount : this.desiredCount; + return new Ec2Service(this, 'Service', { cluster: this.cluster, - desiredCount: this.desiredCount, + desiredCount: desiredCount, taskDefinition: this.taskDefinition, assignPublicIp: false, serviceName: props.serviceName, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts index fae46b68e7380..881a346c74f8a 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts @@ -1,4 +1,5 @@ import { Ec2Service, Ec2TaskDefinition } from '@aws-cdk/aws-ecs'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { NetworkLoadBalancedServiceBase, NetworkLoadBalancedServiceBaseProps } from '../base/network-load-balanced-service-base'; @@ -115,9 +116,11 @@ export class NetworkLoadBalancedEc2Service extends NetworkLoadBalancedServiceBas throw new Error('You must specify one of: taskDefinition or image'); } + const desiredCount = this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? this.internalDesiredCount : this.desiredCount; + this.service = new Ec2Service(this, 'Service', { cluster: this.cluster, - desiredCount: this.desiredCount, + desiredCount: desiredCount, taskDefinition: this.taskDefinition, assignPublicIp: false, serviceName: props.serviceName, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-multiple-target-groups-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-multiple-target-groups-ecs-service.ts index eb8392d3b2148..f0d3b0a1571ce 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-multiple-target-groups-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-multiple-target-groups-ecs-service.ts @@ -1,5 +1,6 @@ import { Ec2Service, Ec2TaskDefinition } from '@aws-cdk/aws-ecs'; import { NetworkTargetGroup } from '@aws-cdk/aws-elasticloadbalancingv2'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { NetworkMultipleTargetGroupsServiceBase, @@ -136,9 +137,11 @@ export class NetworkMultipleTargetGroupsEc2Service extends NetworkMultipleTarget } private createEc2Service(props: NetworkMultipleTargetGroupsEc2ServiceProps): Ec2Service { + const desiredCount = this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? this.internalDesiredCount : this.desiredCount; + return new Ec2Service(this, 'Service', { cluster: this.cluster, - desiredCount: this.desiredCount, + desiredCount: desiredCount, taskDefinition: this.taskDefinition, assignPublicIp: false, serviceName: props.serviceName, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts index f9d9b98810aa0..6858813dfa9cc 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts @@ -1,4 +1,5 @@ import { Ec2Service, Ec2TaskDefinition } from '@aws-cdk/aws-ecs'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { QueueProcessingServiceBase, QueueProcessingServiceBaseProps } from '../base/queue-processing-service-base'; @@ -98,11 +99,14 @@ export class QueueProcessingEc2Service extends QueueProcessingServiceBase { logging: this.logDriver, }); + // The desiredCount should be removed from the fargate service when the feature flag is removed. + const desiredCount = this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? undefined : this.desiredCount; + // Create an ECS service with the previously defined Task Definition and configure // autoscaling based on cpu utilization and number of messages visible in the SQS queue. this.service = new Ec2Service(this, 'QueueProcessingService', { cluster: this.cluster, - desiredCount: this.desiredCount, + desiredCount: desiredCount, taskDefinition: this.taskDefinition, serviceName: props.serviceName, minHealthyPercent: props.minHealthyPercent, @@ -111,6 +115,7 @@ export class QueueProcessingEc2Service extends QueueProcessingServiceBase { enableECSManagedTags: props.enableECSManagedTags, deploymentController: props.deploymentController, }); + this.configureAutoscalingForService(this.service); this.grantPermissionsToService(this.service); } diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts index 2ae468bcae558..fbb68aef84b2f 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts @@ -1,5 +1,6 @@ import { ISecurityGroup, SubnetSelection } from '@aws-cdk/aws-ec2'; import { FargatePlatformVersion, FargateService, FargateTaskDefinition } from '@aws-cdk/aws-ecs'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { ApplicationLoadBalancedServiceBase, ApplicationLoadBalancedServiceBaseProps } from '../base/application-load-balanced-service-base'; @@ -153,9 +154,11 @@ export class ApplicationLoadBalancedFargateService extends ApplicationLoadBalanc throw new Error('You must specify one of: taskDefinition or image'); } + const desiredCount = this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? this.internalDesiredCount : this.desiredCount; + this.service = new FargateService(this, 'Service', { cluster: this.cluster, - desiredCount: this.desiredCount, + desiredCount: desiredCount, taskDefinition: this.taskDefinition, assignPublicIp: this.assignPublicIp, serviceName: props.serviceName, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-multiple-target-groups-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-multiple-target-groups-fargate-service.ts index 495049dfccfa8..6759e8e001376 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-multiple-target-groups-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-multiple-target-groups-fargate-service.ts @@ -1,5 +1,6 @@ import { FargatePlatformVersion, FargateService, FargateTaskDefinition } from '@aws-cdk/aws-ecs'; import { ApplicationTargetGroup } from '@aws-cdk/aws-elasticloadbalancingv2'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { ApplicationMultipleTargetGroupsServiceBase, @@ -168,9 +169,11 @@ export class ApplicationMultipleTargetGroupsFargateService extends ApplicationMu } private createFargateService(props: ApplicationMultipleTargetGroupsFargateServiceProps): FargateService { + const desiredCount = this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? this.internalDesiredCount : this.desiredCount; + return new FargateService(this, 'Service', { cluster: this.cluster, - desiredCount: this.desiredCount, + desiredCount: desiredCount, taskDefinition: this.taskDefinition, assignPublicIp: this.assignPublicIp, serviceName: props.serviceName, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts index 4aad4b31e7efe..404d5429acfed 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts @@ -1,5 +1,6 @@ import { SubnetSelection } from '@aws-cdk/aws-ec2'; import { FargatePlatformVersion, FargateService, FargateTaskDefinition } from '@aws-cdk/aws-ecs'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { NetworkLoadBalancedServiceBase, NetworkLoadBalancedServiceBaseProps } from '../base/network-load-balanced-service-base'; @@ -140,9 +141,11 @@ export class NetworkLoadBalancedFargateService extends NetworkLoadBalancedServic throw new Error('You must specify one of: taskDefinition or image'); } + const desiredCount = this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? this.internalDesiredCount : this.desiredCount; + this.service = new FargateService(this, 'Service', { cluster: this.cluster, - desiredCount: this.desiredCount, + desiredCount: desiredCount, taskDefinition: this.taskDefinition, assignPublicIp: this.assignPublicIp, serviceName: props.serviceName, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-multiple-target-groups-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-multiple-target-groups-fargate-service.ts index dab033b1938ce..4a4974af7cce3 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-multiple-target-groups-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-multiple-target-groups-fargate-service.ts @@ -1,5 +1,6 @@ import { FargatePlatformVersion, FargateService, FargateTaskDefinition } from '@aws-cdk/aws-ecs'; import { NetworkTargetGroup } from '@aws-cdk/aws-elasticloadbalancingv2'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { NetworkMultipleTargetGroupsServiceBase, @@ -168,9 +169,11 @@ export class NetworkMultipleTargetGroupsFargateService extends NetworkMultipleTa } private createFargateService(props: NetworkMultipleTargetGroupsFargateServiceProps): FargateService { + const desiredCount = this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? this.internalDesiredCount : this.desiredCount; + return new FargateService(this, 'Service', { cluster: this.cluster, - desiredCount: this.desiredCount, + desiredCount: desiredCount, taskDefinition: this.taskDefinition, assignPublicIp: this.assignPublicIp, serviceName: props.serviceName, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts index e3712b00ae4b1..6444d05f81da6 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts @@ -1,5 +1,6 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import { FargatePlatformVersion, FargateService, FargateTaskDefinition } from '@aws-cdk/aws-ecs'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { QueueProcessingServiceBase, QueueProcessingServiceBaseProps } from '../base/queue-processing-service-base'; @@ -128,11 +129,14 @@ export class QueueProcessingFargateService extends QueueProcessingServiceBase { logging: this.logDriver, }); + // The desiredCount should be removed from the fargate service when the feature flag is removed. + const desiredCount = this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? undefined : this.desiredCount; + // Create a Fargate service with the previously defined Task Definition and configure // autoscaling based on cpu utilization and number of messages visible in the SQS queue. this.service = new FargateService(this, 'QueueProcessingFargateService', { cluster: this.cluster, - desiredCount: this.desiredCount, + desiredCount: desiredCount, taskDefinition: this.taskDefinition, serviceName: props.serviceName, minHealthyPercent: props.minHealthyPercent, @@ -145,6 +149,7 @@ export class QueueProcessingFargateService extends QueueProcessingServiceBase { vpcSubnets: props.taskSubnets, assignPublicIp: props.assignPublicIp, }); + this.configureAutoscalingForService(this.service); this.grantPermissionsToService(this.service); } diff --git a/packages/@aws-cdk/aws-ecs-patterns/package.json b/packages/@aws-cdk/aws-ecs-patterns/package.json index bc2a816cd6b11..dcb4d8b436bda 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/package.json +++ b/packages/@aws-cdk/aws-ecs-patterns/package.json @@ -86,6 +86,7 @@ "@aws-cdk/aws-servicediscovery": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/core": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "constructs": "^3.2.0" }, "homepage": "https://github.com/aws/aws-cdk", @@ -103,6 +104,7 @@ "@aws-cdk/aws-servicediscovery": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/core": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "constructs": "^3.2.0" }, "engines": { diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.multiple-application-load-balanced-ecs-service.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.multiple-application-load-balanced-ecs-service.expected.json index 147a8bc8a7bfb..ec9686e054019 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.multiple-application-load-balanced-ecs-service.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.multiple-application-load-balanced-ecs-service.expected.json @@ -719,14 +719,12 @@ "Code": { "ZipFile": "import boto3, json, os, time\n\necs = boto3.client('ecs')\nautoscaling = boto3.client('autoscaling')\n\n\ndef lambda_handler(event, context):\n print(json.dumps(event))\n cluster = os.environ['CLUSTER']\n snsTopicArn = event['Records'][0]['Sns']['TopicArn']\n lifecycle_event = json.loads(event['Records'][0]['Sns']['Message'])\n instance_id = lifecycle_event.get('EC2InstanceId')\n if not instance_id:\n print('Got event without EC2InstanceId: %s', json.dumps(event))\n return\n\n instance_arn = container_instance_arn(cluster, instance_id)\n print('Instance %s has container instance ARN %s' % (lifecycle_event['EC2InstanceId'], instance_arn))\n\n if not instance_arn:\n return\n\n while has_tasks(cluster, instance_arn):\n time.sleep(10)\n\n try:\n print('Terminating instance %s' % instance_id)\n autoscaling.complete_lifecycle_action(\n LifecycleActionResult='CONTINUE',\n **pick(lifecycle_event, 'LifecycleHookName', 'LifecycleActionToken', 'AutoScalingGroupName'))\n except Exception as e:\n # Lifecycle action may have already completed.\n print(str(e))\n\n\ndef container_instance_arn(cluster, instance_id):\n \"\"\"Turn an instance ID into a container instance ARN.\"\"\"\n arns = ecs.list_container_instances(cluster=cluster, filter='ec2InstanceId==' + instance_id)['containerInstanceArns']\n if not arns:\n return None\n return arns[0]\n\n\ndef has_tasks(cluster, instance_arn):\n \"\"\"Return True if the instance is running tasks for the given cluster.\"\"\"\n instances = ecs.describe_container_instances(cluster=cluster, containerInstances=[instance_arn])['containerInstances']\n if not instances:\n return False\n instance = instances[0]\n\n if instance['status'] == 'ACTIVE':\n # Start draining, then try again later\n set_container_instance_to_draining(cluster, instance_arn)\n return True\n\n tasks = instance['runningTasksCount'] + instance['pendingTasksCount']\n print('Instance %s has %s tasks' % (instance_arn, tasks))\n\n return tasks > 0\n\n\ndef set_container_instance_to_draining(cluster, instance_arn):\n ecs.update_container_instances_state(\n cluster=cluster,\n containerInstances=[instance_arn], status='DRAINING')\n\n\ndef pick(dct, *keys):\n \"\"\"Pick a subset of a dict.\"\"\"\n return {k: v for k, v in dct.items() if k in keys}\n" }, - "Handler": "index.lambda_handler", "Role": { "Fn::GetAtt": [ "ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole2AC250B1", "Arn" ] }, - "Runtime": "python3.6", "Environment": { "Variables": { "CLUSTER": { @@ -734,6 +732,8 @@ } } }, + "Handler": "index.lambda_handler", + "Runtime": "python3.6", "Tags": [ { "Key": "Name", @@ -1128,7 +1128,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "EC2", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.scheduled-ecs-task.lit.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.scheduled-ecs-task.lit.expected.json index 5d4345573b0c5..53c05f9aeba8e 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.scheduled-ecs-task.lit.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.scheduled-ecs-task.lit.expected.json @@ -536,14 +536,12 @@ "Code": { "ZipFile": "import boto3, json, os, time\n\necs = boto3.client('ecs')\nautoscaling = boto3.client('autoscaling')\n\n\ndef lambda_handler(event, context):\n print(json.dumps(event))\n cluster = os.environ['CLUSTER']\n snsTopicArn = event['Records'][0]['Sns']['TopicArn']\n lifecycle_event = json.loads(event['Records'][0]['Sns']['Message'])\n instance_id = lifecycle_event.get('EC2InstanceId')\n if not instance_id:\n print('Got event without EC2InstanceId: %s', json.dumps(event))\n return\n\n instance_arn = container_instance_arn(cluster, instance_id)\n print('Instance %s has container instance ARN %s' % (lifecycle_event['EC2InstanceId'], instance_arn))\n\n if not instance_arn:\n return\n\n while has_tasks(cluster, instance_arn):\n time.sleep(10)\n\n try:\n print('Terminating instance %s' % instance_id)\n autoscaling.complete_lifecycle_action(\n LifecycleActionResult='CONTINUE',\n **pick(lifecycle_event, 'LifecycleHookName', 'LifecycleActionToken', 'AutoScalingGroupName'))\n except Exception as e:\n # Lifecycle action may have already completed.\n print(str(e))\n\n\ndef container_instance_arn(cluster, instance_id):\n \"\"\"Turn an instance ID into a container instance ARN.\"\"\"\n arns = ecs.list_container_instances(cluster=cluster, filter='ec2InstanceId==' + instance_id)['containerInstanceArns']\n if not arns:\n return None\n return arns[0]\n\n\ndef has_tasks(cluster, instance_arn):\n \"\"\"Return True if the instance is running tasks for the given cluster.\"\"\"\n instances = ecs.describe_container_instances(cluster=cluster, containerInstances=[instance_arn])['containerInstances']\n if not instances:\n return False\n instance = instances[0]\n\n if instance['status'] == 'ACTIVE':\n # Start draining, then try again later\n set_container_instance_to_draining(cluster, instance_arn)\n return True\n\n tasks = instance['runningTasksCount'] + instance['pendingTasksCount']\n print('Instance %s has %s tasks' % (instance_arn, tasks))\n\n return tasks > 0\n\n\ndef set_container_instance_to_draining(cluster, instance_arn):\n ecs.update_container_instances_state(\n cluster=cluster,\n containerInstances=[instance_arn], status='DRAINING')\n\n\ndef pick(dct, *keys):\n \"\"\"Pick a subset of a dict.\"\"\"\n return {k: v for k, v in dct.items() if k in keys}\n" }, - "Handler": "index.lambda_handler", "Role": { "Fn::GetAtt": [ "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole94543EDA", "Arn" ] }, - "Runtime": "python3.6", "Environment": { "Variables": { "CLUSTER": { @@ -551,6 +549,8 @@ } } }, + "Handler": "index.lambda_handler", + "Runtime": "python3.6", "Tags": [ { "Key": "Name", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts index 959a6348fde11..6f5a4aa0b3337 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts @@ -1,4 +1,4 @@ -import { arrayWith, expect, haveResource, haveResourceLike, objectLike } from '@aws-cdk/assert'; +import { ABSENT, arrayWith, expect, haveResource, haveResourceLike, objectLike } from '@aws-cdk/assert'; import { Certificate } from '@aws-cdk/aws-certificatemanager'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; @@ -6,6 +6,7 @@ import { ApplicationLoadBalancer, ApplicationProtocol, NetworkLoadBalancer } fro import { PublicHostedZone } from '@aws-cdk/aws-route53'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Test } from 'nodeunit'; import * as ecsPatterns from '../../lib'; @@ -60,6 +61,102 @@ export = { test.done(); }, + 'ApplicationLoadBalancedEc2Service desiredCount can be undefined when feature flag is set'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + stack.node.setContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT, true); + + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + + // WHEN + new ecsPatterns.ApplicationLoadBalancedEc2Service(stack, 'Service', { + cluster, + memoryLimitMiB: 1024, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry('test'), + }, + }); + + expect(stack).to(haveResource('AWS::ECS::Service', { + DesiredCount: ABSENT, + })); + + test.done(); + }, + + 'ApplicationLoadBalancedFargateService desiredCount can be undefined when feature flag is set'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + stack.node.setContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT, true); + + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + + // WHEN + new ecsPatterns.ApplicationLoadBalancedFargateService(stack, 'Service', { + cluster, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry('test'), + }, + }); + + expect(stack).to(haveResource('AWS::ECS::Service', { + DesiredCount: ABSENT, + })); + + test.done(); + }, + + 'NetworkLoadBalancedEc2Service desiredCount can be undefined when feature flag is set'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + stack.node.setContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT, true); + + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + + // WHEN + new ecsPatterns.NetworkLoadBalancedEc2Service(stack, 'Service', { + cluster, + memoryLimitMiB: 1024, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry('test'), + }, + }); + + expect(stack).to(haveResource('AWS::ECS::Service', { + DesiredCount: ABSENT, + })); + + test.done(); + }, + + 'NetworkLoadBalancedFargateService desiredCount can be undefined when feature flag is set'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + stack.node.setContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT, true); + + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + + // WHEN + new ecsPatterns.NetworkLoadBalancedFargateService(stack, 'Service', { + cluster, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry('test'), + }, + }); + + expect(stack).to(haveResource('AWS::ECS::Service', { + DesiredCount: ABSENT, + })); + + test.done(); + }, + 'set vpc instead of cluster'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts index d449cc27db2c4..2c410f6581b1e 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts @@ -1,8 +1,9 @@ -import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; +import { ABSENT, expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; import * as sqs from '@aws-cdk/aws-sqs'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Test } from 'nodeunit'; import * as ecsPatterns from '../../lib'; @@ -80,6 +81,31 @@ export = { test.done(); }, + 'test ECS queue worker service construct - with remove default desiredCount feature flag'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + stack.node.setContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT, true); + + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + + // WHEN + new ecsPatterns.QueueProcessingEc2Service(stack, 'Service', { + cluster, + memoryLimitMiB: 512, + image: ecs.ContainerImage.fromRegistry('test'), + }); + + // THEN - QueueWorker is of EC2 launch type, and desiredCount is not defined on the Ec2Service. + expect(stack).to(haveResource('AWS::ECS::Service', { + DesiredCount: ABSENT, + LaunchType: 'EC2', + })); + + test.done(); + }, + 'test ECS queue worker service construct - with optional props for queues'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.asset-image.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.asset-image.expected.json index 364f0a27d8b15..4fdde5753fdf6 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.asset-image.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.asset-image.expected.json @@ -637,7 +637,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", @@ -752,4 +751,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.executionrole.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.executionrole.expected.json index 2d70c2c40cae2..93c7a7307a6ee 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.executionrole.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.executionrole.expected.json @@ -589,7 +589,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.l3-autocreate.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.l3-autocreate.expected.json index 84a73622ee2d8..5813cd78e41f3 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.l3-autocreate.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.l3-autocreate.expected.json @@ -228,7 +228,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", @@ -894,7 +893,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.l3-vpconly.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.l3-vpconly.expected.json index c549746d8769e..5556df70a59b7 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.l3-vpconly.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.l3-vpconly.expected.json @@ -583,7 +583,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", @@ -1249,7 +1248,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", @@ -1560,7 +1558,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.l3.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.l3.expected.json index c79a622e159a9..40d86be5d331b 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.l3.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.l3.expected.json @@ -586,7 +586,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json index 532377e2accdd..1d670f79f58a6 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json @@ -664,7 +664,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": true, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.multiple-network-load-balanced-fargate-service.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.multiple-network-load-balanced-fargate-service.expected.json index 815cfe99b94f6..2cdd6991792e8 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.multiple-network-load-balanced-fargate-service.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.multiple-network-load-balanced-fargate-service.expected.json @@ -597,7 +597,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-isolated.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-isolated.expected.json index b1d88ed107154..6124fba473584 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-isolated.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-isolated.expected.json @@ -903,7 +903,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "LaunchType": "FARGATE", "NetworkConfiguration": { diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-public.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-public.expected.json index fd8f791e3a868..de873260aa209 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-public.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-public.expected.json @@ -753,7 +753,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "LaunchType": "FARGATE", "NetworkConfiguration": { diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service.expected.json index 889eeefcd985e..6fbcb79ecfa00 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service.expected.json @@ -594,7 +594,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "LaunchType": "FARGATE", "NetworkConfiguration": { diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.special-listener.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.special-listener.expected.json index 76e5e6a13aaf7..5075f6511573b 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.special-listener.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.special-listener.expected.json @@ -539,7 +539,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", @@ -826,7 +825,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts index 9d2d5dd0747a6..27671ae22e70d 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts @@ -1,8 +1,9 @@ -import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; +import { ABSENT, expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; import * as sqs from '@aws-cdk/aws-sqs'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Test } from 'nodeunit'; import * as ecsPatterns from '../../lib'; @@ -102,6 +103,30 @@ export = { test.done(); }, + 'test fargate queue worker service construct - with remove default desiredCount feature flag'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + stack.node.setContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT, true); + + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + + // WHEN + new ecsPatterns.QueueProcessingFargateService(stack, 'Service', { + cluster, + memoryLimitMiB: 512, + image: ecs.ContainerImage.fromRegistry('test'), + }); + + // THEN - QueueWorker is of FARGATE launch type, and desiredCount is not defined on the FargateService. + expect(stack).to(haveResource('AWS::ECS::Service', { + DesiredCount: ABSENT, + LaunchType: 'FARGATE', + })); + + test.done(); + }, + 'test fargate queue worker service construct - with optional props for queues'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -199,6 +224,97 @@ export = { test.done(); }, + 'test Fargate queue worker service construct - without desiredCount specified'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const queue = new sqs.Queue(stack, 'fargate-test-queue', { + queueName: 'fargate-test-sqs-queue', + }); + + // WHEN + new ecsPatterns.QueueProcessingFargateService(stack, 'Service', { + cluster, + memoryLimitMiB: 512, + image: ecs.ContainerImage.fromRegistry('test'), + command: ['-c', '4', 'amazon.com'], + enableLogging: false, + environment: { + TEST_ENVIRONMENT_VARIABLE1: 'test environment variable 1 value', + TEST_ENVIRONMENT_VARIABLE2: 'test environment variable 2 value', + }, + queue, + maxScalingCapacity: 5, + minScalingCapacity: 2, + minHealthyPercent: 60, + maxHealthyPercent: 150, + serviceName: 'fargate-test-service', + family: 'fargate-task-family', + platformVersion: ecs.FargatePlatformVersion.VERSION1_4, + deploymentController: { + type: ecs.DeploymentControllerType.CODE_DEPLOY, + }, + }); + + // THEN - QueueWorker is of FARGATE launch type, an SQS queue is created and all optional properties are set. + expect(stack).to(haveResource('AWS::ECS::Service', { + DeploymentConfiguration: { + MinimumHealthyPercent: 60, + MaximumPercent: 150, + }, + LaunchType: 'FARGATE', + ServiceName: 'fargate-test-service', + PlatformVersion: ecs.FargatePlatformVersion.VERSION1_4, + DeploymentController: { + Type: 'CODE_DEPLOY', + }, + })); + + expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + MaxCapacity: 5, + MinCapacity: 2, + })); + + expect(stack).to(haveResource('AWS::SQS::Queue', { QueueName: 'fargate-test-sqs-queue' })); + + expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Command: [ + '-c', + '4', + 'amazon.com', + ], + Environment: [ + { + Name: 'TEST_ENVIRONMENT_VARIABLE1', + Value: 'test environment variable 1 value', + }, + { + Name: 'TEST_ENVIRONMENT_VARIABLE2', + Value: 'test environment variable 2 value', + }, + { + Name: 'QUEUE_NAME', + Value: { + 'Fn::GetAtt': [ + 'fargatetestqueue28B43841', + 'QueueName', + ], + }, + }, + ], + Image: 'test', + }, + ], + Family: 'fargate-task-family', + })); + + test.done(); + }, + 'test Fargate queue worker service construct - with optional props'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 438f63e14e009..efe5126caf004 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -237,23 +237,35 @@ const container = ec2TaskDefinition.addContainer("WebContainer", { You can specify container properties when you add them to the task definition, or with various methods, e.g.: +To add a port mapping when adding a container to the task definition, specify the `portMappings` option: + +```ts +taskDefinition.addContainer("WebContainer", { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + memoryLimitMiB: 1024, + portMappings: [{ containerPort: 3000 }] +}); +``` + +To add port mappings directly to a container definition, call `addPortMappings()`: + ```ts container.addPortMappings({ containerPort: 3000 -}) +}); ``` To add data volumes to a task definition, call `addVolume()`: ```ts -const volume = ecs.Volume("Volume", { +const volume = { // Use an Elastic FileSystem name: "mydatavolume", efsVolumeConfiguration: ecs.EfsVolumeConfiguration({ fileSystemId: "EFS" // ... other options here ... }) -}); +}; const container = fargateTaskDefinition.addVolume("mydatavolume"); ``` diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 983489743482f..9911a49a039cf 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -288,6 +288,12 @@ export interface ContainerDefinitionOptions { * @default - No GPUs assigned. */ readonly gpuCount?: number; + + /** + * The port mappings to add to the container definition. + * @default - No ports are mapped. + */ + readonly portMappings?: PortMapping[]; } /** @@ -433,6 +439,10 @@ export class ContainerDefinition extends CoreConstruct { } props.taskDefinition._linkContainer(this); + + if (props.portMappings) { + this.addPortMappings(...props.portMappings); + } } /** diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts index 18c6df350fb4e..4cf4de8a83292 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts @@ -229,9 +229,9 @@ export class Ec2Service extends BaseService implements IEc2Service { this.addPlacementConstraints(...props.placementConstraints || []); this.addPlacementStrategies(...props.placementStrategies || []); - if (!this.taskDefinition.defaultContainer) { - throw new Error('A TaskDefinition must have at least one essential container'); - } + this.node.addValidation({ + validate: () => !this.taskDefinition.defaultContainer ? ['A TaskDefinition must have at least one essential container'] : [], + }); } /** @@ -249,7 +249,7 @@ export class Ec2Service extends BaseService implements IEc2Service { } /** - * Adds one or more placement contstraints to use for tasks in the service. For more information, see + * Adds one or more placement constraints to use for tasks in the service. For more information, see * [Amazon ECS Task Placement Constraints](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-placement-constraints.html). */ public addPlacementConstraints(...constraints: PlacementConstraint[]) { diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts index 1db94fc5286e0..793fb633e83d0 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -172,9 +172,9 @@ export class FargateService extends BaseService implements IFargateService { this.configureAwsVpcNetworkingWithSecurityGroups(props.cluster.vpc, props.assignPublicIp, props.vpcSubnets, securityGroups); - if (!props.taskDefinition.defaultContainer) { - throw new Error('A TaskDefinition must have at least one essential container'); - } + this.node.addValidation({ + validate: () => !this.taskDefinition.defaultContainer ? ['A TaskDefinition must have at least one essential container'] : [], + }); } } diff --git a/packages/@aws-cdk/aws-ecs/lib/images/ecr.ts b/packages/@aws-cdk/aws-ecs/lib/images/ecr.ts index e8dc339bf9e82..b9786f13e2816 100644 --- a/packages/@aws-cdk/aws-ecs/lib/images/ecr.ts +++ b/packages/@aws-cdk/aws-ecs/lib/images/ecr.ts @@ -22,10 +22,14 @@ export class EcrImage extends ContainerImage { /** * Constructs a new instance of the EcrImage class. */ - constructor(private readonly repository: ecr.IRepository, private readonly tag: string) { + constructor(private readonly repository: ecr.IRepository, private readonly tagOrDigest: string) { super(); - this.imageName = this.repository.repositoryUriForTag(this.tag); + if (tagOrDigest?.startsWith('sha256:')) { + this.imageName = this.repository.repositoryUriForDigest(this.tagOrDigest); + } else { + this.imageName = this.repository.repositoryUriForTag(this.tagOrDigest); + } } public bind(_scope: CoreConstruct, containerDefinition: ContainerDefinition): ContainerImageConfig { diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index f7408e518f2fe..ec829127f9e84 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -73,6 +73,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-s3-deployment": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "@types/nodeunit": "^0.0.31", "@types/proxyquire": "^1.3.28", "cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-ecs/test/container-definition.test.ts b/packages/@aws-cdk/aws-ecs/test/container-definition.test.ts index c167720e362cf..81e9f274160b7 100644 --- a/packages/@aws-cdk/aws-ecs/test/container-definition.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/container-definition.test.ts @@ -6,6 +6,7 @@ import * as s3 from '@aws-cdk/aws-s3'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import * as ssm from '@aws-cdk/aws-ssm'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as ecs from '../lib'; @@ -702,6 +703,55 @@ describe('container definition', () => { }); + test('can add port mappings to the container definition by props', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + + // WHEN + taskDefinition.addContainer('cont', { + image: ecs.ContainerImage.fromRegistry('test'), + memoryLimitMiB: 1024, + portMappings: [{ containerPort: 80 }], + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + PortMappings: [{ ContainerPort: 80 }], + }, + ], + }); + }); + + test('can add port mappings using props and addPortMappings and both are included', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + + // WHEN + const containerDefinition = taskDefinition.addContainer('cont', { + image: ecs.ContainerImage.fromRegistry('test'), + memoryLimitMiB: 1024, + portMappings: [{ containerPort: 80 }], + }); + + containerDefinition.addPortMappings({ containerPort: 443 }); + + // THEN + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + PortMappings: [ + { ContainerPort: 80 }, + { ContainerPort: 443 }, + ], + }, + ], + }); + }); + describe('Environment Files', () => { describe('with EC2 task definitions', () => { test('can add asset environment file to the container definition', () => { @@ -1643,7 +1693,7 @@ describe('container definition', () => { }); }); - testFutureBehavior('can use a DockerImageAsset directly for a container image', { '@aws-cdk/aws-ecr-assets:dockerIgnoreSupport': true }, cdk.App, (app) => { + testFutureBehavior('can use a DockerImageAsset directly for a container image', { [cxapi.DOCKER_IGNORE_SUPPORT]: true }, cdk.App, (app) => { // GIVEN const stack = new cdk.Stack(app, 'Stack'); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts index 2279245aebaa4..d88c5ecafd1d7 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elb from '@aws-cdk/aws-elasticloadbalancing'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; @@ -541,14 +541,48 @@ nodeunitShim({ const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + // Errors on validation, not on construction. + new ecs.Ec2Service(stack, 'Ec2Service', { + cluster, + taskDefinition, + }); + // THEN test.throws(() => { - new ecs.Ec2Service(stack, 'Ec2Service', { - cluster, - taskDefinition, - }); + expect(stack); + }, /one essential container/); + + test.done(); + }, + + 'allows adding the default container after creating the service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + + new ecs.Ec2Service(stack, 'FargateService', { + cluster, + taskDefinition, }); + // Add the container *after* creating the service + taskDefinition.addContainer('main', { + image: ecs.ContainerImage.fromRegistry('somecontainer'), + memoryReservationMiB: 10, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Name: 'main', + }, + ], + })); + test.done(); }, diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-task-definition.test.ts b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-task-definition.test.ts index bea2e3c0733e5..bd13e8b83b524 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-task-definition.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-task-definition.test.ts @@ -6,6 +6,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import * as ssm from '@aws-cdk/aws-ssm'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as ecs from '../../lib'; @@ -530,6 +531,146 @@ describe('ec2 task definition', () => { }); + test('correctly sets containers from ECR repository using an image tag', () => { + // GIVEN + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromEcrRepository(new Repository(stack, 'myECRImage'), 'myTag'), + memoryLimitMiB: 512, + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [{ + Essential: true, + Memory: 512, + Image: { + 'Fn::Join': [ + '', + [ + { + 'Fn::Select': [ + 4, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.dkr.ecr.', + { + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + '/', + { + Ref: 'myECRImage7DEAE474', + }, + ':myTag', + ], + ], + }, + Name: 'web', + }], + }); + }); + + test('correctly sets containers from ECR repository using an image digest', () => { + // GIVEN + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromEcrRepository(new Repository(stack, 'myECRImage'), 'sha256:94afd1f2e64d908bc90dbca0035a5b567EXAMPLE'), + memoryLimitMiB: 512, + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [{ + Essential: true, + Memory: 512, + Image: { + 'Fn::Join': [ + '', + [ + { + 'Fn::Select': [ + 4, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.dkr.ecr.', + { + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + '/', + { + Ref: 'myECRImage7DEAE474', + }, + '@sha256:94afd1f2e64d908bc90dbca0035a5b567EXAMPLE', + ], + ], + }, + Name: 'web', + }], + }); + }); + test('correctly sets containers from ECR repository using default props', () => { // GIVEN const stack = new cdk.Stack(); @@ -585,7 +726,7 @@ describe('ec2 task definition', () => { }); - testFutureBehavior('correctly sets containers from asset using default props', { '@aws-cdk/aws-ecr-assets:dockerIgnoreSupport': true }, cdk.App, (app) => { + testFutureBehavior('correctly sets containers from asset using default props', { [cxapi.DOCKER_IGNORE_SUPPORT]: true }, cdk.App, (app) => { // GIVEN const stack = new cdk.Stack(app, 'Stack'); diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts index acddf713d6d0a..c7aa1fc633a1d 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts @@ -482,14 +482,46 @@ nodeunitShim({ const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + // Errors on validation, not on construction. + new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + }); + // THEN test.throws(() => { - new ecs.FargateService(stack, 'FargateService', { - cluster, - taskDefinition, - }); + expect(stack); + }, /one essential container/); + + test.done(); + }, + + 'allows adding the default container after creating the service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + + new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, }); + // Add the container *after* creating the service + taskDefinition.addContainer('main', { + image: ecs.ContainerImage.fromRegistry('somecontainer'), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Name: 'main', + }, + ], + })); + test.done(); }, diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts index d333fd9df89e2..e2d40e4ef52f6 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts @@ -15,13 +15,12 @@ const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', { cpu: 512, }); -const container = taskDefinition.addContainer('web', { +taskDefinition.addContainer('web', { image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), -}); - -container.addPortMappings({ - containerPort: 80, - protocol: ecs.Protocol.TCP, + portMappings: [{ + containerPort: 80, + protocol: ecs.Protocol.TCP, + }], }); const service = new ecs.FargateService(stack, 'Service', { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md index ac397ba62bd94..e0f3b1f4e5ea5 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md @@ -182,6 +182,9 @@ lb.addRedirect({ If you do not provide any options for this method, it redirects HTTP port 80 to HTTPS port 443. +By default all ingress traffic will be allowed on the source port. If you want to be more selective with your +ingress rules then set `open: false` and use the listener's `connections` object to selectively grant access to the listener. + ## Defining a Network Load Balancer Network Load Balancers are defined in a similar way to Application Load @@ -243,6 +246,33 @@ const group = listener.addTargets('AppFleet', { group.addTarget(asg2); ``` +### Sticky sessions for your Application Load Balancer + +By default, an Application Load Balancer routes each request independently to a registered target based on the chosen load-balancing algorithm. However, you can use the sticky session feature (also known as session affinity) to enable the load balancer to bind a user's session to a specific target. This ensures that all requests from the user during the session are sent to the same target. This feature is useful for servers that maintain state information in order to provide a continuous experience to clients. To use sticky sessions, the client must support cookies. + +Application Load Balancers support both duration-based cookies (`lb_cookie`) and application-based cookies (`app_cookie`). The key to managing sticky sessions is determining how long your load balancer should consistently route the user's request to the same target. Sticky sessions are enabled at the target group level. You can use a combination of duration-based stickiness, application-based stickiness, and no stickiness across all of your target groups. + +```ts +// Target group with duration-based stickiness with load-balancer generated cookie +const tg1 = new elbv2.ApplicationTargetGroup(stack, 'TG1', { + targetType: elbv2.TargetType.INSTANCE, + port: 80, + stickinessCookieDuration: cdk.Duration.minutes(5), + vpc, +}); + +// Target group with application-based stickiness +const tg2 = new elbv2.ApplicationTargetGroup(stack, 'TG2', { + targetType: elbv2.TargetType.INSTANCE, + port: 80, + stickinessCookieDuration: cdk.Duration.minutes(5), + stickinessCookieName: 'MyDeliciousCookie', + vpc, +}); +``` + +For more information see: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/sticky-sessions.html#application-based-stickiness + ## Using Lambda Targets To use a Lambda Function as a target, use the integration class in the diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index 1cd8a91c932aa..1844314e1f560 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -363,6 +363,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis protocol: props.protocol, slowStart: props.slowStart, stickinessCookieDuration: props.stickinessCookieDuration, + stickinessCookieName: props.stickinessCookieName, targetGroupName: props.targetGroupName, targets: props.targets, vpc: this.loadBalancer.vpc, @@ -813,6 +814,20 @@ export interface AddApplicationTargetsProps extends AddRuleProps { */ readonly stickinessCookieDuration?: Duration; + /** + * The name of an application-based stickiness cookie. + * + * Names that start with the following prefixes are not allowed: AWSALB, AWSALBAPP, + * and AWSALBTG; they're reserved for use by the load balancer. + * + * Note: `stickinessCookieName` parameter depends on the presence of `stickinessCookieDuration` parameter. + * If `stickinessCookieDuration` is not set, `stickinessCookieName` will be omitted. + * + * @default - If `stickinessCookieDuration` is set, a load-balancer generated cookie is used. Otherwise, no stickiness is defined. + * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/sticky-sessions.html + */ + readonly stickinessCookieName?: string; + /** * The targets to add to this target group. * diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts index 8f0ccf963cc5b..4ad4dcb5fa081 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts @@ -119,7 +119,7 @@ export class ApplicationLoadBalancer extends BaseLoadBalancer implements IApplic return this.addListener(`Redirect${sourcePort}To${targetPort}`, { protocol: props.sourceProtocol ?? ApplicationProtocol.HTTP, port: sourcePort, - open: true, + open: props.open ?? true, defaultAction: ListenerAction.redirect({ port: targetPort, protocol: props.targetProtocol ?? ApplicationProtocol.HTTPS, @@ -665,4 +665,19 @@ export interface ApplicationLoadBalancerRedirectConfig { */ readonly targetPort?: number; + /** + * Allow anyone to connect to this listener + * + * If this is specified, the listener will be opened up to anyone who can reach it. + * For internal load balancers this is anyone in the same VPC. For public load + * balancers, this is anyone on the internet. + * + * If you want to be more selective about who can access this load + * balancer, set this to `false` and use the listener's `connections` + * object to selectively grant access to the listener. + * + * @default true + */ + readonly open?: boolean; + } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts index a1d3de25bf82d..74938a08ee745 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts @@ -1,6 +1,6 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; -import { Annotations, Duration } from '@aws-cdk/core'; +import { Annotations, Duration, Token } from '@aws-cdk/core'; import { IConstruct, Construct } from 'constructs'; import { ApplicationELBMetrics } from '../elasticloadbalancingv2-canned-metrics.generated'; import { @@ -57,6 +57,20 @@ export interface ApplicationTargetGroupProps extends BaseTargetGroupProps { */ readonly stickinessCookieDuration?: Duration; + /** + * The name of an application-based stickiness cookie. + * + * Names that start with the following prefixes are not allowed: AWSALB, AWSALBAPP, + * and AWSALBTG; they're reserved for use by the load balancer. + * + * Note: `stickinessCookieName` parameter depends on the presence of `stickinessCookieDuration` parameter. + * If `stickinessCookieDuration` is not set, `stickinessCookieName` will be omitted. + * + * @default - If `stickinessCookieDuration` is set, a load-balancer generated cookie is used. Otherwise, no stickiness is defined. + * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/sticky-sessions.html + */ + readonly stickinessCookieName?: string; + /** * The targets to add to this target group. * @@ -109,10 +123,13 @@ export class ApplicationTargetGroup extends TargetGroupBase implements IApplicat if (props) { if (props.slowStart !== undefined) { + if (props.slowStart.toSeconds() < 30 || props.slowStart.toSeconds() > 900) { + throw new Error('Slow start duration value must be between 30 and 900 seconds.'); + } this.setAttribute('slow_start.duration_seconds', props.slowStart.toSeconds().toString()); } - if (props.stickinessCookieDuration !== undefined) { - this.enableCookieStickiness(props.stickinessCookieDuration); + if (props.stickinessCookieDuration) { + this.enableCookieStickiness(props.stickinessCookieDuration, props.stickinessCookieName); } this.addTarget(...(props.targets || [])); } @@ -129,12 +146,34 @@ export class ApplicationTargetGroup extends TargetGroupBase implements IApplicat } /** - * Enable sticky routing via a cookie to members of this target group + * Enable sticky routing via a cookie to members of this target group. + * + * Note: If the `cookieName` parameter is set, application-based stickiness will be applied, + * otherwise it defaults to duration-based stickiness attributes (`lb_cookie`). + * + * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/sticky-sessions.html */ - public enableCookieStickiness(duration: Duration) { + public enableCookieStickiness(duration: Duration, cookieName?: string) { + if (duration.toSeconds() < 1 || duration.toSeconds() > 604800) { + throw new Error('Stickiness cookie duration value must be between 1 second and 7 days (604800 seconds).'); + } + if (cookieName !== undefined) { + if (!Token.isUnresolved(cookieName) && (cookieName.startsWith('AWSALB') || cookieName.startsWith('AWSALBAPP') || cookieName.startsWith('AWSALBTG'))) { + throw new Error('App cookie names that start with the following prefixes are not allowed: AWSALB, AWSALBAPP, and AWSALBTG; they\'re reserved for use by the load balancer.'); + } + if (cookieName === '') { + throw new Error('App cookie name cannot be an empty string.'); + } + } this.setAttribute('stickiness.enabled', 'true'); - this.setAttribute('stickiness.type', 'lb_cookie'); - this.setAttribute('stickiness.lb_cookie.duration_seconds', duration.toSeconds().toString()); + if (cookieName) { + this.setAttribute('stickiness.type', 'app_cookie'); + this.setAttribute('stickiness.app_cookie.cookie_name', cookieName); + this.setAttribute('stickiness.app_cookie.duration_seconds', duration.toSeconds().toString()); + } else { + this.setAttribute('stickiness.type', 'lb_cookie'); + this.setAttribute('stickiness.lb_cookie.duration_seconds', duration.toSeconds().toString()); + } } /** diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts index b9de0961423ec..3e7b639cb1a8f 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts @@ -71,7 +71,7 @@ describe('tests', () => { }); }); - test('Listener default to open - IPv4 and IPv6 (dualstack)', () => { + test('Listener default to open - IPv4 and IPv6 (dual stack)', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -316,7 +316,7 @@ describe('tests', () => { }); }); - test('Enable stickiness for targets', () => { + test('Enable alb stickiness for targets', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -349,6 +349,43 @@ describe('tests', () => { }); }); + test('Enable app stickiness for targets', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Stack'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + const listener = lb.addListener('Listener', { port: 80 }); + + // WHEN + const group = listener.addTargets('Group', { + port: 80, + targets: [new FakeSelfRegisteringTarget(stack, 'Target', vpc)], + }); + group.enableCookieStickiness(cdk.Duration.hours(1), 'MyDeliciousCookie'); + + // THEN + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + TargetGroupAttributes: [ + { + Key: 'stickiness.enabled', + Value: 'true', + }, + { + Key: 'stickiness.type', + Value: 'app_cookie', + }, + { + Key: 'stickiness.app_cookie.cookie_name', + Value: 'MyDeliciousCookie', + }, + { + Key: 'stickiness.app_cookie.duration_seconds', + Value: '3600', + }, + ], + }); + }); + test('Enable health check for targets', () => { // GIVEN const stack = new cdk.Stack(); @@ -690,6 +727,31 @@ describe('tests', () => { }); }); + test('Can supress default ingress rules on a simple redirect response', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Stack'); + + const loadBalancer = new elbv2.ApplicationLoadBalancer(stack, 'LB', { + vpc, + }); + + // WHEN + loadBalancer.addRedirect({ open: false }); + + // THEN + expect(stack).not.toHaveResourceLike('AWS::EC2::SecurityGroup', { + SecurityGroupIngress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'Allow from anyone on port 80', + IpProtocol: 'tcp', + }, + ], + }); + + }); + test('Can add simple redirect responses with custom values', () => { // GIVEN const stack = new cdk.Stack(); @@ -823,7 +885,7 @@ describe('tests', () => { }); }); - test('Throws when specifying both target groups and fixed reponse', () => { + test('Throws when specifying both target groups and fixed response', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -868,7 +930,7 @@ describe('tests', () => { })).toThrowError('Priority must have value greater than or equal to 1'); }); - test('Throws when specifying both target groups and redirect reponse', () => { + test('Throws when specifying both target groups and redirect response', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -970,7 +1032,7 @@ describe('tests', () => { }); }); - test('Can add additional certificates via addCertficateArns to application listener', () => { + test('Can add additional certificates via addCertificateArns to application listener', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -1050,7 +1112,7 @@ describe('tests', () => { })).toThrowError('Both `pathPatterns` and `pathPattern` are specified, specify only one'); }); - test('Add additonal condition to listener rule', () => { + test('Add additional condition to listener rule', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -1244,7 +1306,7 @@ describe('tests', () => { }); }); - test('Can exist together legacy style conditions and modan style conditions', () => { + test('Can exist together legacy style conditions and modern style conditions', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts index 38b328ec153a4..f017a4a67f3fa 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts @@ -4,8 +4,12 @@ import { Metric } from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; +import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as elbv2 from '../../lib'; +const s3GrantWriteCtx = { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true }; + describe('tests', () => { test('Trivial construction: internet facing', () => { // GIVEN @@ -122,9 +126,9 @@ describe('tests', () => { }); }); - test('Access logging', () => { + testFutureBehavior('Access logging', s3GrantWriteCtx, cdk.App, (app) => { // GIVEN - const stack = new cdk.Stack(undefined, undefined, { env: { region: 'us-east-1' } }); + const stack = new cdk.Stack(app, undefined, { env: { region: 'us-east-1' } }); const vpc = new ec2.Vpc(stack, 'Stack'); const bucket = new s3.Bucket(stack, 'AccessLoggingBucket'); const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); @@ -154,7 +158,7 @@ describe('tests', () => { Version: '2012-10-17', Statement: [ { - Action: ['s3:PutObject*', 's3:Abort*'], + Action: ['s3:PutObject', 's3:Abort*'], Effect: 'Allow', Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::127311923021:root']] } }, Resource: { @@ -172,9 +176,9 @@ describe('tests', () => { }, ResourcePart.CompleteDefinition); }); - test('access logging with prefix', () => { + testFutureBehavior('access logging with prefix', s3GrantWriteCtx, cdk.App, (app) => { // GIVEN - const stack = new cdk.Stack(undefined, undefined, { env: { region: 'us-east-1' } }); + const stack = new cdk.Stack(app, undefined, { env: { region: 'us-east-1' } }); const vpc = new ec2.Vpc(stack, 'Stack'); const bucket = new s3.Bucket(stack, 'AccessLoggingBucket'); const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); @@ -207,7 +211,7 @@ describe('tests', () => { Version: '2012-10-17', Statement: [ { - Action: ['s3:PutObject*', 's3:Abort*'], + Action: ['s3:PutObject', 's3:Abort*'], Effect: 'Allow', Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::127311923021:root']] } }, Resource: { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts index 1d8df0a706d9c..77858e9c21af4 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts @@ -88,4 +88,138 @@ describe('tests', () => { UnhealthyThresholdCount: 27, }); }); + + test('Load balancer duration cookie stickiness', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const vpc = new ec2.Vpc(stack, 'VPC', {}); + + // WHEN + new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', { + stickinessCookieDuration: cdk.Duration.minutes(5), + vpc, + }); + + // THEN + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + TargetGroupAttributes: [ + { + Key: 'stickiness.enabled', + Value: 'true', + }, + { + Key: 'stickiness.type', + Value: 'lb_cookie', + }, + { + Key: 'stickiness.lb_cookie.duration_seconds', + Value: '300', + }, + ], + }); + }); + + test('Load balancer app cookie stickiness', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const vpc = new ec2.Vpc(stack, 'VPC', {}); + + // WHEN + new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', { + stickinessCookieDuration: cdk.Duration.minutes(5), + stickinessCookieName: 'MyDeliciousCookie', + vpc, + }); + + // THEN + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + TargetGroupAttributes: [ + { + Key: 'stickiness.enabled', + Value: 'true', + }, + { + Key: 'stickiness.type', + Value: 'app_cookie', + }, + { + Key: 'stickiness.app_cookie.cookie_name', + Value: 'MyDeliciousCookie', + }, + { + Key: 'stickiness.app_cookie.duration_seconds', + Value: '300', + }, + ], + }); + }); + + test('Bad stickiness cookie names', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const vpc = new ec2.Vpc(stack, 'VPC', {}); + const errMessage = 'App cookie names that start with the following prefixes are not allowed: AWSALB, AWSALBAPP, and AWSALBTG; they\'re reserved for use by the load balancer'; + + // THEN + ['AWSALBCookieName', 'AWSALBstickinessCookieName', 'AWSALBTGCookieName'].forEach((badCookieName, i) => { + expect(() => { + new elbv2.ApplicationTargetGroup(stack, `TargetGroup${i}`, { + stickinessCookieDuration: cdk.Duration.minutes(5), + stickinessCookieName: badCookieName, + vpc, + }); + }).toThrow(errMessage); + }); + }); + + test('Empty stickiness cookie name', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const vpc = new ec2.Vpc(stack, 'VPC', {}); + + // THEN + expect(() => { + new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', { + stickinessCookieDuration: cdk.Duration.minutes(5), + stickinessCookieName: '', + vpc, + }); + }).toThrow(/App cookie name cannot be an empty string./); + }); + + test('Bad stickiness duration value', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const vpc = new ec2.Vpc(stack, 'VPC', {}); + + // THEN + expect(() => { + new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', { + stickinessCookieDuration: cdk.Duration.days(8), + vpc, + }); + }).toThrow(/Stickiness cookie duration value must be between 1 second and 7 days \(604800 seconds\)./); + }); + + test('Bad slow start duration value', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const vpc = new ec2.Vpc(stack, 'VPC', {}); + + // THEN + [cdk.Duration.minutes(16), cdk.Duration.seconds(29)].forEach((badDuration, i) => { + expect(() => { + new elbv2.ApplicationTargetGroup(stack, `TargetGroup${i}`, { + slowStart: badDuration, + vpc, + }); + }).toThrow(/Slow start duration value must be between 30 and 900 seconds./); + }); + }); }); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.expected.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.expected.json index 8fd29b717c9d5..56acbde9bb1f1 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.expected.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.expected.json @@ -438,6 +438,20 @@ "Properties": { "Port": 80, "Protocol": "HTTP", + "TargetGroupAttributes": [ + { + "Key": "stickiness.enabled", + "Value": "true" + }, + { + "Key": "stickiness.type", + "Value": "lb_cookie" + }, + { + "Key": "stickiness.lb_cookie.duration_seconds", + "Value": "300" + } + ], "Targets": [ { "Id": "10.0.128.4" @@ -454,6 +468,28 @@ "Properties": { "Port": 80, "Protocol": "HTTP", + "TargetGroupAttributes": [ + { + "Key": "slow_start.duration_seconds", + "Value": "60" + }, + { + "Key": "stickiness.enabled", + "Value": "true" + }, + { + "Key": "stickiness.type", + "Value": "app_cookie" + }, + { + "Key": "stickiness.app_cookie.cookie_name", + "Value": "MyDeliciousCookie" + }, + { + "Key": "stickiness.app_cookie.duration_seconds", + "Value": "300" + } + ], "Targets": [ { "Id": "10.0.128.5" diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.ts index 251c730c2fa42..8643f7b4c1f69 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.ts @@ -22,6 +22,7 @@ const listener = lb.addListener('Listener', { const group1 = listener.addTargets('Target', { port: 80, targets: [new elbv2.IpTarget('10.0.128.4')], + stickinessCookieDuration: cdk.Duration.minutes(5), }); const group2 = listener.addTargets('ConditionalTarget', { @@ -29,6 +30,9 @@ const group2 = listener.addTargets('ConditionalTarget', { hostHeader: 'example.com', port: 80, targets: [new elbv2.IpTarget('10.0.128.5')], + stickinessCookieDuration: cdk.Duration.minutes(5), + stickinessCookieName: 'MyDeliciousCookie', + slowStart: cdk.Duration.minutes(1), }); group1.metricTargetResponseTime().createAlarm(stack, 'ResponseTimeHigh1', { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts index 88a3999ec0a6f..546b88ab7d541 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts @@ -3,8 +3,12 @@ import '@aws-cdk/assert/jest'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; +import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as elbv2 from '../../lib'; +const s3GrantWriteCtx = { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true }; + describe('tests', () => { test('Trivial construction: internet facing', () => { // GIVEN @@ -69,9 +73,9 @@ describe('tests', () => { }); }); - test('Access logging', () => { + testFutureBehavior('Access logging', s3GrantWriteCtx, cdk.App, (app) => { // GIVEN - const stack = new cdk.Stack(undefined, undefined, { env: { region: 'us-east-1' } }); + const stack = new cdk.Stack(app, undefined, { env: { region: 'us-east-1' } }); const vpc = new ec2.Vpc(stack, 'Stack'); const bucket = new s3.Bucket(stack, 'AccessLoggingBucket'); const lb = new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc }); @@ -101,7 +105,7 @@ describe('tests', () => { Version: '2012-10-17', Statement: [ { - Action: ['s3:PutObject*', 's3:Abort*'], + Action: ['s3:PutObject', 's3:Abort*'], Effect: 'Allow', Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::127311923021:root']] } }, Resource: { @@ -137,9 +141,9 @@ describe('tests', () => { }, ResourcePart.CompleteDefinition); }); - test('access logging with prefix', () => { + testFutureBehavior('access logging with prefix', s3GrantWriteCtx, cdk.App, (app) => { // GIVEN - const stack = new cdk.Stack(undefined, undefined, { env: { region: 'us-east-1' } }); + const stack = new cdk.Stack(app, undefined, { env: { region: 'us-east-1' } }); const vpc = new ec2.Vpc(stack, 'Stack'); const bucket = new s3.Bucket(stack, 'AccessLoggingBucket'); const lb = new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc }); @@ -172,7 +176,7 @@ describe('tests', () => { Version: '2012-10-17', Statement: [ { - Action: ['s3:PutObject*', 's3:Abort*'], + Action: ['s3:PutObject', 's3:Abort*'], Effect: 'Allow', Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::127311923021:root']] } }, Resource: { diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index afd5bf6b45430..12810832c0da8 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -382,17 +382,37 @@ describe('log groups', () => { // Domain1 expect(stack).toHaveResourceLike('Custom::CloudwatchLogResourcePolicy', { Create: { - parameters: { - policyName: 'ESLogPolicyc836fd92f07ec41eb70c2f6f08dc4b43cfb7c25391', - }, + 'Fn::Join': [ + '', + [ + '{"service":"CloudWatchLogs","action":"putResourcePolicy","parameters":{"policyName":"ESLogPolicyc836fd92f07ec41eb70c2f6f08dc4b43cfb7c25391","policyDocument":"{\\"Statement\\":[{\\"Action\\":[\\"logs:PutLogEvents\\",\\"logs:CreateLogStream\\"],\\"Effect\\":\\"Allow\\",\\"Principal\\":{\\"Service\\":\\"es.amazonaws.com\\"},\\"Resource\\":\\"', + { + 'Fn::GetAtt': [ + 'Domain1AppLogs6E8D1D67', + 'Arn', + ], + }, + '\\"}],\\"Version\\":\\"2012-10-17\\"}"},"physicalResourceId":{"id":"ESLogGroupPolicyc836fd92f07ec41eb70c2f6f08dc4b43cfb7c25391"}}', + ], + ], }, }); // Domain2 expect(stack).toHaveResourceLike('Custom::CloudwatchLogResourcePolicy', { Create: { - parameters: { - policyName: 'ESLogPolicyc8f05f015be3baf6ec1ee06cd1ee5cc8706ebbe5b2', - }, + 'Fn::Join': [ + '', + [ + '{"service":"CloudWatchLogs","action":"putResourcePolicy","parameters":{"policyName":"ESLogPolicyc8f05f015be3baf6ec1ee06cd1ee5cc8706ebbe5b2","policyDocument":"{\\"Statement\\":[{\\"Action\\":[\\"logs:PutLogEvents\\",\\"logs:CreateLogStream\\"],\\"Effect\\":\\"Allow\\",\\"Principal\\":{\\"Service\\":\\"es.amazonaws.com\\"},\\"Resource\\":\\"', + { + 'Fn::GetAtt': [ + 'Domain2AppLogs810876E2', + 'Arn', + ], + }, + '\\"}],\\"Version\\":\\"2012-10-17\\"}"},"physicalResourceId":{"id":"ESLogGroupPolicyc8f05f015be3baf6ec1ee06cd1ee5cc8706ebbe5b2"}}', + ], + ], }, }); }); diff --git a/packages/@aws-cdk/aws-elasticsearch/test/elasticsearch-access-policy.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/elasticsearch-access-policy.test.ts index 2b815c16b2048..8f19cef53553c 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/elasticsearch-access-policy.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/elasticsearch-access-policy.test.ts @@ -33,29 +33,25 @@ test('minimal example renders correctly', () => { 'Arn', ], }, - Create: { - service: 'ES', + Create: JSON.stringify({ action: 'updateElasticsearchDomainConfig', + service: 'ES', parameters: { DomainName: 'TestDomain', - AccessPolicies: '{\"Statement\":[{\"Action\":\"es:ESHttp*\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Resource\":\"test:arn\"}],\"Version\":\"2012-10-17\"}', + AccessPolicies: '{"Statement":[{"Action":"es:ESHttp*","Effect":"Allow","Principal":"*","Resource":"test:arn"}],"Version":"2012-10-17"}', }, outputPath: 'DomainConfig.ElasticsearchClusterConfig.AccessPolicies', - physicalResourceId: { - id: 'TestDomainAccessPolicy', - }, - }, - Update: { - service: 'ES', + physicalResourceId: { id: 'TestDomainAccessPolicy' }, + }), + Update: JSON.stringify({ action: 'updateElasticsearchDomainConfig', + service: 'ES', parameters: { DomainName: 'TestDomain', - AccessPolicies: '{\"Statement\":[{\"Action\":\"es:ESHttp*\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Resource\":\"test:arn\"}],\"Version\":\"2012-10-17\"}', + AccessPolicies: '{"Statement":[{"Action":"es:ESHttp*","Effect":"Allow","Principal":"*","Resource":"test:arn"}],"Version":"2012-10-17"}', }, outputPath: 'DomainConfig.ElasticsearchClusterConfig.AccessPolicies', - physicalResourceId: { - id: 'TestDomainAccessPolicy', - }, - }, + physicalResourceId: { id: 'TestDomainAccessPolicy' }, + }), }); }); diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.custom-kms-key.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.custom-kms-key.expected.json index 309d8c2831e8d..5424865cc3bd7 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.custom-kms-key.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.custom-kms-key.expected.json @@ -87,77 +87,50 @@ ] }, "Create": { - "service": "CloudWatchLogs", - "action": "putResourcePolicy", - "parameters": { - "policyName": "ESLogPolicyc82ca7bfe2f2589b859ebab89e88da2efd284adfad", - "policyDocument": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"es.amazonaws.com\"},\"Resource\":[\"", - { - "Fn::GetAtt": [ - "DomainSlowSearchLogs5B35A97A", - "Arn" - ] - }, - "\",\"", - { - "Fn::GetAtt": [ - "DomainAppLogs21698C1B", - "Arn" - ] - }, - "\"]}],\"Version\":\"2012-10-17\"}" + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"ESLogPolicyc82ca7bfe2f2589b859ebab89e88da2efd284adfad\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"es.amazonaws.com\\\"},\\\"Resource\\\":[\\\"", + { + "Fn::GetAtt": [ + "DomainSlowSearchLogs5B35A97A", + "Arn" ] - ] - } - }, - "physicalResourceId": { - "id": "ESLogGroupPolicyc82ca7bfe2f2589b859ebab89e88da2efd284adfad" - } + }, + "\\\",\\\"", + { + "Fn::GetAtt": [ + "DomainAppLogs21698C1B", + "Arn" + ] + }, + "\\\"]}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"ESLogGroupPolicyc82ca7bfe2f2589b859ebab89e88da2efd284adfad\"}}" + ] + ] }, "Update": { - "service": "CloudWatchLogs", - "action": "putResourcePolicy", - "parameters": { - "policyName": "ESLogPolicyc82ca7bfe2f2589b859ebab89e88da2efd284adfad", - "policyDocument": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"es.amazonaws.com\"},\"Resource\":[\"", - { - "Fn::GetAtt": [ - "DomainSlowSearchLogs5B35A97A", - "Arn" - ] - }, - "\",\"", - { - "Fn::GetAtt": [ - "DomainAppLogs21698C1B", - "Arn" - ] - }, - "\"]}],\"Version\":\"2012-10-17\"}" + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"ESLogPolicyc82ca7bfe2f2589b859ebab89e88da2efd284adfad\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"es.amazonaws.com\\\"},\\\"Resource\\\":[\\\"", + { + "Fn::GetAtt": [ + "DomainSlowSearchLogs5B35A97A", + "Arn" ] - ] - } - }, - "physicalResourceId": { - "id": "ESLogGroupPolicyc82ca7bfe2f2589b859ebab89e88da2efd284adfad" - } - }, - "Delete": { - "service": "CloudWatchLogs", - "action": "deleteResourcePolicy", - "parameters": { - "policyName": "ESLogPolicyc82ca7bfe2f2589b859ebab89e88da2efd284adfad" - }, - "ignoreErrorCodesMatching": "400" + }, + "\\\",\\\"", + { + "Fn::GetAtt": [ + "DomainAppLogs21698C1B", + "Arn" + ] + }, + "\\\"]}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"ESLogGroupPolicyc82ca7bfe2f2589b859ebab89e88da2efd284adfad\"}}" + ] + ] }, + "Delete": "{\"service\":\"CloudWatchLogs\",\"action\":\"deleteResourcePolicy\",\"parameters\":{\"policyName\":\"ESLogPolicyc82ca7bfe2f2589b859ebab89e88da2efd284adfad\"},\"ignoreErrorCodesMatching\":\"400\"}", "InstallLatestAwsSdk": true }, "DependsOn": [ @@ -264,52 +237,36 @@ ] }, "Create": { - "action": "updateElasticsearchDomainConfig", - "service": "ES", - "parameters": { - "DomainName": { - "Ref": "Domain66AC69E0" - }, - "AccessPolicies": "{\"Statement\":[{\"Action\":\"es:ESHttp*\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Resource\":\"*\"}],\"Version\":\"2012-10-17\"}" - }, - "outputPath": "DomainConfig.ElasticsearchClusterConfig.AccessPolicies", - "physicalResourceId": { - "id": { - "Fn::Join": [ - "", - [ - { - "Ref": "Domain66AC69E0" - }, - "AccessPolicy" - ] - ] - } - } + "Fn::Join": [ + "", + [ + "{\"action\":\"updateElasticsearchDomainConfig\",\"service\":\"ES\",\"parameters\":{\"DomainName\":\"", + { + "Ref": "Domain66AC69E0" + }, + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + { + "Ref": "Domain66AC69E0" + }, + "AccessPolicy\"}}" + ] + ] }, "Update": { - "action": "updateElasticsearchDomainConfig", - "service": "ES", - "parameters": { - "DomainName": { - "Ref": "Domain66AC69E0" - }, - "AccessPolicies": "{\"Statement\":[{\"Action\":\"es:ESHttp*\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Resource\":\"*\"}],\"Version\":\"2012-10-17\"}" - }, - "outputPath": "DomainConfig.ElasticsearchClusterConfig.AccessPolicies", - "physicalResourceId": { - "id": { - "Fn::Join": [ - "", - [ - { - "Ref": "Domain66AC69E0" - }, - "AccessPolicy" - ] - ] - } - } + "Fn::Join": [ + "", + [ + "{\"action\":\"updateElasticsearchDomainConfig\",\"service\":\"ES\",\"parameters\":{\"DomainName\":\"", + { + "Ref": "Domain66AC69E0" + }, + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + { + "Ref": "Domain66AC69E0" + }, + "AccessPolicy\"}}" + ] + ] }, "InstallLatestAwsSdk": true }, @@ -386,7 +343,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904" }, "S3Key": { "Fn::Join": [ @@ -399,7 +356,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -412,7 +369,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -439,17 +396,17 @@ } }, "Parameters": { - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904": { "Type": "String", - "Description": "S3 bucket for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 bucket for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF": { "Type": "String", - "Description": "S3 key for asset version \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 key for asset version \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94ArtifactHash782948FC": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343ArtifactHash0C561FF5": { "Type": "String", - "Description": "Artifact hash for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "Artifact hash for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json index e510eb3534625..6d54b0cfbc27e 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json @@ -52,77 +52,50 @@ ] }, "Create": { - "service": "CloudWatchLogs", - "action": "putResourcePolicy", - "parameters": { - "policyName": "ESLogPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6", - "policyDocument": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"es.amazonaws.com\"},\"Resource\":[\"", - { - "Fn::GetAtt": [ - "Domain1SlowSearchLogs8F3B0506", - "Arn" - ] - }, - "\",\"", - { - "Fn::GetAtt": [ - "Domain1AppLogs6E8D1D67", - "Arn" - ] - }, - "\"]}],\"Version\":\"2012-10-17\"}" + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"ESLogPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"es.amazonaws.com\\\"},\\\"Resource\\\":[\\\"", + { + "Fn::GetAtt": [ + "Domain1SlowSearchLogs8F3B0506", + "Arn" ] - ] - } - }, - "physicalResourceId": { - "id": "ESLogGroupPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6" - } + }, + "\\\",\\\"", + { + "Fn::GetAtt": [ + "Domain1AppLogs6E8D1D67", + "Arn" + ] + }, + "\\\"]}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"ESLogGroupPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6\"}}" + ] + ] }, "Update": { - "service": "CloudWatchLogs", - "action": "putResourcePolicy", - "parameters": { - "policyName": "ESLogPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6", - "policyDocument": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"es.amazonaws.com\"},\"Resource\":[\"", - { - "Fn::GetAtt": [ - "Domain1SlowSearchLogs8F3B0506", - "Arn" - ] - }, - "\",\"", - { - "Fn::GetAtt": [ - "Domain1AppLogs6E8D1D67", - "Arn" - ] - }, - "\"]}],\"Version\":\"2012-10-17\"}" + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"ESLogPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"es.amazonaws.com\\\"},\\\"Resource\\\":[\\\"", + { + "Fn::GetAtt": [ + "Domain1SlowSearchLogs8F3B0506", + "Arn" ] - ] - } - }, - "physicalResourceId": { - "id": "ESLogGroupPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6" - } - }, - "Delete": { - "service": "CloudWatchLogs", - "action": "deleteResourcePolicy", - "parameters": { - "policyName": "ESLogPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6" - }, - "ignoreErrorCodesMatching": "400" + }, + "\\\",\\\"", + { + "Fn::GetAtt": [ + "Domain1AppLogs6E8D1D67", + "Arn" + ] + }, + "\\\"]}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"ESLogGroupPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6\"}}" + ] + ] }, + "Delete": "{\"service\":\"CloudWatchLogs\",\"action\":\"deleteResourcePolicy\",\"parameters\":{\"policyName\":\"ESLogPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6\"},\"ignoreErrorCodesMatching\":\"400\"}", "InstallLatestAwsSdk": true }, "DependsOn": [ @@ -226,52 +199,36 @@ ] }, "Create": { - "action": "updateElasticsearchDomainConfig", - "service": "ES", - "parameters": { - "DomainName": { - "Ref": "Domain19FCBCB91" - }, - "AccessPolicies": "{\"Statement\":[{\"Action\":\"es:ESHttp*\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Resource\":\"*\"}],\"Version\":\"2012-10-17\"}" - }, - "outputPath": "DomainConfig.ElasticsearchClusterConfig.AccessPolicies", - "physicalResourceId": { - "id": { - "Fn::Join": [ - "", - [ - { - "Ref": "Domain19FCBCB91" - }, - "AccessPolicy" - ] - ] - } - } + "Fn::Join": [ + "", + [ + "{\"action\":\"updateElasticsearchDomainConfig\",\"service\":\"ES\",\"parameters\":{\"DomainName\":\"", + { + "Ref": "Domain19FCBCB91" + }, + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + { + "Ref": "Domain19FCBCB91" + }, + "AccessPolicy\"}}" + ] + ] }, "Update": { - "action": "updateElasticsearchDomainConfig", - "service": "ES", - "parameters": { - "DomainName": { - "Ref": "Domain19FCBCB91" - }, - "AccessPolicies": "{\"Statement\":[{\"Action\":\"es:ESHttp*\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Resource\":\"*\"}],\"Version\":\"2012-10-17\"}" - }, - "outputPath": "DomainConfig.ElasticsearchClusterConfig.AccessPolicies", - "physicalResourceId": { - "id": { - "Fn::Join": [ - "", - [ - { - "Ref": "Domain19FCBCB91" - }, - "AccessPolicy" - ] - ] - } - } + "Fn::Join": [ + "", + [ + "{\"action\":\"updateElasticsearchDomainConfig\",\"service\":\"ES\",\"parameters\":{\"DomainName\":\"", + { + "Ref": "Domain19FCBCB91" + }, + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + { + "Ref": "Domain19FCBCB91" + }, + "AccessPolicy\"}}" + ] + ] }, "InstallLatestAwsSdk": true }, @@ -318,7 +275,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904" }, "S3Key": { "Fn::Join": [ @@ -331,7 +288,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -344,7 +301,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -420,77 +377,50 @@ ] }, "Create": { - "service": "CloudWatchLogs", - "action": "putResourcePolicy", - "parameters": { - "policyName": "ESLogPolicyc8405238e455eeabd840cf6933e1814efc51d2de71", - "policyDocument": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"es.amazonaws.com\"},\"Resource\":[\"", - { - "Fn::GetAtt": [ - "Domain2SlowSearchLogs0C75F64B", - "Arn" - ] - }, - "\",\"", - { - "Fn::GetAtt": [ - "Domain2AppLogs810876E2", - "Arn" - ] - }, - "\"]}],\"Version\":\"2012-10-17\"}" + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"ESLogPolicyc8405238e455eeabd840cf6933e1814efc51d2de71\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"es.amazonaws.com\\\"},\\\"Resource\\\":[\\\"", + { + "Fn::GetAtt": [ + "Domain2SlowSearchLogs0C75F64B", + "Arn" ] - ] - } - }, - "physicalResourceId": { - "id": "ESLogGroupPolicyc8405238e455eeabd840cf6933e1814efc51d2de71" - } + }, + "\\\",\\\"", + { + "Fn::GetAtt": [ + "Domain2AppLogs810876E2", + "Arn" + ] + }, + "\\\"]}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"ESLogGroupPolicyc8405238e455eeabd840cf6933e1814efc51d2de71\"}}" + ] + ] }, "Update": { - "service": "CloudWatchLogs", - "action": "putResourcePolicy", - "parameters": { - "policyName": "ESLogPolicyc8405238e455eeabd840cf6933e1814efc51d2de71", - "policyDocument": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"es.amazonaws.com\"},\"Resource\":[\"", - { - "Fn::GetAtt": [ - "Domain2SlowSearchLogs0C75F64B", - "Arn" - ] - }, - "\",\"", - { - "Fn::GetAtt": [ - "Domain2AppLogs810876E2", - "Arn" - ] - }, - "\"]}],\"Version\":\"2012-10-17\"}" + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"ESLogPolicyc8405238e455eeabd840cf6933e1814efc51d2de71\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"es.amazonaws.com\\\"},\\\"Resource\\\":[\\\"", + { + "Fn::GetAtt": [ + "Domain2SlowSearchLogs0C75F64B", + "Arn" ] - ] - } - }, - "physicalResourceId": { - "id": "ESLogGroupPolicyc8405238e455eeabd840cf6933e1814efc51d2de71" - } - }, - "Delete": { - "service": "CloudWatchLogs", - "action": "deleteResourcePolicy", - "parameters": { - "policyName": "ESLogPolicyc8405238e455eeabd840cf6933e1814efc51d2de71" - }, - "ignoreErrorCodesMatching": "400" + }, + "\\\",\\\"", + { + "Fn::GetAtt": [ + "Domain2AppLogs810876E2", + "Arn" + ] + }, + "\\\"]}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"ESLogGroupPolicyc8405238e455eeabd840cf6933e1814efc51d2de71\"}}" + ] + ] }, + "Delete": "{\"service\":\"CloudWatchLogs\",\"action\":\"deleteResourcePolicy\",\"parameters\":{\"policyName\":\"ESLogPolicyc8405238e455eeabd840cf6933e1814efc51d2de71\"},\"ignoreErrorCodesMatching\":\"400\"}", "InstallLatestAwsSdk": true }, "DependsOn": [ @@ -594,52 +524,36 @@ ] }, "Create": { - "action": "updateElasticsearchDomainConfig", - "service": "ES", - "parameters": { - "DomainName": { - "Ref": "Domain2644FE48C" - }, - "AccessPolicies": "{\"Statement\":[{\"Action\":\"es:ESHttp*\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Resource\":\"*\"}],\"Version\":\"2012-10-17\"}" - }, - "outputPath": "DomainConfig.ElasticsearchClusterConfig.AccessPolicies", - "physicalResourceId": { - "id": { - "Fn::Join": [ - "", - [ - { - "Ref": "Domain2644FE48C" - }, - "AccessPolicy" - ] - ] - } - } + "Fn::Join": [ + "", + [ + "{\"action\":\"updateElasticsearchDomainConfig\",\"service\":\"ES\",\"parameters\":{\"DomainName\":\"", + { + "Ref": "Domain2644FE48C" + }, + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + { + "Ref": "Domain2644FE48C" + }, + "AccessPolicy\"}}" + ] + ] }, "Update": { - "action": "updateElasticsearchDomainConfig", - "service": "ES", - "parameters": { - "DomainName": { - "Ref": "Domain2644FE48C" - }, - "AccessPolicies": "{\"Statement\":[{\"Action\":\"es:ESHttp*\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Resource\":\"*\"}],\"Version\":\"2012-10-17\"}" - }, - "outputPath": "DomainConfig.ElasticsearchClusterConfig.AccessPolicies", - "physicalResourceId": { - "id": { - "Fn::Join": [ - "", - [ - { - "Ref": "Domain2644FE48C" - }, - "AccessPolicy" - ] - ] - } - } + "Fn::Join": [ + "", + [ + "{\"action\":\"updateElasticsearchDomainConfig\",\"service\":\"ES\",\"parameters\":{\"DomainName\":\"", + { + "Ref": "Domain2644FE48C" + }, + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + { + "Ref": "Domain2644FE48C" + }, + "AccessPolicy\"}}" + ] + ] }, "InstallLatestAwsSdk": true }, @@ -652,17 +566,17 @@ } }, "Parameters": { - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904": { "Type": "String", - "Description": "S3 bucket for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 bucket for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF": { "Type": "String", - "Description": "S3 key for asset version \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 key for asset version \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94ArtifactHash782948FC": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343ArtifactHash0C561FF5": { "Type": "String", - "Description": "Artifact hash for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "Artifact hash for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.expected.json index 065e569573421..89a79c79f8316 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.expected.json @@ -101,80 +101,50 @@ ] }, "Create": { - "action": "updateElasticsearchDomainConfig", - "service": "ES", - "parameters": { - "DomainName": { - "Ref": "Domain66AC69E0" - }, - "AccessPolicies": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":\"es:ESHttp*\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Resource\":\"", - { - "Fn::GetAtt": [ - "Domain66AC69E0", - "Arn" - ] - }, - "/*\"}],\"Version\":\"2012-10-17\"}" - ] - ] - } - }, - "outputPath": "DomainConfig.ElasticsearchClusterConfig.AccessPolicies", - "physicalResourceId": { - "id": { - "Fn::Join": [ - "", - [ - { - "Ref": "Domain66AC69E0" - }, - "AccessPolicy" + "Fn::Join": [ + "", + [ + "{\"action\":\"updateElasticsearchDomainConfig\",\"service\":\"ES\",\"parameters\":{\"DomainName\":\"", + { + "Ref": "Domain66AC69E0" + }, + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Resource\\\":\\\"", + { + "Fn::GetAtt": [ + "Domain66AC69E0", + "Arn" ] - ] - } - } + }, + "/*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + { + "Ref": "Domain66AC69E0" + }, + "AccessPolicy\"}}" + ] + ] }, "Update": { - "action": "updateElasticsearchDomainConfig", - "service": "ES", - "parameters": { - "DomainName": { - "Ref": "Domain66AC69E0" - }, - "AccessPolicies": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":\"es:ESHttp*\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Resource\":\"", - { - "Fn::GetAtt": [ - "Domain66AC69E0", - "Arn" - ] - }, - "/*\"}],\"Version\":\"2012-10-17\"}" - ] - ] - } - }, - "outputPath": "DomainConfig.ElasticsearchClusterConfig.AccessPolicies", - "physicalResourceId": { - "id": { - "Fn::Join": [ - "", - [ - { - "Ref": "Domain66AC69E0" - }, - "AccessPolicy" + "Fn::Join": [ + "", + [ + "{\"action\":\"updateElasticsearchDomainConfig\",\"service\":\"ES\",\"parameters\":{\"DomainName\":\"", + { + "Ref": "Domain66AC69E0" + }, + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Resource\\\":\\\"", + { + "Fn::GetAtt": [ + "Domain66AC69E0", + "Arn" ] - ] - } - } + }, + "/*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + { + "Ref": "Domain66AC69E0" + }, + "AccessPolicy\"}}" + ] + ] }, "InstallLatestAwsSdk": true }, @@ -221,7 +191,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904" }, "S3Key": { "Fn::Join": [ @@ -234,7 +204,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -247,7 +217,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -273,17 +243,17 @@ } }, "Parameters": { - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904": { "Type": "String", - "Description": "S3 bucket for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 bucket for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF": { "Type": "String", - "Description": "S3 key for asset version \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 key for asset version \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94ArtifactHash782948FC": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343ArtifactHash0C561FF5": { "Type": "String", - "Description": "Artifact hash for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "Artifact hash for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticsearch/test/log-group-resource-policy.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/log-group-resource-policy.test.ts index 4fefd1d13f8b6..f007f31f38c6d 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/log-group-resource-policy.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/log-group-resource-policy.test.ts @@ -31,7 +31,7 @@ test('minimal example renders correctly', () => { 'Arn', ], }, - Create: { + Create: JSON.stringify({ service: 'CloudWatchLogs', action: 'putResourcePolicy', parameters: { @@ -41,8 +41,8 @@ test('minimal example renders correctly', () => { physicalResourceId: { id: 'LogGroupResourcePolicy', }, - }, - Update: { + }), + Update: JSON.stringify({ service: 'CloudWatchLogs', action: 'putResourcePolicy', parameters: { @@ -52,14 +52,14 @@ test('minimal example renders correctly', () => { physicalResourceId: { id: 'LogGroupResourcePolicy', }, - }, - Delete: { + }), + Delete: JSON.stringify({ service: 'CloudWatchLogs', action: 'deleteResourcePolicy', parameters: { policyName: 'TestPolicy', }, ignoreErrorCodesMatching: '400', - }, + }), }); }); diff --git a/packages/@aws-cdk/aws-events-targets/README.md b/packages/@aws-cdk/aws-events-targets/README.md index 787bfcc433d30..ba0e83c2a82ba 100644 --- a/packages/@aws-cdk/aws-events-targets/README.md +++ b/packages/@aws-cdk/aws-events-targets/README.md @@ -27,6 +27,7 @@ Currently supported are: * Put a record to a Kinesis stream * Log an event into a LogGroup * Put a record to a Kinesis Data Firehose stream +* Put an event on an EventBridge bus See the README of the `@aws-cdk/aws-events` library for more information on EventBridge. diff --git a/packages/@aws-cdk/aws-events-targets/lib/event-bus.ts b/packages/@aws-cdk/aws-events-targets/lib/event-bus.ts new file mode 100644 index 0000000000000..1d07261a8eace --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/lib/event-bus.ts @@ -0,0 +1,45 @@ +import * as events from '@aws-cdk/aws-events'; +import * as iam from '@aws-cdk/aws-iam'; +import { singletonEventRole } from './util'; + +/** + * Configuration properties of an Event Bus event + */ +export interface EventBusProps { + /** + * Role to be used to publish the event + * + * @default a new role is created. + */ + readonly role?: iam.IRole; +} + +/** + * Notify an existing Event Bus of an event + */ +export class EventBus implements events.IRuleTarget { + private readonly role?: iam.IRole; + + constructor(private readonly eventBus: events.IEventBus, props: EventBusProps = {}) { + this.role = props.role; + } + + bind(rule: events.IRule, id?: string): events.RuleTargetConfig { + if (this.role) { + this.role.addToPrincipalPolicy(this.putEventStatement()); + } + const role = this.role ?? singletonEventRole(rule, [this.putEventStatement()]); + return { + id: id ?? '', + arn: this.eventBus.eventBusArn, + role, + }; + } + + private putEventStatement() { + return new iam.PolicyStatement({ + actions: ['events:PutEvents'], + resources: [this.eventBus.eventBusArn], + }); + } +} diff --git a/packages/@aws-cdk/aws-events-targets/lib/index.ts b/packages/@aws-cdk/aws-events-targets/lib/index.ts index bef8ce2463ffa..155791c195d1e 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/index.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/index.ts @@ -7,6 +7,7 @@ export * from './aws-api'; export * from './lambda'; export * from './ecs-task-properties'; export * from './ecs-task'; +export * from './event-bus'; export * from './state-machine'; export * from './kinesis-stream'; export * from './log-group'; diff --git a/packages/@aws-cdk/aws-events-targets/lib/util.ts b/packages/@aws-cdk/aws-events-targets/lib/util.ts index 74465558bb3f9..1026d1ae35a1a 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/util.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/util.ts @@ -33,13 +33,14 @@ export function singletonEventRole(scope: IConstruct, policyStatements: iam.Poli export function addLambdaPermission(rule: events.IRule, handler: lambda.IFunction): void { let scope: Construct | undefined; let node: ConstructNode = handler.permissionsNode; + let permissionId = `AllowEventRule${Names.nodeUniqueId(rule.node)}`; if (rule instanceof Construct) { // Place the Permission resource in the same stack as Rule rather than the Function // This is to reduce circular dependency when the lambda handler and the rule are across stacks. scope = rule; node = rule.node; + permissionId = `AllowEventRule${Names.nodeUniqueId(handler.node)}`; } - const permissionId = `AllowEventRule${Names.nodeUniqueId(rule.node)}`; if (!node.tryFindChild(permissionId)) { handler.addPermission(permissionId, { scope, diff --git a/packages/@aws-cdk/aws-events-targets/test/aws-api/integ.aws-api.expected.json b/packages/@aws-cdk/aws-events-targets/test/aws-api/integ.aws-api.expected.json index 031c41c4e954c..f5ae531bc1a27 100644 --- a/packages/@aws-cdk/aws-events-targets/test/aws-api/integ.aws-api.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/aws-api/integ.aws-api.expected.json @@ -29,7 +29,26 @@ ] } }, - "ScheduleRuleAllowEventRuleawscdkawsapitargetintegScheduleRule51140722763E20C1": { + "ScheduleRuleAllowEventRuleawscdkawsapitargetintegScheduleRuleScheduleRuleTarget0HandlerF2C0C898874A4805": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "AWSb4cf1abd4e4f4bc699441af7ccd9ec371511E620", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "ScheduleRuleDA5BD877", + "Arn" + ] + } + } + }, + "ScheduleRuleAllowEventRuleawscdkawsapitargetintegScheduleRuleScheduleRuleTarget1Handler4688817C0179F894": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", @@ -115,7 +134,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters4e52413f31cff0a335f5083fa6197a6cb61928644842d89026c42c2d2a98342eS3Bucket38E36746" + "Ref": "AssetParameters6e29dae9ab5b8a6c0aa8f922991cad784ebd048388ec6587d4832f307ef92f94S3BucketEBC7D1F4" }, "S3Key": { "Fn::Join": [ @@ -128,7 +147,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters4e52413f31cff0a335f5083fa6197a6cb61928644842d89026c42c2d2a98342eS3VersionKeyFB07730C" + "Ref": "AssetParameters6e29dae9ab5b8a6c0aa8f922991cad784ebd048388ec6587d4832f307ef92f94S3VersionKeyDC9DCE00" } ] } @@ -141,7 +160,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters4e52413f31cff0a335f5083fa6197a6cb61928644842d89026c42c2d2a98342eS3VersionKeyFB07730C" + "Ref": "AssetParameters6e29dae9ab5b8a6c0aa8f922991cad784ebd048388ec6587d4832f307ef92f94S3VersionKeyDC9DCE00" } ] } @@ -151,13 +170,13 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "AWSb4cf1abd4e4f4bc699441af7ccd9ec37ServiceRole9FFE9C50", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs12.x" }, "DependsOn": [ @@ -198,7 +217,7 @@ ] } }, - "PatternRuleAllowEventRuleawscdkawsapitargetintegPatternRule3D388581AA4F776B": { + "PatternRuleAllowEventRuleawscdkawsapitargetintegPatternRulePatternRuleTarget0HandlerA0821464BB49C5D3": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", @@ -219,17 +238,17 @@ } }, "Parameters": { - "AssetParameters4e52413f31cff0a335f5083fa6197a6cb61928644842d89026c42c2d2a98342eS3Bucket38E36746": { + "AssetParameters6e29dae9ab5b8a6c0aa8f922991cad784ebd048388ec6587d4832f307ef92f94S3BucketEBC7D1F4": { "Type": "String", - "Description": "S3 bucket for asset \"4e52413f31cff0a335f5083fa6197a6cb61928644842d89026c42c2d2a98342e\"" + "Description": "S3 bucket for asset \"6e29dae9ab5b8a6c0aa8f922991cad784ebd048388ec6587d4832f307ef92f94\"" }, - "AssetParameters4e52413f31cff0a335f5083fa6197a6cb61928644842d89026c42c2d2a98342eS3VersionKeyFB07730C": { + "AssetParameters6e29dae9ab5b8a6c0aa8f922991cad784ebd048388ec6587d4832f307ef92f94S3VersionKeyDC9DCE00": { "Type": "String", - "Description": "S3 key for asset version \"4e52413f31cff0a335f5083fa6197a6cb61928644842d89026c42c2d2a98342e\"" + "Description": "S3 key for asset version \"6e29dae9ab5b8a6c0aa8f922991cad784ebd048388ec6587d4832f307ef92f94\"" }, - "AssetParameters4e52413f31cff0a335f5083fa6197a6cb61928644842d89026c42c2d2a98342eArtifactHash3C551617": { + "AssetParameters6e29dae9ab5b8a6c0aa8f922991cad784ebd048388ec6587d4832f307ef92f94ArtifactHash34B6F705": { "Type": "String", - "Description": "Artifact hash for asset \"4e52413f31cff0a335f5083fa6197a6cb61928644842d89026c42c2d2a98342e\"" + "Description": "Artifact hash for asset \"6e29dae9ab5b8a6c0aa8f922991cad784ebd048388ec6587d4832f307ef92f94\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.expected.json b/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.expected.json index 85af1bfde21c1..66418749eda33 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.expected.json @@ -41,6 +41,20 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, + "pipelinePipeline22F2A91DArtifactsBucketEncryptionKeyAlias9530209A": { + "Type": "AWS::KMS::Alias", + "Properties": { + "AliasName": "alias/codepipeline-pipelineeventspipelinepipeline22f2a91dfbb66895", + "TargetKeyId": { + "Fn::GetAtt": [ + "pipelinePipeline22F2A91DArtifactsBucketEncryptionKey87C796D2", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, "pipelinePipeline22F2A91DArtifactsBucketC1799DCD": { "Type": "AWS::S3::Bucket", "Properties": { @@ -69,20 +83,6 @@ "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" }, - "pipelinePipeline22F2A91DArtifactsBucketEncryptionKeyAlias9530209A": { - "Type": "AWS::KMS::Alias", - "Properties": { - "AliasName": "alias/codepipeline-pipelineeventspipelinepipeline22f2a91dfbb66895", - "TargetKeyId": { - "Fn::GetAtt": [ - "pipelinePipeline22F2A91DArtifactsBucketEncryptionKey87C796D2", - "Arn" - ] - } - }, - "UpdateReplacePolicy": "Delete", - "DeletionPolicy": "Delete" - }, "pipelinePipeline22F2A91DRole58B7B05E": { "Type": "AWS::IAM::Role", "Properties": { @@ -439,4 +439,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-ec2-task.lit.expected.json b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-ec2-task.lit.expected.json index eaa617893829d..6dc9c62e63102 100644 --- a/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-ec2-task.lit.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-ec2-task.lit.expected.json @@ -278,8 +278,6 @@ "ecs:Poll", "ecs:StartTelemetrySession" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -289,7 +287,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ @@ -474,8 +474,6 @@ "ecs:DescribeContainerInstances", "ecs:DescribeTasks" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -485,7 +483,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ @@ -536,14 +536,12 @@ "Code": { "ZipFile": "import boto3, json, os, time\n\necs = boto3.client('ecs')\nautoscaling = boto3.client('autoscaling')\n\n\ndef lambda_handler(event, context):\n print(json.dumps(event))\n cluster = os.environ['CLUSTER']\n snsTopicArn = event['Records'][0]['Sns']['TopicArn']\n lifecycle_event = json.loads(event['Records'][0]['Sns']['Message'])\n instance_id = lifecycle_event.get('EC2InstanceId')\n if not instance_id:\n print('Got event without EC2InstanceId: %s', json.dumps(event))\n return\n\n instance_arn = container_instance_arn(cluster, instance_id)\n print('Instance %s has container instance ARN %s' % (lifecycle_event['EC2InstanceId'], instance_arn))\n\n if not instance_arn:\n return\n\n while has_tasks(cluster, instance_arn):\n time.sleep(10)\n\n try:\n print('Terminating instance %s' % instance_id)\n autoscaling.complete_lifecycle_action(\n LifecycleActionResult='CONTINUE',\n **pick(lifecycle_event, 'LifecycleHookName', 'LifecycleActionToken', 'AutoScalingGroupName'))\n except Exception as e:\n # Lifecycle action may have already completed.\n print(str(e))\n\n\ndef container_instance_arn(cluster, instance_id):\n \"\"\"Turn an instance ID into a container instance ARN.\"\"\"\n arns = ecs.list_container_instances(cluster=cluster, filter='ec2InstanceId==' + instance_id)['containerInstanceArns']\n if not arns:\n return None\n return arns[0]\n\n\ndef has_tasks(cluster, instance_arn):\n \"\"\"Return True if the instance is running tasks for the given cluster.\"\"\"\n instances = ecs.describe_container_instances(cluster=cluster, containerInstances=[instance_arn])['containerInstances']\n if not instances:\n return False\n instance = instances[0]\n\n if instance['status'] == 'ACTIVE':\n # Start draining, then try again later\n set_container_instance_to_draining(cluster, instance_arn)\n return True\n\n tasks = instance['runningTasksCount'] + instance['pendingTasksCount']\n print('Instance %s has %s tasks' % (instance_arn, tasks))\n\n return tasks > 0\n\n\ndef set_container_instance_to_draining(cluster, instance_arn):\n ecs.update_container_instances_state(\n cluster=cluster,\n containerInstances=[instance_arn], status='DRAINING')\n\n\ndef pick(dct, *keys):\n \"\"\"Pick a subset of a dict.\"\"\"\n return {k: v for k, v in dct.items() if k in keys}\n" }, - "Handler": "index.lambda_handler", "Role": { "Fn::GetAtt": [ "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole94543EDA", "Arn" ] }, - "Runtime": "python3.6", "Environment": { "Variables": { "CLUSTER": { @@ -551,6 +549,8 @@ } } }, + "Handler": "index.lambda_handler", + "Runtime": "python3.6", "Tags": [ { "Key": "Name", @@ -935,4 +935,4 @@ "Default": "/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id" } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-fargate-task.expected.json b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-fargate-task.expected.json index 8062d441cc13d..123578699510c 100644 --- a/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-fargate-task.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-fargate-task.expected.json @@ -361,22 +361,6 @@ ] } }, - "TaskDefSecurityGroupD50E7CF0": { - "Type": "AWS::EC2::SecurityGroup", - "Properties": { - "GroupDescription": "aws-ecs-integ-fargate/TaskDef/SecurityGroup", - "SecurityGroupEgress": [ - { - "CidrIp": "0.0.0.0/0", - "Description": "Allow all outbound traffic by default", - "IpProtocol": "-1" - } - ], - "VpcId": { - "Ref": "Vpc8378EB38" - } - } - }, "TaskDefEventsRoleFB3B67B8": { "Type": "AWS::IAM::Role", "Properties": { @@ -447,6 +431,22 @@ ] } }, + "TaskDefSecurityGroupD50E7CF0": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ-fargate/TaskDef/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, "Rule4C995B7F": { "Type": "AWS::Events::Rule", "Properties": { @@ -498,4 +498,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/event-bus/event-rule-target.test.ts b/packages/@aws-cdk/aws-events-targets/test/event-bus/event-rule-target.test.ts new file mode 100644 index 0000000000000..4c39a907210d7 --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/test/event-bus/event-rule-target.test.ts @@ -0,0 +1,93 @@ +import '@aws-cdk/assert/jest'; +import * as events from '@aws-cdk/aws-events'; +import * as iam from '@aws-cdk/aws-iam'; +import { Stack } from '@aws-cdk/core'; +import * as targets from '../../lib'; + +test('Use EventBus as an event rule target', () => { + const stack = new Stack(); + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 min)'), + }); + + rule.addTarget(new targets.EventBus(events.EventBus.fromEventBusArn( + stack, + 'External', + 'arn:aws:events:us-east-1:111111111111:default', + ), + )); + + expect(stack).toHaveResource('AWS::Events::Rule', { + Targets: [ + { + Arn: 'arn:aws:events:us-east-1:111111111111:default', + Id: 'Target0', + RoleArn: { + 'Fn::GetAtt': [ + 'RuleEventsRoleC51A4248', + 'Arn', + ], + }, + }, + ], + }); + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [{ + Effect: 'Allow', + Action: 'events:PutEvents', + Resource: 'arn:aws:events:us-east-1:111111111111:default', + }], + Version: '2012-10-17', + }, + Roles: [{ + Ref: 'RuleEventsRoleC51A4248', + }], + }); +}); + +test('with supplied role', () => { + const stack = new Stack(); + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 min)'), + }); + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('events.amazonaws.com'), + roleName: 'GivenRole', + }); + + rule.addTarget(new targets.EventBus( + events.EventBus.fromEventBusArn( + stack, + 'External', + 'arn:aws:events:us-east-1:123456789012:default', + ), + { role }, + )); + + expect(stack).toHaveResource('AWS::Events::Rule', { + Targets: [{ + Arn: 'arn:aws:events:us-east-1:123456789012:default', + Id: 'Target0', + RoleArn: { + 'Fn::GetAtt': [ + 'Role1ABCC5F0', + 'Arn', + ], + }, + }], + }); + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [{ + Effect: 'Allow', + Action: 'events:PutEvents', + Resource: 'arn:aws:events:us-east-1:123456789012:default', + }], + Version: '2012-10-17', + }, + Roles: [{ + Ref: 'Role1ABCC5F0', + }], + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/event-bus/integ.event-bus.expected.json b/packages/@aws-cdk/aws-events-targets/test/event-bus/integ.event-bus.expected.json new file mode 100644 index 0000000000000..632ddf1767598 --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/test/event-bus/integ.event-bus.expected.json @@ -0,0 +1,83 @@ +{ + "Resources": { + "Rule4C995B7F": { + "Type": "AWS::Events::Rule", + "Properties": { + "ScheduleExpression": "rate(1 minute)", + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::Join": [ + "", + [ + "arn:aws:events:", + { + "Ref": "AWS::Region" + }, + ":999999999999:event-bus/test-bus" + ] + ] + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "RuleEventsRoleC51A4248", + "Arn" + ] + } + } + ] + } + }, + "RuleEventsRoleC51A4248": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "RuleEventsRoleDefaultPolicy0510525D": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "events:PutEvents", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:events:", + { + "Ref": "AWS::Region" + }, + ":999999999999:event-bus/test-bus" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "RuleEventsRoleDefaultPolicy0510525D", + "Roles": [ + { + "Ref": "RuleEventsRoleC51A4248" + } + ] + } + } + } +} diff --git a/packages/@aws-cdk/aws-events-targets/test/event-bus/integ.event-bus.ts b/packages/@aws-cdk/aws-events-targets/test/event-bus/integ.event-bus.ts new file mode 100644 index 0000000000000..c0ec2ea421b85 --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/test/event-bus/integ.event-bus.ts @@ -0,0 +1,26 @@ +/// !cdk-integ pragma:ignore-assets +import * as events from '@aws-cdk/aws-events'; +import * as cdk from '@aws-cdk/core'; +import * as targets from '../../lib'; + +const app = new cdk.App(); + +class EventSourceStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const rule = new events.Rule(this, 'Rule', { + schedule: events.Schedule.expression('rate(1 minute)'), + }); + rule.addTarget(new targets.EventBus( + events.EventBus.fromEventBusArn( + this, + 'External', + `arn:aws:events:${this.region}:999999999999:event-bus/test-bus`, + ), + )); + } +} + +new EventSourceStack(app, 'event-source-stack'); +app.synth(); diff --git a/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.expected.json b/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.expected.json index 460d13d03e0ca..4151890c35ef3 100644 --- a/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.expected.json @@ -1,89 +1,89 @@ { - "Resources":{ - "MyStream5C050E93":{ - "Type":"AWS::Kinesis::Stream", - "Properties":{ - "ShardCount":1, - "RetentionPeriodHours":24, - "StreamEncryption":{ - "Fn::If":[ + "Resources": { + "MyStream5C050E93": { + "Type": "AWS::Kinesis::Stream", + "Properties": { + "ShardCount": 1, + "RetentionPeriodHours": 24, + "StreamEncryption": { + "Fn::If": [ "AwsCdkKinesisEncryptedStreamsUnsupportedRegions", { - "Ref":"AWS::NoValue" + "Ref": "AWS::NoValue" }, { - "EncryptionType":"KMS", - "KeyId":"alias/aws/kinesis" + "EncryptionType": "KMS", + "KeyId": "alias/aws/kinesis" } ] } } }, - "MyStreamEventsRole5B6CC6AF":{ - "Type":"AWS::IAM::Role", - "Properties":{ - "AssumeRolePolicyDocument":{ - "Statement":[ + "MyStreamEventsRole5B6CC6AF": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ { - "Action":"sts:AssumeRole", - "Effect":"Allow", - "Principal":{ - "Service":"events.amazonaws.com" + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" } } ], - "Version":"2012-10-17" + "Version": "2012-10-17" } } }, - "MyStreamEventsRoleDefaultPolicy2089B49E":{ - "Type":"AWS::IAM::Policy", - "Properties":{ - "PolicyDocument":{ - "Statement":[ + "MyStreamEventsRoleDefaultPolicy2089B49E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ { - "Action":[ + "Action": [ "kinesis:PutRecord", "kinesis:PutRecords" ], - "Effect":"Allow", - "Resource":{ - "Fn::GetAtt":[ + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ "MyStream5C050E93", "Arn" ] } } ], - "Version":"2012-10-17" + "Version": "2012-10-17" }, - "PolicyName":"MyStreamEventsRoleDefaultPolicy2089B49E", - "Roles":[ + "PolicyName": "MyStreamEventsRoleDefaultPolicy2089B49E", + "Roles": [ { - "Ref":"MyStreamEventsRole5B6CC6AF" + "Ref": "MyStreamEventsRole5B6CC6AF" } ] } }, - "EveryMinute2BBCEA8F":{ - "Type":"AWS::Events::Rule", - "Properties":{ - "ScheduleExpression":"rate(1 minute)", - "State":"ENABLED", - "Targets":[ + "EveryMinute2BBCEA8F": { + "Type": "AWS::Events::Rule", + "Properties": { + "ScheduleExpression": "rate(1 minute)", + "State": "ENABLED", + "Targets": [ { - "Arn":{ - "Fn::GetAtt":[ + "Arn": { + "Fn::GetAtt": [ "MyStream5C050E93", "Arn" ] }, - "Id":"Target0", - "KinesisParameters":{ - "PartitionKeyPath":"$.id" + "Id": "Target0", + "KinesisParameters": { + "PartitionKeyPath": "$.id" }, - "RoleArn":{ - "Fn::GetAtt":[ + "RoleArn": { + "Fn::GetAtt": [ "MyStreamEventsRole5B6CC6AF", "Arn" ] @@ -93,21 +93,21 @@ } } }, - "Conditions":{ - "AwsCdkKinesisEncryptedStreamsUnsupportedRegions":{ - "Fn::Or":[ + "Conditions": { + "AwsCdkKinesisEncryptedStreamsUnsupportedRegions": { + "Fn::Or": [ { - "Fn::Equals":[ + "Fn::Equals": [ { - "Ref":"AWS::Region" + "Ref": "AWS::Region" }, "cn-north-1" ] }, { - "Fn::Equals":[ + "Fn::Equals": [ { - "Ref":"AWS::Region" + "Ref": "AWS::Region" }, "cn-northwest-1" ] diff --git a/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.expected.json b/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.expected.json index aad7c05f0bd5f..c12d92ef34810 100644 --- a/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.expected.json @@ -37,13 +37,13 @@ "Code": { "ZipFile": "exports.handler = function handler(event, _context, callback) {\n console.log(JSON.stringify(event, undefined, 2));\n return callback();\n}" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "MyFuncServiceRole54065130", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -68,7 +68,7 @@ ] } }, - "TimerAllowEventRulelambdaeventsTimer0E6AB6D890F582F4": { + "TimerAllowEventRulelambdaeventsMyFunc910E580F793D7BBB": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", @@ -105,7 +105,7 @@ ] } }, - "Timer2AllowEventRulelambdaeventsTimer27F866A1E50659689": { + "Timer2AllowEventRulelambdaeventsMyFunc910E580FCCD9CDCE": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", diff --git a/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts b/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts index 28df0932c9ba4..29be8973fe3c3 100644 --- a/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts @@ -78,6 +78,27 @@ test('adding same lambda function as target mutiple times creates permission onl expect(stack).toCountResources('AWS::Lambda::Permission', 1); }); +test('adding different lambda functions as target mutiple times creates multiple permissions', () => { + // GIVEN + const stack = new cdk.Stack(); + const fn1 = newTestLambda(stack); + const fn2 = newTestLambda(stack, '2'); + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), + }); + + // WHEN + rule.addTarget(new targets.LambdaFunction(fn1, { + event: events.RuleTargetInput.fromObject({ key: 'value1' }), + })); + rule.addTarget(new targets.LambdaFunction(fn2, { + event: events.RuleTargetInput.fromObject({ key: 'value2' }), + })); + + // THEN + expect(stack).toCountResources('AWS::Lambda::Permission', 2); +}); + test('adding same singleton lambda function as target mutiple times creates permission only once', () => { // GIVEN const stack = new cdk.Stack(); @@ -126,8 +147,8 @@ test('lambda handler and cloudwatch event across stacks', () => { expect(eventStack).toCountResources('AWS::Lambda::Permission', 1); }); -function newTestLambda(scope: constructs.Construct) { - return new lambda.Function(scope, 'MyLambda', { +function newTestLambda(scope: constructs.Construct, suffix = '') { + return new lambda.Function(scope, `MyLambda${suffix}`, { code: new lambda.InlineCode('foo'), handler: 'bar', runtime: lambda.Runtime.PYTHON_2_7, diff --git a/packages/@aws-cdk/aws-events-targets/test/logs/integ.log-group.expected.json b/packages/@aws-cdk/aws-events-targets/test/logs/integ.log-group.expected.json index 1f4e086b51607..9a150d9cafa32 100644 --- a/packages/@aws-cdk/aws-events-targets/test/logs/integ.log-group.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/logs/integ.log-group.expected.json @@ -98,63 +98,36 @@ ] }, "Create": { - "service": "CloudWatchLogs", - "action": "putResourcePolicy", - "parameters": { - "policyName": "loggroupeventsEventsLogGroupPolicyloggroupeventsTimerC63340B025F606BE", - "policyDocument": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"events.amazonaws.com\"},\"Resource\":\"", - { - "Fn::GetAtt": [ - "loggroupB02AAEB1", - "Arn" - ] - }, - "\"}],\"Version\":\"2012-10-17\"}" + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"loggroupeventsEventsLogGroupPolicyloggroupeventsTimerC63340B025F606BE\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"events.amazonaws.com\\\"},\\\"Resource\\\":\\\"", + { + "Fn::GetAtt": [ + "loggroupB02AAEB1", + "Arn" ] - ] - } - }, - "physicalResourceId": { - "id": "EventsLogGroupPolicyloggroupeventsTimerC63340B0" - } + }, + "\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"EventsLogGroupPolicyloggroupeventsTimerC63340B0\"}}" + ] + ] }, "Update": { - "service": "CloudWatchLogs", - "action": "putResourcePolicy", - "parameters": { - "policyName": "loggroupeventsEventsLogGroupPolicyloggroupeventsTimerC63340B025F606BE", - "policyDocument": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"events.amazonaws.com\"},\"Resource\":\"", - { - "Fn::GetAtt": [ - "loggroupB02AAEB1", - "Arn" - ] - }, - "\"}],\"Version\":\"2012-10-17\"}" + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"loggroupeventsEventsLogGroupPolicyloggroupeventsTimerC63340B025F606BE\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"events.amazonaws.com\\\"},\\\"Resource\\\":\\\"", + { + "Fn::GetAtt": [ + "loggroupB02AAEB1", + "Arn" ] - ] - } - }, - "physicalResourceId": { - "id": "EventsLogGroupPolicyloggroupeventsTimerC63340B0" - } - }, - "Delete": { - "service": "CloudWatchLogs", - "action": "deleteResourcePolicy", - "parameters": { - "policyName": "loggroupeventsEventsLogGroupPolicyloggroupeventsTimerC63340B025F606BE" - }, - "ignoreErrorCodesMatching": "400" + }, + "\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"EventsLogGroupPolicyloggroupeventsTimerC63340B0\"}}" + ] + ] }, + "Delete": "{\"service\":\"CloudWatchLogs\",\"action\":\"deleteResourcePolicy\",\"parameters\":{\"policyName\":\"loggroupeventsEventsLogGroupPolicyloggroupeventsTimerC63340B025F606BE\"},\"ignoreErrorCodesMatching\":\"400\"}", "InstallLatestAwsSdk": true }, "DependsOn": [ @@ -199,7 +172,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904" }, "S3Key": { "Fn::Join": [ @@ -212,7 +185,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -225,7 +198,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -235,13 +208,13 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs12.x", "Timeout": 120 }, @@ -326,63 +299,36 @@ ] }, "Create": { - "service": "CloudWatchLogs", - "action": "putResourcePolicy", - "parameters": { - "policyName": "loggroupeventsEventsLogGroupPolicyloggroupeventsTimer289E3527EF8F6205F", - "policyDocument": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"events.amazonaws.com\"},\"Resource\":\"", - { - "Fn::GetAtt": [ - "loggroup2F19C5C9B", - "Arn" - ] - }, - "\"}],\"Version\":\"2012-10-17\"}" + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"loggroupeventsEventsLogGroupPolicyloggroupeventsTimer289E3527EF8F6205F\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"events.amazonaws.com\\\"},\\\"Resource\\\":\\\"", + { + "Fn::GetAtt": [ + "loggroup2F19C5C9B", + "Arn" ] - ] - } - }, - "physicalResourceId": { - "id": "EventsLogGroupPolicyloggroupeventsTimer289E3527E" - } + }, + "\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"EventsLogGroupPolicyloggroupeventsTimer289E3527E\"}}" + ] + ] }, "Update": { - "service": "CloudWatchLogs", - "action": "putResourcePolicy", - "parameters": { - "policyName": "loggroupeventsEventsLogGroupPolicyloggroupeventsTimer289E3527EF8F6205F", - "policyDocument": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"events.amazonaws.com\"},\"Resource\":\"", - { - "Fn::GetAtt": [ - "loggroup2F19C5C9B", - "Arn" - ] - }, - "\"}],\"Version\":\"2012-10-17\"}" + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"loggroupeventsEventsLogGroupPolicyloggroupeventsTimer289E3527EF8F6205F\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"events.amazonaws.com\\\"},\\\"Resource\\\":\\\"", + { + "Fn::GetAtt": [ + "loggroup2F19C5C9B", + "Arn" ] - ] - } - }, - "physicalResourceId": { - "id": "EventsLogGroupPolicyloggroupeventsTimer289E3527E" - } - }, - "Delete": { - "service": "CloudWatchLogs", - "action": "deleteResourcePolicy", - "parameters": { - "policyName": "loggroupeventsEventsLogGroupPolicyloggroupeventsTimer289E3527EF8F6205F" - }, - "ignoreErrorCodesMatching": "400" + }, + "\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"EventsLogGroupPolicyloggroupeventsTimer289E3527E\"}}" + ] + ] }, + "Delete": "{\"service\":\"CloudWatchLogs\",\"action\":\"deleteResourcePolicy\",\"parameters\":{\"policyName\":\"loggroupeventsEventsLogGroupPolicyloggroupeventsTimer289E3527EF8F6205F\"},\"ignoreErrorCodesMatching\":\"400\"}", "InstallLatestAwsSdk": true }, "DependsOn": [ @@ -459,73 +405,46 @@ ] }, "Create": { - "service": "CloudWatchLogs", - "action": "putResourcePolicy", - "parameters": { - "policyName": "loggroupeventsEventsLogGroupPolicyloggroupeventsTimer37DF74C17EF314A8E", - "policyDocument": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"events.amazonaws.com\"},\"Resource\":\"arn:", - { - "Ref": "AWS::Partition" - }, - ":logs:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":log-group:MyLogGroupNameToBeImported:*\"}],\"Version\":\"2012-10-17\"}" - ] - ] - } - }, - "physicalResourceId": { - "id": "EventsLogGroupPolicyloggroupeventsTimer37DF74C17" - } + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"loggroupeventsEventsLogGroupPolicyloggroupeventsTimer37DF74C17EF314A8E\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"events.amazonaws.com\\\"},\\\"Resource\\\":\\\"arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:MyLogGroupNameToBeImported:*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"EventsLogGroupPolicyloggroupeventsTimer37DF74C17\"}}" + ] + ] }, "Update": { - "service": "CloudWatchLogs", - "action": "putResourcePolicy", - "parameters": { - "policyName": "loggroupeventsEventsLogGroupPolicyloggroupeventsTimer37DF74C17EF314A8E", - "policyDocument": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"events.amazonaws.com\"},\"Resource\":\"arn:", - { - "Ref": "AWS::Partition" - }, - ":logs:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":log-group:MyLogGroupNameToBeImported:*\"}],\"Version\":\"2012-10-17\"}" - ] - ] - } - }, - "physicalResourceId": { - "id": "EventsLogGroupPolicyloggroupeventsTimer37DF74C17" - } - }, - "Delete": { - "service": "CloudWatchLogs", - "action": "deleteResourcePolicy", - "parameters": { - "policyName": "loggroupeventsEventsLogGroupPolicyloggroupeventsTimer37DF74C17EF314A8E" - }, - "ignoreErrorCodesMatching": "400" + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"loggroupeventsEventsLogGroupPolicyloggroupeventsTimer37DF74C17EF314A8E\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"events.amazonaws.com\\\"},\\\"Resource\\\":\\\"arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:MyLogGroupNameToBeImported:*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"EventsLogGroupPolicyloggroupeventsTimer37DF74C17\"}}" + ] + ] }, + "Delete": "{\"service\":\"CloudWatchLogs\",\"action\":\"deleteResourcePolicy\",\"parameters\":{\"policyName\":\"loggroupeventsEventsLogGroupPolicyloggroupeventsTimer37DF74C17EF314A8E\"},\"ignoreErrorCodesMatching\":\"400\"}", "InstallLatestAwsSdk": true }, "DependsOn": [ @@ -536,17 +455,17 @@ } }, "Parameters": { - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904": { "Type": "String", - "Description": "S3 bucket for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 bucket for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF": { "Type": "String", - "Description": "S3 key for asset version \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 key for asset version \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94ArtifactHash782948FC": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343ArtifactHash0C561FF5": { "Type": "String", - "Description": "Artifact hash for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "Artifact hash for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/logs/log-group-resource-policy.test.ts b/packages/@aws-cdk/aws-events-targets/test/logs/log-group-resource-policy.test.ts index 999d813105baf..e60c4b84be8bb 100644 --- a/packages/@aws-cdk/aws-events-targets/test/logs/log-group-resource-policy.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/logs/log-group-resource-policy.test.ts @@ -31,35 +31,8 @@ test('minimal example renders correctly', () => { 'Arn', ], }, - Create: { - service: 'CloudWatchLogs', - action: 'putResourcePolicy', - parameters: { - policyName: 'TestPolicy', - policyDocument: '{"Statement":[{"Action":["logs:PutLogEvents","logs:CreateLogStream"],"Effect":"Allow","Principal":{"Service":"es.amazonaws.com"},"Resource":"*"}],"Version":"2012-10-17"}', - }, - physicalResourceId: { - id: 'LogGroupResourcePolicy', - }, - }, - Update: { - service: 'CloudWatchLogs', - action: 'putResourcePolicy', - parameters: { - policyName: 'TestPolicy', - policyDocument: '{"Statement":[{"Action":["logs:PutLogEvents","logs:CreateLogStream"],"Effect":"Allow","Principal":{"Service":"es.amazonaws.com"},"Resource":"*"}],"Version":"2012-10-17"}', - }, - physicalResourceId: { - id: 'LogGroupResourcePolicy', - }, - }, - Delete: { - service: 'CloudWatchLogs', - action: 'deleteResourcePolicy', - parameters: { - policyName: 'TestPolicy', - }, - ignoreErrorCodesMatching: '400', - }, + Create: '{"service":"CloudWatchLogs","action":"putResourcePolicy","parameters":{"policyName":"TestPolicy","policyDocument":"{\\"Statement\\":[{\\"Action\\":[\\"logs:PutLogEvents\\",\\"logs:CreateLogStream\\"],\\"Effect\\":\\"Allow\\",\\"Principal\\":{\\"Service\\":\\"es.amazonaws.com\\"},\\"Resource\\":\\"*\\"}],\\"Version\\":\\"2012-10-17\\"}"},"physicalResourceId":{"id":"LogGroupResourcePolicy"}}', + Update: '{"service":"CloudWatchLogs","action":"putResourcePolicy","parameters":{"policyName":"TestPolicy","policyDocument":"{\\"Statement\\":[{\\"Action\\":[\\"logs:PutLogEvents\\",\\"logs:CreateLogStream\\"],\\"Effect\\":\\"Allow\\",\\"Principal\\":{\\"Service\\":\\"es.amazonaws.com\\"},\\"Resource\\":\\"*\\"}],\\"Version\\":\\"2012-10-17\\"}"},"physicalResourceId":{"id":"LogGroupResourcePolicy"}}', + Delete: '{"service":"CloudWatchLogs","action":"deleteResourcePolicy","parameters":{"policyName":"TestPolicy"},"ignoreErrorCodesMatching":"400"}', }); }); diff --git a/packages/@aws-cdk/aws-events/README.md b/packages/@aws-cdk/aws-events/README.md index d19309455559d..565b0537d091d 100644 --- a/packages/@aws-cdk/aws-events/README.md +++ b/packages/@aws-cdk/aws-events/README.md @@ -163,3 +163,26 @@ In this situation, the CDK will wire the 2 accounts together: For more information, see the [AWS documentation on cross-account events](https://docs.aws.amazon.com/eventbridge/latest/userguide/eventbridge-cross-account-event-delivery.html). + +## Archiving + +It is possible to archive all or some events sent to an event bus. It is then possible to [replay these events](https://aws.amazon.com/blogs/aws/new-archive-and-replay-events-with-amazon-eventbridge/). + +```ts +import * as cdk from '@aws-cdk/core'; + +const stack = new stack(); + +const bus = new EventBus(stack, 'bus', { + eventBusName: 'MyCustomEventBus' +}); + +bus.archive('MyArchive', { + archiveName: 'MyCustomEventBusArchive', + description: 'MyCustomerEventBus Archive', + eventPattern: { + account: [stack.account], + }, + retention: cdk.Duration.days(365), +}); +``` diff --git a/packages/@aws-cdk/aws-events/lib/archive.ts b/packages/@aws-cdk/aws-events/lib/archive.ts new file mode 100644 index 0000000000000..3da79df6682a2 --- /dev/null +++ b/packages/@aws-cdk/aws-events/lib/archive.ts @@ -0,0 +1,77 @@ +import { Duration, Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { IEventBus } from './event-bus'; +import { EventPattern } from './event-pattern'; +import { CfnArchive } from './events.generated'; + +/** + * The event archive base properties + */ +export interface BaseArchiveProps { + /** + * The name of the archive. + * + * @default - Automatically generated + */ + readonly archiveName?: string; + /** + * A description for the archive. + * + * @default - none + */ + readonly description?: string; + /** + * An event pattern to use to filter events sent to the archive. + */ + readonly eventPattern: EventPattern; + /** + * The number of days to retain events for. Default value is 0. If set to 0, events are retained indefinitely. + * @default - Infinite + */ + readonly retention?: Duration; +} + + +/** + * The event archive properties + */ +export interface ArchiveProps extends BaseArchiveProps { + /** + * The event source associated with the archive. + */ + readonly sourceEventBus: IEventBus; +} + +/** + * Define an EventBridge Archive + * + * @resource AWS::Events::Archive + */ +export class Archive extends Resource { + /** + * The archive name. + * @attribute + */ + public readonly archiveName: string; + + /** + * The ARN of the archive created. + * @attribute + */ + public readonly archiveArn: string; + + constructor(scope: Construct, id: string, props: ArchiveProps) { + super(scope, id, { physicalName: props.archiveName }); + + let archive = new CfnArchive(this, 'Archive', { + sourceArn: props.sourceEventBus.eventBusArn, + description: props.description, + eventPattern: props.eventPattern, + retentionDays: props.retention?.toDays({ integral: true }) || 0, + archiveName: this.physicalName, + }); + + this.archiveArn = archive.attrArn; + this.archiveName = archive.attrArchiveName; + } +} diff --git a/packages/@aws-cdk/aws-events/lib/event-bus.ts b/packages/@aws-cdk/aws-events/lib/event-bus.ts index 27c79c9c7fe3a..cd0c7f913cbf6 100644 --- a/packages/@aws-cdk/aws-events/lib/event-bus.ts +++ b/packages/@aws-cdk/aws-events/lib/event-bus.ts @@ -1,6 +1,7 @@ import * as iam from '@aws-cdk/aws-iam'; import { IResource, Lazy, Names, Resource, Stack, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; +import { Archive, BaseArchiveProps } from './archive'; import { CfnEventBus } from './events.generated'; /** @@ -37,6 +38,15 @@ export interface IEventBus extends IResource { * @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-eventbus.html#cfn-events-eventbus-eventsourcename */ readonly eventSourceName?: string; + + /** + * Create an EventBridge archive to send events to. + * When you create an archive, incoming events might not immediately start being sent to the archive. + * Allow a short period of time for changes to take effect. + * + * @param props Properties of the archive + */ + archive(id: string, props: BaseArchiveProps): Archive; } /** @@ -96,12 +106,45 @@ export interface EventBusAttributes { readonly eventSourceName?: string; } +abstract class EventBusBase extends Resource implements IEventBus { + /** + * The physical ID of this event bus resource + */ + public abstract readonly eventBusName: string; + + /** + * The ARN of the event bus, such as: + * arn:aws:events:us-east-2:123456789012:event-bus/aws.partner/PartnerName/acct1/repo1. + */ + public abstract readonly eventBusArn: string; + + /** + * The policy for the event bus in JSON form. + */ + public abstract readonly eventBusPolicy: string; + + /** + * The name of the partner event source + */ + public abstract readonly eventSourceName?: string; + + public archive(id: string, props: BaseArchiveProps): Archive { + return new Archive(this, id, { + sourceEventBus: this, + description: props.description || `Event Archive for ${this.eventBusName} Event Bus`, + eventPattern: props.eventPattern, + retention: props.retention, + archiveName: props.archiveName, + }); + } +} + /** * Define an EventBridge EventBus * * @resource AWS::Events::EventBus */ -export class EventBus extends Resource implements IEventBus { +export class EventBus extends EventBusBase { /** * Import an existing event bus resource @@ -112,13 +155,11 @@ export class EventBus extends Resource implements IEventBus { public static fromEventBusArn(scope: Construct, id: string, eventBusArn: string): IEventBus { const parts = Stack.of(scope).parseArn(eventBusArn); - class Import extends Resource implements IEventBus { - public readonly eventBusArn = eventBusArn; - public readonly eventBusName = parts.resourceName || ''; - public readonly eventBusPolicy = ''; - } - - return new Import(scope, id); + return new ImportedEventBus(scope, id, { + eventBusArn: eventBusArn, + eventBusName: parts.resourceName || '', + eventBusPolicy: '', + }); } /** @@ -128,14 +169,7 @@ export class EventBus extends Resource implements IEventBus { * @param attrs Imported event bus properties */ public static fromEventBusAttributes(scope: Construct, id: string, attrs: EventBusAttributes): IEventBus { - class Import extends Resource implements IEventBus { - public readonly eventBusArn = attrs.eventBusArn; - public readonly eventBusName = attrs.eventBusName; - public readonly eventBusPolicy = attrs.eventBusPolicy; - public readonly eventSourceName = attrs.eventSourceName; - } - - return new Import(scope, id); + return new ImportedEventBus(scope, id, attrs); } /** @@ -241,3 +275,18 @@ export class EventBus extends Resource implements IEventBus { this.eventSourceName = eventBus.eventSourceName; } } + +class ImportedEventBus extends EventBusBase { + public readonly eventBusArn: string; + public readonly eventBusName: string; + public readonly eventBusPolicy: string; + public readonly eventSourceName?: string; + constructor(scope: Construct, id: string, attrs: EventBusAttributes) { + super(scope, id); + + this.eventBusArn = attrs.eventBusArn; + this.eventBusName = attrs.eventBusName; + this.eventBusPolicy = attrs.eventBusPolicy; + this.eventSourceName = attrs.eventSourceName; + } +} diff --git a/packages/@aws-cdk/aws-events/lib/index.ts b/packages/@aws-cdk/aws-events/lib/index.ts index e0aa655afaf72..718b236bf6e91 100644 --- a/packages/@aws-cdk/aws-events/lib/index.ts +++ b/packages/@aws-cdk/aws-events/lib/index.ts @@ -6,6 +6,7 @@ export * from './event-bus'; export * from './event-pattern'; export * from './schedule'; export * from './on-event-options'; +export * from './archive'; // AWS::Events CloudFormation Resources: export * from './events.generated'; diff --git a/packages/@aws-cdk/aws-events/test/test.archive.ts b/packages/@aws-cdk/aws-events/test/test.archive.ts new file mode 100644 index 0000000000000..fc8f38a516f2b --- /dev/null +++ b/packages/@aws-cdk/aws-events/test/test.archive.ts @@ -0,0 +1,45 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import { Duration, Stack } from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import { EventBus } from '../lib'; +import { Archive } from '../lib/archive'; + +export = { + 'creates an archive for an EventBus'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + let eventBus = new EventBus(stack, 'Bus'); + + new Archive(stack, 'Archive', { + sourceEventBus: eventBus, + eventPattern: { + account: [stack.account], + }, + retention: Duration.days(10), + }); + + // THEN + expect(stack).to(haveResource('AWS::Events::EventBus', { + Name: 'Bus', + })); + + expect(stack).to(haveResource('AWS::Events::Archive', { + EventPattern: { + account: [{ + Ref: 'AWS::AccountId', + }], + }, + RetentionDays: 10, + SourceArn: { + 'Fn::GetAtt': [ + 'BusEA82B648', + 'Arn', + ], + }, + })); + + test.done(); + }, +} diff --git a/packages/@aws-cdk/aws-events/test/test.event-bus.ts b/packages/@aws-cdk/aws-events/test/test.event-bus.ts index a6c32885ec4a9..2e8434e147bb1 100644 --- a/packages/@aws-cdk/aws-events/test/test.event-bus.ts +++ b/packages/@aws-cdk/aws-events/test/test.event-bus.ts @@ -245,6 +245,133 @@ export = { ], })); + test.done(); + }, + 'can archive events'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const event = new EventBus(stack, 'Bus'); + + event.archive('MyArchive', { + eventPattern: { + account: [stack.account], + }, + archiveName: 'MyArchive', + }); + + // THEN + expect(stack).to(haveResource('AWS::Events::EventBus', { + Name: 'Bus', + })); + + expect(stack).to(haveResource('AWS::Events::Archive', { + SourceArn: { + 'Fn::GetAtt': [ + 'BusEA82B648', + 'Arn', + ], + }, + Description: { + 'Fn::Join': [ + '', + [ + 'Event Archive for ', + { + Ref: 'BusEA82B648', + }, + ' Event Bus', + ], + ], + }, + EventPattern: { + account: [ + { + Ref: 'AWS::AccountId', + }, + ], + }, + RetentionDays: 0, + ArchiveName: 'MyArchive', + })); + + test.done(); + }, + 'can archive events from an imported EventBus'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const bus = new EventBus(stack, 'Bus'); + + const importedBus = EventBus.fromEventBusArn(stack, 'ImportedBus', bus.eventBusArn); + + importedBus.archive('MyArchive', { + eventPattern: { + account: [stack.account], + }, + archiveName: 'MyArchive', + }); + + // THEN + expect(stack).to(haveResource('AWS::Events::EventBus', { + Name: 'Bus', + })); + + expect(stack).to(haveResource('AWS::Events::Archive', { + SourceArn: { + 'Fn::GetAtt': [ + 'BusEA82B648', + 'Arn', + ], + }, + Description: { + 'Fn::Join': [ + '', + [ + 'Event Archive for ', + { + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + '/', + { + 'Fn::Select': [ + 5, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'BusEA82B648', + 'Arn', + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ' Event Bus', + ], + ], + }, + EventPattern: { + account: [ + { + Ref: 'AWS::AccountId', + }, + ], + }, + RetentionDays: 0, + ArchiveName: 'MyArchive', + })); + test.done(); }, }; diff --git a/packages/@aws-cdk/aws-globalaccelerator/test/globalaccelerator-security-group.test.ts b/packages/@aws-cdk/aws-globalaccelerator/test/globalaccelerator-security-group.test.ts index c7eb0d87f8fa8..20881d152f396 100644 --- a/packages/@aws-cdk/aws-globalaccelerator/test/globalaccelerator-security-group.test.ts +++ b/packages/@aws-cdk/aws-globalaccelerator/test/globalaccelerator-security-group.test.ts @@ -31,29 +31,16 @@ test('custom resource exists', () => { ], }, Create: { - action: 'describeSecurityGroups', - service: 'EC2', - parameters: { - Filters: [ + 'Fn::Join': [ + '', + [ + '{"service":"EC2","action":"describeSecurityGroups","parameters":{"Filters":[{"Name":"group-name","Values":["GlobalAccelerator"]},{"Name":"vpc-id","Values":["', { - Name: 'group-name', - Values: [ - 'GlobalAccelerator', - ], - }, - { - Name: 'vpc-id', - Values: [ - { - Ref: 'VPCB9E5F0B4', - }, - ], + Ref: 'VPCB9E5F0B4', }, + '"]}]},"physicalResourceId":{"responsePath":"SecurityGroups.0.GroupId"}}', ], - }, - physicalResourceId: { - responsePath: 'SecurityGroups.0.GroupId', - }, + ], }, InstallLatestAwsSdk: true, }, diff --git a/packages/@aws-cdk/aws-glue/package.json b/packages/@aws-cdk/aws-glue/package.json index 0eaa0ba607b63..3f6a8fa63ceba 100644 --- a/packages/@aws-cdk/aws-glue/package.json +++ b/packages/@aws-cdk/aws-glue/package.json @@ -72,6 +72,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-glue/test/table.test.ts b/packages/@aws-cdk/aws-glue/test/table.test.ts index 0969088346491..e31c879208aa2 100644 --- a/packages/@aws-cdk/aws-glue/test/table.test.ts +++ b/packages/@aws-cdk/aws-glue/test/table.test.ts @@ -5,8 +5,12 @@ 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 { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as glue from '../lib'; +const s3GrantWriteCtx = { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true }; + test('unpartitioned JSON table', () => { const app = new cdk.App(); const dbStack = new cdk.Stack(app, 'db'); @@ -1048,8 +1052,8 @@ test('grants: read only', () => { }); -test('grants: write only', () => { - const stack = new cdk.Stack(); +testFutureBehavior('grants: write only', s3GrantWriteCtx, cdk.App, (app) => { + const stack = new cdk.Stack(app); const user = new iam.User(stack, 'User'); const database = new glue.Database(stack, 'Database', { databaseName: 'database', @@ -1111,7 +1115,7 @@ test('grants: write only', () => { { Action: [ 's3:DeleteObject*', - 's3:PutObject*', + 's3:PutObject', 's3:Abort*', ], Effect: 'Allow', @@ -1151,8 +1155,8 @@ test('grants: write only', () => { }); -test('grants: read and write', () => { - const stack = new cdk.Stack(); +testFutureBehavior('grants: read and write', s3GrantWriteCtx, cdk.App, (app) => { + const stack = new cdk.Stack(app); const user = new iam.User(stack, 'User'); const database = new glue.Database(stack, 'Database', { databaseName: 'database', @@ -1225,7 +1229,7 @@ test('grants: read and write', () => { 's3:GetBucket*', 's3:List*', 's3:DeleteObject*', - 's3:PutObject*', + 's3:PutObject', 's3:Abort*', ], Effect: 'Allow', diff --git a/packages/@aws-cdk/aws-kinesis/package.json b/packages/@aws-cdk/aws-kinesis/package.json index e8153880812db..3c7fd281d363b 100644 --- a/packages/@aws-cdk/aws-kinesis/package.json +++ b/packages/@aws-cdk/aws-kinesis/package.json @@ -72,6 +72,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-kinesis/test/stream.test.ts b/packages/@aws-cdk/aws-kinesis/test/stream.test.ts index 9c3cdc1eb7125..b295ac73c75b5 100644 --- a/packages/@aws-cdk/aws-kinesis/test/stream.test.ts +++ b/packages/@aws-cdk/aws-kinesis/test/stream.test.ts @@ -3,6 +3,7 @@ import { arrayWith } from '@aws-cdk/assert'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import { App, Duration, Stack, CfnParameter } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { testFutureBehavior, testLegacyBehavior } from 'cdk-build-tools/lib/feature-flag'; import { Stream, StreamEncryption } from '../lib'; @@ -1255,7 +1256,7 @@ describe('Kinesis data streams', () => { }).toThrow(/'stack.' depends on 'stack.'/); }); - testFutureBehavior('cross stack permissions - with encryption', { '@aws-cdk/aws-kms:defaultKeyPolicies': true }, App, (app) => { + testFutureBehavior('cross stack permissions - with encryption', { [cxapi.KMS_DEFAULT_KEY_POLICIES]: true }, App, (app) => { const stackA = new Stack(app, 'stackA'); const streamFromStackA = new Stream(stackA, 'MyStream', { encryption: StreamEncryption.KMS, diff --git a/packages/@aws-cdk/aws-kms/test/key.test.ts b/packages/@aws-cdk/aws-kms/test/key.test.ts index b2d37496d00ec..38ae0e1e61059 100644 --- a/packages/@aws-cdk/aws-kms/test/key.test.ts +++ b/packages/@aws-cdk/aws-kms/test/key.test.ts @@ -2,6 +2,7 @@ import { arrayWith, ResourcePart } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { testFutureBehavior, testLegacyBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as kms from '../lib'; @@ -40,7 +41,7 @@ const LEGACY_ADMIN_ACTIONS: string[] = [ 'kms:UntagResource', ]; -const flags = { '@aws-cdk/aws-kms:defaultKeyPolicies': true }; +const flags = { [cxapi.KMS_DEFAULT_KEY_POLICIES]: true }; testFutureBehavior('default key', flags, cdk.App, (app) => { const stack = new cdk.Stack(app); diff --git a/packages/@aws-cdk/aws-lambda-destinations/README.md b/packages/@aws-cdk/aws-lambda-destinations/README.md index 8459820e9686e..404b0b3157adb 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/README.md +++ b/packages/@aws-cdk/aws-lambda-destinations/README.md @@ -21,7 +21,7 @@ The following destinations are supported * SNS topic * EventBridge event bus -Example with a SNS topic for sucessful invocations: +Example with a SNS topic for successful invocations: ```ts import * as lambda from '@aws-cdk/aws-lambda'; diff --git a/packages/@aws-cdk/aws-lambda-destinations/test/integ.lambda-chain.expected.json b/packages/@aws-cdk/aws-lambda-destinations/test/integ.lambda-chain.expected.json index f8f6f78713d64..5fc64df8417f3 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/test/integ.lambda-chain.expected.json +++ b/packages/@aws-cdk/aws-lambda-destinations/test/integ.lambda-chain.expected.json @@ -58,13 +58,13 @@ "Code": { "ZipFile": "exports.handler = async (event) => {\n console.log('Event: %j', event);\n if (event === 'error') throw new Error('UnkownError');\n return event;\n };" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "FirstServiceRole097DB3A5", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -114,7 +114,7 @@ ] } }, - "FirstEventInvokeConfigFailureAllowEventRuleawscdklambdachainFirstEventInvokeConfigFailure7180F42FA8F1F1F0": { + "FirstEventInvokeConfigFailureAllowEventRuleawscdklambdachainErrorC073CD8DCAD68018": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", @@ -175,7 +175,7 @@ ] } }, - "FirstEventInvokeConfigSuccessAllowEventRuleawscdklambdachainFirstEventInvokeConfigSuccess2DCAE39FC2495AB7": { + "FirstEventInvokeConfigSuccessAllowEventRuleawscdklambdachainSecond178F48F8A8DE2790": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", @@ -308,13 +308,13 @@ "Code": { "ZipFile": "exports.handler = async (event) => {\n console.log('Event: %j', event);\n if (event === 'error') throw new Error('UnkownError');\n return event;\n };" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "SecondServiceRole55940A31", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -364,7 +364,7 @@ ] } }, - "SecondEventInvokeConfigSuccessAllowEventRuleawscdklambdachainSecondEventInvokeConfigSuccess2078CDC9C7FB9F61": { + "SecondEventInvokeConfigSuccessAllowEventRuleawscdklambdachainThird031C7FF6ABA1C15A": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", @@ -453,13 +453,13 @@ "Code": { "ZipFile": "exports.handler = async (event) => {\n console.log('Event: %j', event);\n if (event === 'error') throw new Error('UnkownError');\n return event;\n };" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "ThirdServiceRole42701801", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -503,13 +503,13 @@ "Code": { "ZipFile": "exports.handler = async (event) => {\n console.log('Event: %j', event);\n if (event === 'error') throw new Error('UnkownError');\n return event;\n };" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "ErrorServiceRoleCE484966", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-lambda-nodejs/README.md b/packages/@aws-cdk/aws-lambda-nodejs/README.md index f35062120fb9d..d11602c5e656e 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/README.md +++ b/packages/@aws-cdk/aws-lambda-nodejs/README.md @@ -148,7 +148,7 @@ new lambda.NodejsFunction(this, 'my-handler', { }, logLevel: LogLevel.SILENT, // defaults to LogLevel.WARNING keepNames: true, // defaults to false - tsconfig: 'custom-tsconfig.json' // use custom-tsconfig.json instead of default, + tsconfig: 'custom-tsconfig.json' // use custom-tsconfig.json instead of default, metafile: true, // include meta file, defaults to false banner : '/* comments */', // by default no comments are passed footer : '/* comments */', // by default no comments are passed @@ -220,7 +220,7 @@ Use `bundling.dockerImage` to use a custom Docker bundling image: ```ts new lambda.NodejsFunction(this, 'my-handler', { bundling: { - dockerImage: cdk.BundlingDockerImage.fromAsset('/path/to/Dockerfile'), + dockerImage: cdk.DockerImage.fromBuild('/path/to/Dockerfile'), }, }); ``` diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts index a6dcddde7709d..536ca1ea7646a 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts @@ -140,10 +140,10 @@ export class Bundling implements cdk.BundlingOptions { const esbuildCommand: string = [ npx, 'esbuild', - '--bundle', pathJoin(inputDir, this.relativeEntryPath), + '--bundle', `"${pathJoin(inputDir, this.relativeEntryPath)}"`, `--target=${this.props.target ?? toTarget(this.props.runtime)}`, '--platform=node', - `--outfile=${pathJoin(outputDir, 'index.js')}`, + `--outfile="${pathJoin(outputDir, 'index.js')}"`, ...this.props.minify ? ['--minify'] : [], ...this.props.sourceMap ? ['--sourcemap'] : [], ...this.externals.map(external => `--external:${external}`), diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts index bd69394ae757c..e6c32c496b2ff 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts @@ -20,7 +20,7 @@ beforeEach(() => { getEsBuildVersionMock.mockReturnValue('0.8.8'); fromAssetMock.mockReturnValue({ image: 'built-image', - cp: () => {}, + cp: () => 'dest-path', run: () => {}, toJSON: () => 'built-image', }); @@ -53,7 +53,7 @@ test('esbuild bundling in Docker', () => { }, command: [ 'bash', '-c', - 'npx esbuild --bundle /asset-input/lib/handler.ts --target=node12 --platform=node --outfile=/asset-output/index.js --external:aws-sdk --loader:.png=dataurl', + 'npx esbuild --bundle "/asset-input/lib/handler.ts" --target=node12 --platform=node --outfile="/asset-output/index.js" --external:aws-sdk --loader:.png=dataurl', ], workingDirectory: '/', }), @@ -74,7 +74,7 @@ test('esbuild bundling with handler named index.ts', () => { bundling: expect.objectContaining({ command: [ 'bash', '-c', - 'npx esbuild --bundle /asset-input/lib/index.ts --target=node12 --platform=node --outfile=/asset-output/index.js --external:aws-sdk', + 'npx esbuild --bundle "/asset-input/lib/index.ts" --target=node12 --platform=node --outfile="/asset-output/index.js" --external:aws-sdk', ], }), }); @@ -94,7 +94,7 @@ test('esbuild bundling with tsx handler', () => { bundling: expect.objectContaining({ command: [ 'bash', '-c', - 'npx esbuild --bundle /asset-input/lib/handler.tsx --target=node12 --platform=node --outfile=/asset-output/index.js --external:aws-sdk', + 'npx esbuild --bundle "/asset-input/lib/handler.tsx" --target=node12 --platform=node --outfile="/asset-output/index.js" --external:aws-sdk', ], }), }); @@ -139,7 +139,7 @@ test('esbuild bundling with externals and dependencies', () => { command: [ 'bash', '-c', [ - 'npx esbuild --bundle /asset-input/test/bundling.test.js --target=node12 --platform=node --outfile=/asset-output/index.js --external:abc --external:delay', + 'npx esbuild --bundle "/asset-input/test/bundling.test.js" --target=node12 --platform=node --outfile="/asset-output/index.js" --external:abc --external:delay', `echo \'{\"dependencies\":{\"delay\":\"${delayVersion}\"}}\' > /asset-output/package.json`, 'cp /asset-input/package-lock.json /asset-output/package-lock.json', 'cd /asset-output', @@ -181,8 +181,8 @@ test('esbuild bundling with esbuild options', () => { command: [ 'bash', '-c', [ - 'npx esbuild --bundle /asset-input/lib/handler.ts', - '--target=es2020 --platform=node --outfile=/asset-output/index.js', + 'npx esbuild --bundle "/asset-input/lib/handler.ts"', + '--target=es2020 --platform=node --outfile="/asset-output/index.js"', '--minify --sourcemap --external:aws-sdk --loader:.png=dataurl', '--define:DEBUG=true --define:process.env.KEY="VALUE"', '--log-level=silent --keep-names --tsconfig=/asset-input/lib/custom-tsconfig.ts', diff --git a/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts b/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts index d5747e169abb2..a4446611d9a9d 100644 --- a/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts +++ b/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts @@ -31,15 +31,56 @@ export interface BundlingOptions { * Output path suffix ('python' for a layer, '.' otherwise) */ readonly outputPathSuffix: string; + + /** + * Determines how asset hash is calculated. Assets will get rebuild and + * uploaded only if their hash has changed. + * + * If asset hash is set to `SOURCE` (default), then only changes to the source + * directory will cause the asset to rebuild. This means, for example, that in + * order to pick up a new dependency version, a change must be made to the + * source tree. Ideally, this can be implemented by including a dependency + * lockfile in your source tree or using fixed dependencies. + * + * If the asset hash is set to `OUTPUT`, the hash is calculated after + * bundling. This means that any change in the output will cause the asset to + * be invalidated and uploaded. Bear in mind that `pip` adds timestamps to + * dependencies it installs, which implies that in this mode Python bundles + * will _always_ get rebuild and uploaded. Normally this is an anti-pattern + * since build + * + * @default AssetHashType.SOURCE By default, hash is calculated based on the + * contents of the source directory. If `assetHash` is also specified, the + * default is `CUSTOM`. This means that only updates to the source will cause + * the asset to rebuild. + */ + readonly assetHashType?: cdk.AssetHashType; + + /** + * Specify a custom hash for this asset. If `assetHashType` is set it must + * be set to `AssetHashType.CUSTOM`. For consistency, this custom hash will + * be SHA256 hashed and encoded as hex. The resulting hash will be the asset + * hash. + * + * NOTE: the hash is used in order to identify a specific revision of the asset, and + * used for optimizing and caching deployment activities related to this asset such as + * packaging, uploading to Amazon S3, etc. If you chose to customize the hash, you will + * need to make sure it is updated every time the asset changes, or otherwise it is + * possible that some deployments will not be invalidated. + * + * @default - based on `assetHashType` + */ + readonly assetHash?: string; } /** * Produce bundled Lambda asset code */ -export function bundle(options: BundlingOptions): lambda.AssetCode { +export function bundle(options: BundlingOptions): lambda.Code { const { entry, runtime, outputPathSuffix } = options; - const hasDeps = hasDependencies(entry); + const stagedir = cdk.FileSystem.mkdtemp('python-bundling-'); + const hasDeps = stageDependencies(entry, stagedir); const depsCommand = chain([ hasDeps ? `rsync -r ${BUNDLER_DEPENDENCIES_CACHE}/. ${cdk.AssetStaging.BUNDLING_OUTPUT_DIR}/${outputPathSuffix}` : '', @@ -54,15 +95,19 @@ export function bundle(options: BundlingOptions): lambda.AssetCode { ? 'Dockerfile.dependencies' : 'Dockerfile'; - const image = cdk.BundlingDockerImage.fromAsset(entry, { + // copy Dockerfile to workdir + fs.copyFileSync(path.join(__dirname, dockerfile), path.join(stagedir, dockerfile)); + + const image = cdk.BundlingDockerImage.fromAsset(stagedir, { buildArgs: { IMAGE: runtime.bundlingDockerImage.image, }, - file: path.join(__dirname, dockerfile), + file: dockerfile, }); return lambda.Code.fromAsset(entry, { - assetHashType: cdk.AssetHashType.BUNDLE, + assetHashType: options.assetHashType, + assetHash: options.assetHash, exclude: DEPENDENCY_EXCLUDES, bundling: { image, @@ -75,20 +120,25 @@ export function bundle(options: BundlingOptions): lambda.AssetCode { * Checks to see if the `entry` directory contains a type of dependency that * we know how to install. */ -export function hasDependencies(entry: string): boolean { - if (fs.existsSync(path.join(entry, 'Pipfile'))) { - return true; - } - - if (fs.existsSync(path.join(entry, 'poetry.lock'))) { - return true; - } - - if (fs.existsSync(path.join(entry, 'requirements.txt'))) { - return true; +export function stageDependencies(entry: string, stagedir: string): boolean { + const prefixes = [ + 'Pipfile', + 'pyproject', + 'poetry', + 'requirements.txt', + ]; + + let found = false; + for (const file of fs.readdirSync(entry)) { + for (const prefix of prefixes) { + if (file.startsWith(prefix)) { + fs.copyFileSync(path.join(entry, file), path.join(stagedir, file)); + found = true; + } + } } - return false; + return found; } function chain(commands: string[]): string { diff --git a/packages/@aws-cdk/aws-lambda-python/lib/function.ts b/packages/@aws-cdk/aws-lambda-python/lib/function.ts index 267245738cbe6..733c115c0383d 100644 --- a/packages/@aws-cdk/aws-lambda-python/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda-python/lib/function.ts @@ -1,6 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as lambda from '@aws-cdk/aws-lambda'; +import { AssetHashType } from '@aws-cdk/core'; import { bundle } from './bundling'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main @@ -37,6 +38,45 @@ export interface PythonFunctionProps extends lambda.FunctionOptions { * @default lambda.Runtime.PYTHON_3_7 */ readonly runtime?: lambda.Runtime; + + /** + * Determines how asset hash is calculated. Assets will get rebuild and + * uploaded only if their hash has changed. + * + * If asset hash is set to `SOURCE` (default), then only changes to the source + * directory will cause the asset to rebuild. This means, for example, that in + * order to pick up a new dependency version, a change must be made to the + * source tree. Ideally, this can be implemented by including a dependency + * lockfile in your source tree or using fixed dependencies. + * + * If the asset hash is set to `OUTPUT`, the hash is calculated after + * bundling. This means that any change in the output will cause the asset to + * be invalidated and uploaded. Bear in mind that `pip` adds timestamps to + * dependencies it installs, which implies that in this mode Python bundles + * will _always_ get rebuild and uploaded. Normally this is an anti-pattern + * since build + * + * @default AssetHashType.SOURCE By default, hash is calculated based on the + * contents of the source directory. This means that only updates to the + * source will cause the asset to rebuild. + */ + readonly assetHashType?: AssetHashType; + + /** + * Specify a custom hash for this asset. If `assetHashType` is set it must + * be set to `AssetHashType.CUSTOM`. For consistency, this custom hash will + * be SHA256 hashed and encoded as hex. The resulting hash will be the asset + * hash. + * + * NOTE: the hash is used in order to identify a specific revision of the asset, and + * used for optimizing and caching deployment activities related to this asset such as + * packaging, uploading to Amazon S3, etc. If you chose to customize the hash, you will + * need to make sure it is updated every time the asset changes, or otherwise it is + * possible that some deployments will not be invalidated. + * + * @default - based on `assetHashType` + */ + readonly assetHash?: string; } /** @@ -70,6 +110,8 @@ export class PythonFunction extends lambda.Function { runtime, entry, outputPathSuffix: '.', + assetHashType: props.assetHashType, + assetHash: props.assetHash, }), handler: `${index.slice(0, -3)}.${handler}`, }); diff --git a/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts b/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts index 4286d092adf8f..d6de4ee23aedd 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts +++ b/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts @@ -1,11 +1,10 @@ import * as fs from 'fs'; import * as path from 'path'; import { Code, Runtime } from '@aws-cdk/aws-lambda'; -import { hasDependencies, bundle } from '../lib/bundling'; +import { FileSystem } from '@aws-cdk/core'; +import { stageDependencies, bundle } from '../lib/bundling'; jest.mock('@aws-cdk/aws-lambda'); -const existsSyncOriginal = fs.existsSync; -const existsSyncMock = jest.spyOn(fs, 'existsSync'); jest.mock('child_process', () => ({ spawnSync: jest.fn(() => { @@ -41,9 +40,6 @@ test('Bundling a function without dependencies', () => { ], }), })); - - // Searches for requirements.txt in entry - expect(existsSyncMock).toHaveBeenCalledWith(path.join(entry, 'requirements.txt')); }); test('Bundling a function with requirements.txt installed', () => { @@ -63,9 +59,6 @@ test('Bundling a function with requirements.txt installed', () => { ], }), })); - - // Searches for requirements.txt in entry - expect(existsSyncMock).toHaveBeenCalledWith(path.join(entry, 'requirements.txt')); }); test('Bundling Python 2.7 with requirements.txt installed', () => { @@ -85,9 +78,6 @@ test('Bundling Python 2.7 with requirements.txt installed', () => { ], }), })); - - // Searches for requirements.txt in entry - expect(existsSyncMock).toHaveBeenCalledWith(path.join(entry, 'requirements.txt')); }); test('Bundling a layer with dependencies', () => { @@ -128,42 +118,24 @@ test('Bundling a python code layer', () => { })); }); -describe('Dependency detection', () => { - test('Detects pipenv', () => { - existsSyncMock.mockImplementation((p: fs.PathLike) => { - if (/Pipfile/.test(p.toString())) { - return true; - } - return existsSyncOriginal(p); - }); - - expect(hasDependencies('/asset-input')).toEqual(true); - }); - - test('Detects poetry', () => { - existsSyncMock.mockImplementation((p: fs.PathLike) => { - if (/poetry.lock/.test(p.toString())) { - return true; - } - return existsSyncOriginal(p); - }); - - expect(hasDependencies('/asset-input')).toEqual(true); - }); - test('Detects requirements.txt', () => { - existsSyncMock.mockImplementation((p: fs.PathLike) => { - if (/requirements.txt/.test(p.toString())) { - return true; - } - return existsSyncOriginal(p); - }); - - expect(hasDependencies('/asset-input')).toEqual(true); +describe('Dependency detection', () => { + test.each(['Pipfile', 'poetry.lock', 'requirements.txt'])('detect dependency %p', filename => { + // GIVEN + const sourcedir = FileSystem.mkdtemp('source-'); + const stagedir = FileSystem.mkdtemp('stage-'); + fs.writeFileSync(path.join(sourcedir, filename), 'dummy!'); + + // WHEN + const found = stageDependencies(sourcedir, stagedir); + + // THEN + expect(found).toBeTruthy(); + expect(fs.existsSync(path.join(stagedir, filename))).toBeTruthy(); }); test('No known dependencies', () => { - existsSyncMock.mockImplementation(() => false); - expect(hasDependencies('/asset-input')).toEqual(false); + const sourcedir = FileSystem.mkdtemp('source-'); + expect(stageDependencies(sourcedir, '/dummy')).toEqual(false); }); }); diff --git a/packages/@aws-cdk/aws-lambda-python/test/function.test.ts b/packages/@aws-cdk/aws-lambda-python/test/function.test.ts index 98bab1cf35be4..d1f9d64241b61 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/function.test.ts +++ b/packages/@aws-cdk/aws-lambda-python/test/function.test.ts @@ -1,16 +1,36 @@ import '@aws-cdk/assert/jest'; -import { Runtime } from '@aws-cdk/aws-lambda'; -import { Stack } from '@aws-cdk/core'; +import { Code, Runtime } from '@aws-cdk/aws-lambda'; +import { AssetHashType, AssetOptions, Stack } from '@aws-cdk/core'; import { PythonFunction } from '../lib'; import { bundle } from '../lib/bundling'; jest.mock('../lib/bundling', () => { return { - bundle: jest.fn().mockReturnValue({ - bind: () => { - return { inlineCode: 'code' }; - }, - bindToResource: () => { return; }, + bundle: jest.fn().mockImplementation((options: AssetOptions): Code => { + const mockObjectKey = (() => { + const hashType = options.assetHashType ?? (options.assetHash ? 'custom' : 'source'); + switch (hashType) { + case 'source': return 'SOURCE_MOCK'; + case 'output': return 'OUTPUT_MOCK'; + case 'custom': { + if (!options.assetHash) { throw new Error('no custom hash'); } + return options.assetHash; + } + } + + throw new Error('unexpected asset hash type'); + })(); + + return { + isInline: false, + bind: () => ({ + s3Location: { + bucketName: 'mock-bucket-name', + objectKey: mockObjectKey, + }, + }), + bindToResource: () => { return; }, + }; }), hasDependencies: jest.fn().mockReturnValue(false), }; @@ -73,3 +93,53 @@ test('throws with the wrong runtime family', () => { runtime: Runtime.NODEJS_12_X, })).toThrow(/Only `PYTHON` runtimes are supported/); }); + +test('allows specifying hash type', () => { + new PythonFunction(stack, 'source1', { + entry: 'test/lambda-handler-nodeps', + index: 'index.py', + handler: 'handler', + }); + + new PythonFunction(stack, 'source2', { + entry: 'test/lambda-handler-nodeps', + index: 'index.py', + handler: 'handler', + assetHashType: AssetHashType.SOURCE, + }); + + new PythonFunction(stack, 'output', { + entry: 'test/lambda-handler-nodeps', + index: 'index.py', + handler: 'handler', + assetHashType: AssetHashType.OUTPUT, + }); + + new PythonFunction(stack, 'custom', { + entry: 'test/lambda-handler-nodeps', + index: 'index.py', + handler: 'handler', + assetHash: 'MY_CUSTOM_HASH', + }); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + Code: { + S3Bucket: 'mock-bucket-name', + S3Key: 'SOURCE_MOCK', + }, + }); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + Code: { + S3Bucket: 'mock-bucket-name', + S3Key: 'OUTPUT_MOCK', + }, + }); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + Code: { + S3Bucket: 'mock-bucket-name', + S3Key: 'MY_CUSTOM_HASH', + }, + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json index 3690005685439..45fb46b70aeb9 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3Bucket414E0E30" + "Ref": "AssetParameters79d8f328899b90e2c16929e9393ebf344f098abde8981abdff0168fc9b0589acS3Bucket424FEB44" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3VersionKey5ABC9098" + "Ref": "AssetParameters79d8f328899b90e2c16929e9393ebf344f098abde8981abdff0168fc9b0589acS3VersionKeyCEB2635C" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3VersionKey5ABC9098" + "Ref": "AssetParameters79d8f328899b90e2c16929e9393ebf344f098abde8981abdff0168fc9b0589acS3VersionKeyCEB2635C" } ] } @@ -87,17 +87,17 @@ } }, "Parameters": { - "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3Bucket414E0E30": { + "AssetParameters79d8f328899b90e2c16929e9393ebf344f098abde8981abdff0168fc9b0589acS3Bucket424FEB44": { "Type": "String", - "Description": "S3 bucket for asset \"4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353\"" + "Description": "S3 bucket for asset \"79d8f328899b90e2c16929e9393ebf344f098abde8981abdff0168fc9b0589ac\"" }, - "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3VersionKey5ABC9098": { + "AssetParameters79d8f328899b90e2c16929e9393ebf344f098abde8981abdff0168fc9b0589acS3VersionKeyCEB2635C": { "Type": "String", - "Description": "S3 key for asset version \"4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353\"" + "Description": "S3 key for asset version \"79d8f328899b90e2c16929e9393ebf344f098abde8981abdff0168fc9b0589ac\"" }, - "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353ArtifactHashECA6C88C": { + "AssetParameters79d8f328899b90e2c16929e9393ebf344f098abde8981abdff0168fc9b0589acArtifactHashE38133D4": { "Type": "String", - "Description": "Artifact hash for asset \"4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353\"" + "Description": "Artifact hash for asset \"79d8f328899b90e2c16929e9393ebf344f098abde8981abdff0168fc9b0589ac\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.expected.json index ef1f355e528c3..aa7bc5fbbcd68 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45cS3BucketA501FC08" + "Ref": "AssetParameters50fe6f46cf18fb257f00fed007da5c80fbf5dee08bec37fe765c50188eecae0bS3Bucket4125B4E4" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45cS3VersionKey1C3AFB39" + "Ref": "AssetParameters50fe6f46cf18fb257f00fed007da5c80fbf5dee08bec37fe765c50188eecae0bS3VersionKey1CF28CF5" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45cS3VersionKey1C3AFB39" + "Ref": "AssetParameters50fe6f46cf18fb257f00fed007da5c80fbf5dee08bec37fe765c50188eecae0bS3VersionKey1CF28CF5" } ] } @@ -121,7 +121,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28S3Bucket7DE4D4D5" + "Ref": "AssetParameters314bcf2cee9a5b01d3be2c2815602d1d784611fac220dde672aca6cb54299076S3BucketD6F6E4F2" }, "S3Key": { "Fn::Join": [ @@ -134,7 +134,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28S3VersionKeyAEB67E87" + "Ref": "AssetParameters314bcf2cee9a5b01d3be2c2815602d1d784611fac220dde672aca6cb54299076S3VersionKeyE0B47D8E" } ] } @@ -147,7 +147,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28S3VersionKeyAEB67E87" + "Ref": "AssetParameters314bcf2cee9a5b01d3be2c2815602d1d784611fac220dde672aca6cb54299076S3VersionKeyE0B47D8E" } ] } @@ -206,7 +206,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009S3BucketA66E9035" + "Ref": "AssetParameters11846740cde0308720709e44dce627bf1ceb557ee9d0dbb556a05632da565ef2S3BucketE17A9F3E" }, "S3Key": { "Fn::Join": [ @@ -219,7 +219,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009S3VersionKeyAFEB5FDA" + "Ref": "AssetParameters11846740cde0308720709e44dce627bf1ceb557ee9d0dbb556a05632da565ef2S3VersionKey0A0F7F93" } ] } @@ -232,7 +232,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009S3VersionKeyAFEB5FDA" + "Ref": "AssetParameters11846740cde0308720709e44dce627bf1ceb557ee9d0dbb556a05632da565ef2S3VersionKey0A0F7F93" } ] } @@ -257,41 +257,41 @@ } }, "Parameters": { - "AssetParameters94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45cS3BucketA501FC08": { + "AssetParameters50fe6f46cf18fb257f00fed007da5c80fbf5dee08bec37fe765c50188eecae0bS3Bucket4125B4E4": { "Type": "String", - "Description": "S3 bucket for asset \"94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45c\"" + "Description": "S3 bucket for asset \"50fe6f46cf18fb257f00fed007da5c80fbf5dee08bec37fe765c50188eecae0b\"" }, - "AssetParameters94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45cS3VersionKey1C3AFB39": { + "AssetParameters50fe6f46cf18fb257f00fed007da5c80fbf5dee08bec37fe765c50188eecae0bS3VersionKey1CF28CF5": { "Type": "String", - "Description": "S3 key for asset version \"94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45c\"" + "Description": "S3 key for asset version \"50fe6f46cf18fb257f00fed007da5c80fbf5dee08bec37fe765c50188eecae0b\"" }, - "AssetParameters94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45cArtifactHash99DC751A": { + "AssetParameters50fe6f46cf18fb257f00fed007da5c80fbf5dee08bec37fe765c50188eecae0bArtifactHashC28E4EDF": { "Type": "String", - "Description": "Artifact hash for asset \"94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45c\"" + "Description": "Artifact hash for asset \"50fe6f46cf18fb257f00fed007da5c80fbf5dee08bec37fe765c50188eecae0b\"" }, - "AssetParameters3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28S3Bucket7DE4D4D5": { + "AssetParameters314bcf2cee9a5b01d3be2c2815602d1d784611fac220dde672aca6cb54299076S3BucketD6F6E4F2": { "Type": "String", - "Description": "S3 bucket for asset \"3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28\"" + "Description": "S3 bucket for asset \"314bcf2cee9a5b01d3be2c2815602d1d784611fac220dde672aca6cb54299076\"" }, - "AssetParameters3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28S3VersionKeyAEB67E87": { + "AssetParameters314bcf2cee9a5b01d3be2c2815602d1d784611fac220dde672aca6cb54299076S3VersionKeyE0B47D8E": { "Type": "String", - "Description": "S3 key for asset version \"3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28\"" + "Description": "S3 key for asset version \"314bcf2cee9a5b01d3be2c2815602d1d784611fac220dde672aca6cb54299076\"" }, - "AssetParameters3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28ArtifactHashE51CE860": { + "AssetParameters314bcf2cee9a5b01d3be2c2815602d1d784611fac220dde672aca6cb54299076ArtifactHash45C2DF56": { "Type": "String", - "Description": "Artifact hash for asset \"3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28\"" + "Description": "Artifact hash for asset \"314bcf2cee9a5b01d3be2c2815602d1d784611fac220dde672aca6cb54299076\"" }, - "AssetParameters876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009S3BucketA66E9035": { + "AssetParameters11846740cde0308720709e44dce627bf1ceb557ee9d0dbb556a05632da565ef2S3BucketE17A9F3E": { "Type": "String", - "Description": "S3 bucket for asset \"876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009\"" + "Description": "S3 bucket for asset \"11846740cde0308720709e44dce627bf1ceb557ee9d0dbb556a05632da565ef2\"" }, - "AssetParameters876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009S3VersionKeyAFEB5FDA": { + "AssetParameters11846740cde0308720709e44dce627bf1ceb557ee9d0dbb556a05632da565ef2S3VersionKey0A0F7F93": { "Type": "String", - "Description": "S3 key for asset version \"876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009\"" + "Description": "S3 key for asset version \"11846740cde0308720709e44dce627bf1ceb557ee9d0dbb556a05632da565ef2\"" }, - "AssetParameters876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009ArtifactHashB9A1080D": { + "AssetParameters11846740cde0308720709e44dce627bf1ceb557ee9d0dbb556a05632da565ef2ArtifactHash743A82BD": { "Type": "String", - "Description": "Artifact hash for asset \"876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009\"" + "Description": "Artifact hash for asset \"11846740cde0308720709e44dce627bf1ceb557ee9d0dbb556a05632da565ef2\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.poetry.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.poetry.expected.json index 5ea17bca31920..c2ad372f25649 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.poetry.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.poetry.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175efS3BucketD53ED9C5" + "Ref": "AssetParameters3bc4d0e28b60c2b2468004185dabbe33a91c04563872cafb35b5b71e8a8f33d4S3Bucket68AF28A9" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175efS3VersionKey3C218A3E" + "Ref": "AssetParameters3bc4d0e28b60c2b2468004185dabbe33a91c04563872cafb35b5b71e8a8f33d4S3VersionKeyACC0084B" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175efS3VersionKey3C218A3E" + "Ref": "AssetParameters3bc4d0e28b60c2b2468004185dabbe33a91c04563872cafb35b5b71e8a8f33d4S3VersionKeyACC0084B" } ] } @@ -121,7 +121,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956dS3BucketFDE171D0" + "Ref": "AssetParametersc1614067648b8b7e151e321ce82879d259a2b8f2bd10dddd61f0f2ce26287c17S3Bucket980A99E7" }, "S3Key": { "Fn::Join": [ @@ -134,7 +134,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956dS3VersionKey6209E240" + "Ref": "AssetParametersc1614067648b8b7e151e321ce82879d259a2b8f2bd10dddd61f0f2ce26287c17S3VersionKeyC4E1E9B5" } ] } @@ -147,7 +147,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956dS3VersionKey6209E240" + "Ref": "AssetParametersc1614067648b8b7e151e321ce82879d259a2b8f2bd10dddd61f0f2ce26287c17S3VersionKeyC4E1E9B5" } ] } @@ -206,7 +206,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68S3BucketA23E6312" + "Ref": "AssetParameters3610bde00ecd0013f7806e2ab0e80d7ac26232cd3ffc2934b5ca28fef120bdf6S3Bucket91AABE39" }, "S3Key": { "Fn::Join": [ @@ -219,7 +219,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68S3VersionKey1E21AF83" + "Ref": "AssetParameters3610bde00ecd0013f7806e2ab0e80d7ac26232cd3ffc2934b5ca28fef120bdf6S3VersionKeyEE0FAD90" } ] } @@ -232,7 +232,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68S3VersionKey1E21AF83" + "Ref": "AssetParameters3610bde00ecd0013f7806e2ab0e80d7ac26232cd3ffc2934b5ca28fef120bdf6S3VersionKeyEE0FAD90" } ] } @@ -257,41 +257,41 @@ } }, "Parameters": { - "AssetParameters61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175efS3BucketD53ED9C5": { + "AssetParameters3bc4d0e28b60c2b2468004185dabbe33a91c04563872cafb35b5b71e8a8f33d4S3Bucket68AF28A9": { "Type": "String", - "Description": "S3 bucket for asset \"61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175ef\"" + "Description": "S3 bucket for asset \"3bc4d0e28b60c2b2468004185dabbe33a91c04563872cafb35b5b71e8a8f33d4\"" }, - "AssetParameters61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175efS3VersionKey3C218A3E": { + "AssetParameters3bc4d0e28b60c2b2468004185dabbe33a91c04563872cafb35b5b71e8a8f33d4S3VersionKeyACC0084B": { "Type": "String", - "Description": "S3 key for asset version \"61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175ef\"" + "Description": "S3 key for asset version \"3bc4d0e28b60c2b2468004185dabbe33a91c04563872cafb35b5b71e8a8f33d4\"" }, - "AssetParameters61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175efArtifactHash6A1881A8": { + "AssetParameters3bc4d0e28b60c2b2468004185dabbe33a91c04563872cafb35b5b71e8a8f33d4ArtifactHashC2D4B1C3": { "Type": "String", - "Description": "Artifact hash for asset \"61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175ef\"" + "Description": "Artifact hash for asset \"3bc4d0e28b60c2b2468004185dabbe33a91c04563872cafb35b5b71e8a8f33d4\"" }, - "AssetParameters1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956dS3BucketFDE171D0": { + "AssetParametersc1614067648b8b7e151e321ce82879d259a2b8f2bd10dddd61f0f2ce26287c17S3Bucket980A99E7": { "Type": "String", - "Description": "S3 bucket for asset \"1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956d\"" + "Description": "S3 bucket for asset \"c1614067648b8b7e151e321ce82879d259a2b8f2bd10dddd61f0f2ce26287c17\"" }, - "AssetParameters1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956dS3VersionKey6209E240": { + "AssetParametersc1614067648b8b7e151e321ce82879d259a2b8f2bd10dddd61f0f2ce26287c17S3VersionKeyC4E1E9B5": { "Type": "String", - "Description": "S3 key for asset version \"1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956d\"" + "Description": "S3 key for asset version \"c1614067648b8b7e151e321ce82879d259a2b8f2bd10dddd61f0f2ce26287c17\"" }, - "AssetParameters1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956dArtifactHash02B929EC": { + "AssetParametersc1614067648b8b7e151e321ce82879d259a2b8f2bd10dddd61f0f2ce26287c17ArtifactHash5B02FA4D": { "Type": "String", - "Description": "Artifact hash for asset \"1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956d\"" + "Description": "Artifact hash for asset \"c1614067648b8b7e151e321ce82879d259a2b8f2bd10dddd61f0f2ce26287c17\"" }, - "AssetParameters96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68S3BucketA23E6312": { + "AssetParameters3610bde00ecd0013f7806e2ab0e80d7ac26232cd3ffc2934b5ca28fef120bdf6S3Bucket91AABE39": { "Type": "String", - "Description": "S3 bucket for asset \"96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68\"" + "Description": "S3 bucket for asset \"3610bde00ecd0013f7806e2ab0e80d7ac26232cd3ffc2934b5ca28fef120bdf6\"" }, - "AssetParameters96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68S3VersionKey1E21AF83": { + "AssetParameters3610bde00ecd0013f7806e2ab0e80d7ac26232cd3ffc2934b5ca28fef120bdf6S3VersionKeyEE0FAD90": { "Type": "String", - "Description": "S3 key for asset version \"96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68\"" + "Description": "S3 key for asset version \"3610bde00ecd0013f7806e2ab0e80d7ac26232cd3ffc2934b5ca28fef120bdf6\"" }, - "AssetParameters96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68ArtifactHash0043D2A0": { + "AssetParameters3610bde00ecd0013f7806e2ab0e80d7ac26232cd3ffc2934b5ca28fef120bdf6ArtifactHash09CEB444": { "Type": "String", - "Description": "Artifact hash for asset \"96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68\"" + "Description": "Artifact hash for asset \"3610bde00ecd0013f7806e2ab0e80d7ac26232cd3ffc2934b5ca28fef120bdf6\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.expected.json index 9a81c901d7451..aa13e73295e64 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.expected.json @@ -5,7 +5,7 @@ "Properties": { "Content": { "S3Bucket": { - "Ref": "AssetParameters6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4aS3BucketCCD07444" + "Ref": "AssetParameterse174a6a88cb48eb510c29b2bf0203c181cfa059320745e6ae6429e522b36c66eS3Bucket7A00FCB4" }, "S3Key": { "Fn::Join": [ @@ -18,7 +18,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4aS3VersionKeyA8B74284" + "Ref": "AssetParameterse174a6a88cb48eb510c29b2bf0203c181cfa059320745e6ae6429e522b36c66eS3VersionKey8BF2F9D6" } ] } @@ -31,7 +31,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4aS3VersionKeyA8B74284" + "Ref": "AssetParameterse174a6a88cb48eb510c29b2bf0203c181cfa059320745e6ae6429e522b36c66eS3VersionKey8BF2F9D6" } ] } @@ -82,7 +82,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218S3Bucket89C9DB12" + "Ref": "AssetParameters907ec4b12820d4b2dc64e7f0f1f1f6267c1db622bc42aa57f5361efa12b1aafbS3Bucket16F02289" }, "S3Key": { "Fn::Join": [ @@ -95,7 +95,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218S3VersionKey435DAD55" + "Ref": "AssetParameters907ec4b12820d4b2dc64e7f0f1f1f6267c1db622bc42aa57f5361efa12b1aafbS3VersionKeyDAF0A5BD" } ] } @@ -108,7 +108,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218S3VersionKey435DAD55" + "Ref": "AssetParameters907ec4b12820d4b2dc64e7f0f1f1f6267c1db622bc42aa57f5361efa12b1aafbS3VersionKeyDAF0A5BD" } ] } @@ -138,29 +138,29 @@ } }, "Parameters": { - "AssetParameters6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4aS3BucketCCD07444": { + "AssetParameterse174a6a88cb48eb510c29b2bf0203c181cfa059320745e6ae6429e522b36c66eS3Bucket7A00FCB4": { "Type": "String", - "Description": "S3 bucket for asset \"6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4a\"" + "Description": "S3 bucket for asset \"e174a6a88cb48eb510c29b2bf0203c181cfa059320745e6ae6429e522b36c66e\"" }, - "AssetParameters6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4aS3VersionKeyA8B74284": { + "AssetParameterse174a6a88cb48eb510c29b2bf0203c181cfa059320745e6ae6429e522b36c66eS3VersionKey8BF2F9D6": { "Type": "String", - "Description": "S3 key for asset version \"6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4a\"" + "Description": "S3 key for asset version \"e174a6a88cb48eb510c29b2bf0203c181cfa059320745e6ae6429e522b36c66e\"" }, - "AssetParameters6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4aArtifactHashB3093591": { + "AssetParameterse174a6a88cb48eb510c29b2bf0203c181cfa059320745e6ae6429e522b36c66eArtifactHash2DECF34E": { "Type": "String", - "Description": "Artifact hash for asset \"6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4a\"" + "Description": "Artifact hash for asset \"e174a6a88cb48eb510c29b2bf0203c181cfa059320745e6ae6429e522b36c66e\"" }, - "AssetParameters71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218S3Bucket89C9DB12": { + "AssetParameters907ec4b12820d4b2dc64e7f0f1f1f6267c1db622bc42aa57f5361efa12b1aafbS3Bucket16F02289": { "Type": "String", - "Description": "S3 bucket for asset \"71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218\"" + "Description": "S3 bucket for asset \"907ec4b12820d4b2dc64e7f0f1f1f6267c1db622bc42aa57f5361efa12b1aafb\"" }, - "AssetParameters71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218S3VersionKey435DAD55": { + "AssetParameters907ec4b12820d4b2dc64e7f0f1f1f6267c1db622bc42aa57f5361efa12b1aafbS3VersionKeyDAF0A5BD": { "Type": "String", - "Description": "S3 key for asset version \"71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218\"" + "Description": "S3 key for asset version \"907ec4b12820d4b2dc64e7f0f1f1f6267c1db622bc42aa57f5361efa12b1aafb\"" }, - "AssetParameters71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218ArtifactHash0EDF3CD0": { + "AssetParameters907ec4b12820d4b2dc64e7f0f1f1f6267c1db622bc42aa57f5361efa12b1aafbArtifactHashD7592B0F": { "Type": "String", - "Description": "Artifact hash for asset \"71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218\"" + "Description": "Artifact hash for asset \"907ec4b12820d4b2dc64e7f0f1f1f6267c1db622bc42aa57f5361efa12b1aafb\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.expected.json index b5b137205752f..c49f5e312d873 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148adS3BucketA9379638" + "Ref": "AssetParameters6cc4994756d4085a860e734568c92826773e52c22c58894ce368b1e698da413dS3Bucket74CBB570" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148adS3VersionKey4376B462" + "Ref": "AssetParameters6cc4994756d4085a860e734568c92826773e52c22c58894ce368b1e698da413dS3VersionKey3CBAF21A" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148adS3VersionKey4376B462" + "Ref": "AssetParameters6cc4994756d4085a860e734568c92826773e52c22c58894ce368b1e698da413dS3VersionKey3CBAF21A" } ] } @@ -87,17 +87,17 @@ } }, "Parameters": { - "AssetParameters1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148adS3BucketA9379638": { + "AssetParameters6cc4994756d4085a860e734568c92826773e52c22c58894ce368b1e698da413dS3Bucket74CBB570": { "Type": "String", - "Description": "S3 bucket for asset \"1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148ad\"" + "Description": "S3 bucket for asset \"6cc4994756d4085a860e734568c92826773e52c22c58894ce368b1e698da413d\"" }, - "AssetParameters1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148adS3VersionKey4376B462": { + "AssetParameters6cc4994756d4085a860e734568c92826773e52c22c58894ce368b1e698da413dS3VersionKey3CBAF21A": { "Type": "String", - "Description": "S3 key for asset version \"1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148ad\"" + "Description": "S3 key for asset version \"6cc4994756d4085a860e734568c92826773e52c22c58894ce368b1e698da413d\"" }, - "AssetParameters1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148adArtifactHashB9B928DC": { + "AssetParameters6cc4994756d4085a860e734568c92826773e52c22c58894ce368b1e698da413dArtifactHash750F3AF8": { "Type": "String", - "Description": "Artifact hash for asset \"1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148ad\"" + "Description": "Artifact hash for asset \"6cc4994756d4085a860e734568c92826773e52c22c58894ce368b1e698da413d\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.expected.json index 6b3b8230c2874..c70f4a0ca8933 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersaf41a5381eff9302e9acdfeb9c3bcf160b56a97091242b2d599ed5a861af966aS3BucketEE202B67" + "Ref": "AssetParameters6fa8e0c54d06a6402126a86fab5da1fa1397bcce628a0fb56f8356a2edf6280dS3Bucket2F189DB9" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersaf41a5381eff9302e9acdfeb9c3bcf160b56a97091242b2d599ed5a861af966aS3VersionKey8097C675" + "Ref": "AssetParameters6fa8e0c54d06a6402126a86fab5da1fa1397bcce628a0fb56f8356a2edf6280dS3VersionKeyDF03C812" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersaf41a5381eff9302e9acdfeb9c3bcf160b56a97091242b2d599ed5a861af966aS3VersionKey8097C675" + "Ref": "AssetParameters6fa8e0c54d06a6402126a86fab5da1fa1397bcce628a0fb56f8356a2edf6280dS3VersionKeyDF03C812" } ] } @@ -87,17 +87,17 @@ } }, "Parameters": { - "AssetParametersaf41a5381eff9302e9acdfeb9c3bcf160b56a97091242b2d599ed5a861af966aS3BucketEE202B67": { + "AssetParameters6fa8e0c54d06a6402126a86fab5da1fa1397bcce628a0fb56f8356a2edf6280dS3Bucket2F189DB9": { "Type": "String", - "Description": "S3 bucket for asset \"af41a5381eff9302e9acdfeb9c3bcf160b56a97091242b2d599ed5a861af966a\"" + "Description": "S3 bucket for asset \"6fa8e0c54d06a6402126a86fab5da1fa1397bcce628a0fb56f8356a2edf6280d\"" }, - "AssetParametersaf41a5381eff9302e9acdfeb9c3bcf160b56a97091242b2d599ed5a861af966aS3VersionKey8097C675": { + "AssetParameters6fa8e0c54d06a6402126a86fab5da1fa1397bcce628a0fb56f8356a2edf6280dS3VersionKeyDF03C812": { "Type": "String", - "Description": "S3 key for asset version \"af41a5381eff9302e9acdfeb9c3bcf160b56a97091242b2d599ed5a861af966a\"" + "Description": "S3 key for asset version \"6fa8e0c54d06a6402126a86fab5da1fa1397bcce628a0fb56f8356a2edf6280d\"" }, - "AssetParametersaf41a5381eff9302e9acdfeb9c3bcf160b56a97091242b2d599ed5a861af966aArtifactHash3E92B1F8": { + "AssetParameters6fa8e0c54d06a6402126a86fab5da1fa1397bcce628a0fb56f8356a2edf6280dArtifactHash1F692755": { "Type": "String", - "Description": "Artifact hash for asset \"af41a5381eff9302e9acdfeb9c3bcf160b56a97091242b2d599ed5a861af966a\"" + "Description": "Artifact hash for asset \"6fa8e0c54d06a6402126a86fab5da1fa1397bcce628a0fb56f8356a2edf6280d\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.vpc.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.vpc.expected.json index 63fad4c61de14..6787327a15e40 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.vpc.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.vpc.expected.json @@ -296,7 +296,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3Bucket414E0E30" + "Ref": "AssetParameters83be407310ab5911f40fa4934091a233f92ce3be1d81c48846f30fa0a9330530S3Bucket82392CBF" }, "S3Key": { "Fn::Join": [ @@ -309,7 +309,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3VersionKey5ABC9098" + "Ref": "AssetParameters83be407310ab5911f40fa4934091a233f92ce3be1d81c48846f30fa0a9330530S3VersionKey292822E7" } ] } @@ -322,7 +322,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3VersionKey5ABC9098" + "Ref": "AssetParameters83be407310ab5911f40fa4934091a233f92ce3be1d81c48846f30fa0a9330530S3VersionKey292822E7" } ] } @@ -368,17 +368,17 @@ } }, "Parameters": { - "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3Bucket414E0E30": { + "AssetParameters83be407310ab5911f40fa4934091a233f92ce3be1d81c48846f30fa0a9330530S3Bucket82392CBF": { "Type": "String", - "Description": "S3 bucket for asset \"4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353\"" + "Description": "S3 bucket for asset \"83be407310ab5911f40fa4934091a233f92ce3be1d81c48846f30fa0a9330530\"" }, - "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3VersionKey5ABC9098": { + "AssetParameters83be407310ab5911f40fa4934091a233f92ce3be1d81c48846f30fa0a9330530S3VersionKey292822E7": { "Type": "String", - "Description": "S3 key for asset version \"4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353\"" + "Description": "S3 key for asset version \"83be407310ab5911f40fa4934091a233f92ce3be1d81c48846f30fa0a9330530\"" }, - "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353ArtifactHashECA6C88C": { + "AssetParameters83be407310ab5911f40fa4934091a233f92ce3be1d81c48846f30fa0a9330530ArtifactHash8818CE02": { "Type": "String", - "Description": "Artifact hash for asset \"4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353\"" + "Description": "Artifact hash for asset \"83be407310ab5911f40fa4934091a233f92ce3be1d81c48846f30fa0a9330530\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/layer.test.ts b/packages/@aws-cdk/aws-lambda-python/test/layer.test.ts index 8ace2ec3f7a18..255adb0dae646 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/layer.test.ts +++ b/packages/@aws-cdk/aws-lambda-python/test/layer.test.ts @@ -2,7 +2,7 @@ import '@aws-cdk/assert/jest'; import * as path from 'path'; import { Runtime } from '@aws-cdk/aws-lambda'; import { Stack } from '@aws-cdk/core'; -import { hasDependencies, bundle } from '../lib/bundling'; +import { stageDependencies, bundle } from '../lib/bundling'; import { PythonLayerVersion } from '../lib/layer'; jest.mock('../lib/bundling', () => { @@ -18,11 +18,11 @@ jest.mock('../lib/bundling', () => { }, bindToResource: () => { return; }, }), - hasDependencies: jest.fn().mockReturnValue(true), + stageDependencies: jest.fn().mockReturnValue(true), }; }); -const hasDependenciesMock = (hasDependencies as jest.Mock); +const hasDependenciesMock = (stageDependencies as jest.Mock); let stack: Stack; beforeEach(() => { diff --git a/packages/@aws-cdk/aws-lambda/README.md b/packages/@aws-cdk/aws-lambda/README.md index b3fb466ec7253..ae21d6ea9216e 100644 --- a/packages/@aws-cdk/aws-lambda/README.md +++ b/packages/@aws-cdk/aws-lambda/README.md @@ -36,6 +36,9 @@ runtime code. * `lambda.Code.fromAsset(path)` - specify a directory or a .zip file in the local filesystem which will be zipped and uploaded to S3 before deployment. See also [bundling asset code](#bundling-asset-code). + * `lambda.Code.fromDockerBuild(path, options)` - use the result of a Docker + build as code. The runtime code is expected to be located at `/asset` in the + image and will be zipped and uploaded to S3 as an asset. The following example shows how to define a Python function and deploy the code from the local directory `my-lambda-handler` to it: @@ -450,7 +453,7 @@ new lambda.Function(this, 'Function', { bundling: { image: lambda.Runtime.PYTHON_3_6.bundlingDockerImage, command: [ - 'bash', '-c', + 'bash', '-c', 'pip install -r requirements.txt -t /asset-output && cp -au . /asset-output' ], }, @@ -462,8 +465,8 @@ new lambda.Function(this, 'Function', { Runtimes expose a `bundlingDockerImage` property that points to the [AWS SAM](https://github.com/awslabs/aws-sam-cli) build image. -Use `cdk.BundlingDockerImage.fromRegistry(image)` to use an existing image or -`cdk.BundlingDockerImage.fromAsset(path)` to build a specific image: +Use `cdk.DockerImage.fromRegistry(image)` to use an existing image or +`cdk.DockerImage.fromBuild(path)` to build a specific image: ```ts import * as cdk from '@aws-cdk/core'; @@ -471,7 +474,7 @@ import * as cdk from '@aws-cdk/core'; new lambda.Function(this, 'Function', { code: lambda.Code.fromAsset('/path/to/handler', { bundling: { - image: cdk.BundlingDockerImage.fromAsset('/path/to/dir/with/DockerFile', { + image: cdk.DockerImage.fromBuild('/path/to/dir/with/DockerFile', { buildArgs: { ARG1: 'value1', }, @@ -489,3 +492,27 @@ Language-specific higher level constructs are provided in separate modules: * `@aws-cdk/aws-lambda-nodejs`: [Github](https://github.com/aws/aws-cdk/tree/master/packages/%40aws-cdk/aws-lambda-nodejs) & [CDK Docs](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-lambda-nodejs-readme.html) * `@aws-cdk/aws-lambda-python`: [Github](https://github.com/aws/aws-cdk/tree/master/packages/%40aws-cdk/aws-lambda-python) & [CDK Docs](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-lambda-python-readme.html) + +## Code Signing + +Code signing for AWS Lambda helps to ensure that only trusted code runs in your Lambda functions. +When enabled, AWS Lambda checks every code deployment and verifies that the code package is signed by a trusted source. +For more information, see [Configuring code signing for AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/configuration-codesigning.html). +The following code configures a function with code signing. + +```typescript +import * as signer from '@aws-cdk/aws-signer'; + +const signerProfile = signer.SigningProfile(this, 'SigningProfile', { + platform: Platform.AWS_LAMBDA_SHA384_ECDSA +}); + +const codeSigningConfig = new lambda.CodeSigningConfig(stack, 'CodeSigningConfig', { + signingProfiles: [signingProfile], +}); + +new lambda.Function(this, 'Function', { + codeSigningConfig, + // ... +}); +``` diff --git a/packages/@aws-cdk/aws-lambda/lib/code-signing-config.ts b/packages/@aws-cdk/aws-lambda/lib/code-signing-config.ts new file mode 100644 index 0000000000000..0472eb5d048f5 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/lib/code-signing-config.ts @@ -0,0 +1,120 @@ +import { ISigningProfile } from '@aws-cdk/aws-signer'; +import { IResource, Resource, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnCodeSigningConfig } from './lambda.generated'; + +/** + * Code signing configuration policy for deployment validation failure. + */ +export enum UntrustedArtifactOnDeployment { + /** + * Lambda blocks the deployment request if signature validation checks fail. + */ + ENFORCE = 'enforce', + + /** + * Lambda allows the deployment of the code package, but issues a warning. + * Lambda issues a new Amazon CloudWatch metric, called a signature validation error and also stores the warning in CloudTrail. + */ + WARN = 'warn', +} + +/** + * A Code Signing Config + */ +export interface ICodeSigningConfig extends IResource { + /** + * The ARN of Code Signing Config + * @attribute + */ + readonly codeSigningConfigArn: string; + + /** + * The id of Code Signing Config + * @attribute + */ + readonly codeSigningConfigId: string; +} + +/** + * Construction properties for a Code Signing Config object + */ +export interface CodeSigningConfigProps { + /** + * List of signing profiles that defines a + * trusted user who can sign a code package. + */ + readonly signingProfiles: ISigningProfile[], + + /** + * Code signing configuration policy for deployment validation failure. + * If you set the policy to Enforce, Lambda blocks the deployment request + * if signature validation checks fail. + * If you set the policy to Warn, Lambda allows the deployment and + * creates a CloudWatch log. + * + * @default UntrustedArtifactOnDeployment.WARN + */ + readonly untrustedArtifactOnDeployment?: UntrustedArtifactOnDeployment, + + /** + * Code signing configuration description. + * + * @default - No description. + */ + readonly description?: string, +} + +/** + * Defines a Code Signing Config. + * + * @resource AWS::Lambda::CodeSigningConfig + */ +export class CodeSigningConfig extends Resource implements ICodeSigningConfig { + /** + * Creates a Signing Profile construct that represents an external Signing Profile. + * + * @param scope The parent creating construct (usually `this`). + * @param id The construct's name. + * @param codeSigningConfigArn The ARN of code signing config. + */ + public static fromCodeSigningConfigArn( scope: Construct, id: string, codeSigningConfigArn: string): ICodeSigningConfig { + const codeSigningProfileId = Stack.of(scope).parseArn(codeSigningConfigArn).resourceName; + if (!codeSigningProfileId) { + throw new Error(`Code signing config ARN must be in the format 'arn:aws:lambda:::code-signing-config:', got: '${codeSigningConfigArn}'`); + } + const assertedCodeSigningProfileId = codeSigningProfileId; + class Import extends Resource implements ICodeSigningConfig { + public readonly codeSigningConfigArn = codeSigningConfigArn; + public readonly codeSigningConfigId = assertedCodeSigningProfileId; + + constructor() { + super(scope, id); + } + } + return new Import(); + } + + public readonly codeSigningConfigArn: string; + public readonly codeSigningConfigId: string; + + constructor(scope: Construct, id: string, props: CodeSigningConfigProps) { + super(scope, id); + + const signingProfileVersionArns = props.signingProfiles.map(signingProfile => { + return signingProfile.signingProfileVersionArn; + }); + + const resource: CfnCodeSigningConfig = new CfnCodeSigningConfig(this, 'Resource', { + allowedPublishers: { + signingProfileVersionArns, + }, + codeSigningPolicies: { + untrustedArtifactOnDeployment: props.untrustedArtifactOnDeployment ?? UntrustedArtifactOnDeployment.WARN, + }, + description: props.description, + }); + this.codeSigningConfigArn = resource.attrCodeSigningConfigArn; + this.codeSigningConfigId = resource.attrCodeSigningConfigId; + } +} diff --git a/packages/@aws-cdk/aws-lambda/lib/code.ts b/packages/@aws-cdk/aws-lambda/lib/code.ts index 29cd3d02ae4de..b4f41b2804257 100644 --- a/packages/@aws-cdk/aws-lambda/lib/code.ts +++ b/packages/@aws-cdk/aws-lambda/lib/code.ts @@ -57,6 +57,22 @@ export abstract class Code { return new AssetCode(path, options); } + /** + * Loads the function code from an asset created by a Docker build. + * + * By defaut, the asset is expected to be located at `/asset` in the + * image. + * + * @param path The path to the directory containing the Docker file + * @param options Docker build options + */ + public static fromDockerBuild(path: string, options: DockerBuildAssetOptions = {}): AssetCode { + const assetPath = cdk.DockerImage + .fromBuild(path, options) + .cp(options.imagePath ?? '/asset', options.outputPath); + return new AssetCode(assetPath); + } + /** * DEPRECATED * @deprecated use `fromAsset` @@ -488,3 +504,24 @@ export class AssetImageCode extends Code { }; } } + +/** + * Options when creating an asset from a Docker build. + */ +export interface DockerBuildAssetOptions extends cdk.DockerBuildOptions { + /** + * The path in the Docker image where the asset is located after the build + * operation. + * + * @default /asset + */ + readonly imagePath?: string; + + /** + * The path on the local filesystem where the asset will be copied + * using `docker cp`. + * + * @default - a unique temporary directory in the system temp directory + */ + readonly outputPath?: string; +} diff --git a/packages/@aws-cdk/aws-lambda/lib/function.ts b/packages/@aws-cdk/aws-lambda/lib/function.ts index fdcf4b4e0ec24..8d487276a6176 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function.ts @@ -8,6 +8,7 @@ import * as sqs from '@aws-cdk/aws-sqs'; import { Annotations, CfnResource, Duration, Fn, Lazy, Names, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { Code, CodeConfig } from './code'; +import { ICodeSigningConfig } from './code-signing-config'; import { EventInvokeConfigOptions } from './event-invoke-config'; import { IEventSource } from './event-source'; import { FileSystem } from './filesystem'; @@ -290,6 +291,13 @@ export interface FunctionOptions extends EventInvokeConfigOptions { * @default - AWS Lambda creates and uses an AWS managed customer master key (CMK). */ readonly environmentEncryption?: kms.IKey; + + /** + * Code signing config associated with this function + * + * @default - Not Sign the Code + */ + readonly codeSigningConfig?: ICodeSigningConfig; } export interface FunctionProps extends FunctionOptions { @@ -641,6 +649,7 @@ export class Function extends FunctionBase { }), kmsKeyArn: props.environmentEncryption?.keyArn, fileSystemConfigs, + codeSigningConfigArn: props.codeSigningConfig?.codeSigningConfigArn, }); resource.node.addDependency(this.role); diff --git a/packages/@aws-cdk/aws-lambda/lib/index.ts b/packages/@aws-cdk/aws-lambda/lib/index.ts index 1ba17427c5210..2d936755d6ad1 100644 --- a/packages/@aws-cdk/aws-lambda/lib/index.ts +++ b/packages/@aws-cdk/aws-lambda/lib/index.ts @@ -16,6 +16,7 @@ export * from './event-source-mapping'; export * from './destination'; export * from './event-invoke-config'; export * from './scalable-attribute-api'; +export * from './code-signing-config'; export * from './log-retention'; diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index de51f290aa4f5..6eab5cd11b870 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -99,6 +99,7 @@ "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", + "@aws-cdk/aws-signer": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/cx-api": "0.0.0", @@ -119,6 +120,7 @@ "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", + "@aws-cdk/aws-signer": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/cx-api": "0.0.0", @@ -169,7 +171,8 @@ "props-default-doc:@aws-cdk/aws-lambda.Permission.sourceArn", "docs-public-apis:@aws-cdk/aws-lambda.ResourceBindOptions", "docs-public-apis:@aws-cdk/aws-lambda.VersionAttributes", - "props-physical-name:@aws-cdk/aws-lambda.EventInvokeConfigProps" + "props-physical-name:@aws-cdk/aws-lambda.EventInvokeConfigProps", + "props-physical-name:@aws-cdk/aws-lambda.CodeSigningConfigProps" ] }, "stability": "stable", diff --git a/packages/@aws-cdk/aws-lambda/test/code-signing-config.test.ts b/packages/@aws-cdk/aws-lambda/test/code-signing-config.test.ts new file mode 100644 index 0000000000000..3e123ab5d5d89 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/code-signing-config.test.ts @@ -0,0 +1,102 @@ +import '@aws-cdk/assert/jest'; +import * as signer from '@aws-cdk/aws-signer'; +import * as cdk from '@aws-cdk/core'; +import * as lambda from '../lib'; + +let app: cdk.App; +let stack: cdk.Stack; +beforeEach( () => { + app = new cdk.App( {} ); + stack = new cdk.Stack( app ); +} ); + +describe('code signing config', () => { + test('default', () => { + const platform = signer.Platform.AWS_LAMBDA_SHA384_ECDSA; + const signingProfile = new signer.SigningProfile(stack, 'SigningProfile', { platform }); + new lambda.CodeSigningConfig(stack, 'CodeSigningConfig', { + signingProfiles: [signingProfile], + }); + + expect(stack).toHaveResource('AWS::Lambda::CodeSigningConfig', { + AllowedPublishers: { + SigningProfileVersionArns: [{ + 'Fn::GetAtt': [ + 'SigningProfile2139A0F9', + 'ProfileVersionArn', + ], + }], + }, + CodeSigningPolicies: { + UntrustedArtifactOnDeployment: lambda.UntrustedArtifactOnDeployment.WARN, + }, + }); + }); + + test('with multiple signing profiles', () => { + const signingProfile1 = new signer.SigningProfile(stack, 'SigningProfile1', { platform: signer.Platform.AWS_LAMBDA_SHA384_ECDSA }); + const signingProfile2 = new signer.SigningProfile(stack, 'SigningProfile2', { platform: signer.Platform.AMAZON_FREE_RTOS_DEFAULT }); + const signingProfile3 = new signer.SigningProfile(stack, 'SigningProfile3', { platform: signer.Platform.AWS_IOT_DEVICE_MANAGEMENT_SHA256_ECDSA }); + new lambda.CodeSigningConfig(stack, 'CodeSigningConfig', { + signingProfiles: [signingProfile1, signingProfile2, signingProfile3], + }); + + expect(stack).toHaveResource('AWS::Lambda::CodeSigningConfig', { + AllowedPublishers: { + SigningProfileVersionArns: [ + { + 'Fn::GetAtt': [ + 'SigningProfile1D4191686', + 'ProfileVersionArn', + ], + }, + { + 'Fn::GetAtt': [ + 'SigningProfile2E013C934', + 'ProfileVersionArn', + ], + }, + { + 'Fn::GetAtt': [ + 'SigningProfile3A38DE231', + 'ProfileVersionArn', + ], + }, + ], + }, + }); + }); + + test('with description and with untrustedArtifactOnDeployment of "ENFORCE"', () => { + const platform = signer.Platform.AWS_LAMBDA_SHA384_ECDSA; + const signingProfile = new signer.SigningProfile(stack, 'SigningProfile', { platform }); + new lambda.CodeSigningConfig(stack, 'CodeSigningConfig', { + signingProfiles: [signingProfile], + untrustedArtifactOnDeployment: lambda.UntrustedArtifactOnDeployment.ENFORCE, + description: 'test description', + }); + + expect(stack).toHaveResource('AWS::Lambda::CodeSigningConfig', { + CodeSigningPolicies: { + UntrustedArtifactOnDeployment: lambda.UntrustedArtifactOnDeployment.ENFORCE, + }, + Description: 'test description', + }); + }); + + test('import does not create any resources', () => { + const codeSigningConfigId = 'aaa-xxxxxxxxxx'; + const codeSigningConfigArn = `arn:aws:lambda:::code-signing-config:${codeSigningConfigId}`; + const codeSigningConfig = lambda.CodeSigningConfig.fromCodeSigningConfigArn(stack, 'Imported', codeSigningConfigArn ); + + expect(codeSigningConfig.codeSigningConfigArn).toBe(codeSigningConfigArn); + expect(codeSigningConfig.codeSigningConfigId).toBe(codeSigningConfigId); + expect(stack).toCountResources('AWS::Lambda::CodeSigningConfig', 0); + }); + + test('fail import with malformed code signing config arn', () => { + const codeSigningConfigArn = 'arn:aws:lambda:::code-signing-config'; + + expect(() => lambda.CodeSigningConfig.fromCodeSigningConfigArn(stack, 'Imported', codeSigningConfigArn ) ).toThrow(/ARN must be in the format/); + }); +}); diff --git a/packages/@aws-cdk/aws-lambda/test/code.test.ts b/packages/@aws-cdk/aws-lambda/test/code.test.ts index 9b99c095c2467..91de07a17c5a6 100644 --- a/packages/@aws-cdk/aws-lambda/test/code.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/code.test.ts @@ -4,6 +4,7 @@ import { ABSENT, ResourcePart } from '@aws-cdk/assert'; import * as ecr from '@aws-cdk/aws-ecr'; import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; +import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as lambda from '../lib'; /* eslint-disable dot-notation */ @@ -275,9 +276,10 @@ describe('code', () => { }); describe('lambda.Code.fromImageAsset', () => { - test('repository uri is correctly identified', () => { + const flags = { [cxapi.DOCKER_IGNORE_SUPPORT]: true }; + testFutureBehavior('repository uri is correctly identified', flags, cdk.App, (app) => { // given - const stack = new cdk.Stack(); + const stack = new cdk.Stack(app); // when new lambda.Function(stack, 'Fn', { @@ -296,7 +298,7 @@ describe('code', () => { { Ref: 'AWS::Region' }, '.', { Ref: 'AWS::URLSuffix' }, - '/aws-cdk/assets:0874c7dfd254e95f5181cc7fa643e4abf010f68e5717e373b6e635b49a115b2b', + '/aws-cdk/assets:e8a944aeb0a08ba4811503d9c138e514b112dadca84daa5b4608e4a0fb80a0c9', ]], }, }, @@ -327,6 +329,29 @@ describe('code', () => { }); }); }); + + describe('lambda.Code.fromDockerBuild', () => { + test('can use the result of a Docker build as an asset', () => { + // given + const stack = new cdk.Stack(); + stack.node.setContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT, true); + + // when + new lambda.Function(stack, 'Fn', { + code: lambda.Code.fromDockerBuild(path.join(__dirname, 'docker-build-lambda')), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_12_X, + }); + + // then + expect(stack).toHaveResource('AWS::Lambda::Function', { + Metadata: { + [cxapi.ASSET_RESOURCE_METADATA_PATH_KEY]: 'asset.38cd320fa97b348accac88e48d9cede4923f7cab270ce794c95a665be83681a8', + [cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY]: 'Code', + }, + }, ResourcePart.CompleteDefinition); + }); + }); }); function defineFunction(code: lambda.Code, runtime: lambda.Runtime = lambda.Runtime.NODEJS_10_X) { diff --git a/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/Dockerfile b/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/Dockerfile new file mode 100644 index 0000000000000..4643fde141850 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/Dockerfile @@ -0,0 +1,3 @@ +FROM public.ecr.aws/amazonlinux/amazonlinux:latest + +COPY index.js /asset diff --git a/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/index.ts b/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/index.ts new file mode 100644 index 0000000000000..cc867895b4efc --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/index.ts @@ -0,0 +1,5 @@ +/* eslint-disable no-console */ +export async function handler(event: any) { + console.log('Event: %j', event); + return event; +} diff --git a/packages/@aws-cdk/aws-lambda/test/function.test.ts b/packages/@aws-cdk/aws-lambda/test/function.test.ts index 51cfe70fd878c..50cf6b0c9b72b 100644 --- a/packages/@aws-cdk/aws-lambda/test/function.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/function.test.ts @@ -9,6 +9,7 @@ import * as kms from '@aws-cdk/aws-kms'; import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; import * as sqs from '@aws-cdk/aws-sqs'; +import * as signer from '@aws-cdk/aws-signer'; import * as cdk from '@aws-cdk/core'; import * as constructs from 'constructs'; import * as _ from 'lodash'; @@ -2003,6 +2004,36 @@ describe('function', () => { }); }); }); + + describe('code signing config', () => { + test('default', () => { + const stack = new cdk.Stack(); + + const signingProfile = new signer.SigningProfile(stack, 'SigningProfile', { + platform: signer.Platform.AWS_LAMBDA_SHA384_ECDSA, + }); + + const codeSigningConfig = new lambda.CodeSigningConfig(stack, 'CodeSigningConfig', { + signingProfiles: [signingProfile], + }); + + new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_10_X, + codeSigningConfig, + }); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + CodeSigningConfigArn: { + 'Fn::GetAtt': [ + 'CodeSigningConfigD8D41C10', + 'CodeSigningConfigArn', + ], + }, + }); + }); + }); }); function newTestLambda(scope: constructs.Construct) { diff --git a/packages/@aws-cdk/aws-logs/test/test.log-retention-provider.ts b/packages/@aws-cdk/aws-logs/test/test.log-retention-provider.ts index b13eebe9aabe2..a08ff060dc2a4 100644 --- a/packages/@aws-cdk/aws-logs/test/test.log-retention-provider.ts +++ b/packages/@aws-cdk/aws-logs/test/test.log-retention-provider.ts @@ -208,7 +208,7 @@ export = { }, async 'responds with FAILED on error'(test: Test) { - const createLogGroupFake = sinon.fake.rejects(new Error('UnkownError')); + const createLogGroupFake = sinon.fake.rejects(new Error('UnknownError')); AWS.mock('CloudWatchLogs', 'createLogGroup', createLogGroupFake); diff --git a/packages/@aws-cdk/aws-rds/package.json b/packages/@aws-cdk/aws-rds/package.json index 01ce0a8ca4a12..685b5c399b240 100644 --- a/packages/@aws-cdk/aws-rds/package.json +++ b/packages/@aws-cdk/aws-rds/package.json @@ -74,6 +74,7 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-events-targets": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-rds/test/cluster.test.ts b/packages/@aws-cdk/aws-rds/test/cluster.test.ts index 11f18100e9fbe..11e60d8068341 100644 --- a/packages/@aws-cdk/aws-rds/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-rds/test/cluster.test.ts @@ -6,6 +6,7 @@ import * as kms from '@aws-cdk/aws-kms'; import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; import { AuroraEngineVersion, AuroraMysqlEngineVersion, AuroraPostgresEngineVersion, CfnDBCluster, Credentials, DatabaseCluster, @@ -1073,7 +1074,7 @@ describe('cluster', () => { }); - testFutureBehavior('create a cluster with s3 export buckets', { '@aws-cdk/aws-s3:grantWriteWithoutAcl': true }, cdk.App, (app) => { + testFutureBehavior('create a cluster with s3 export buckets', { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true }, cdk.App, (app) => { // GIVEN const stack = testStack(app); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -1837,6 +1838,40 @@ describe('cluster', () => { }); }); +test.each([ + [cdk.RemovalPolicy.RETAIN, 'Retain', 'Retain', 'Retain'], + [cdk.RemovalPolicy.SNAPSHOT, 'Snapshot', 'Delete', ABSENT], + [cdk.RemovalPolicy.DESTROY, 'Delete', 'Delete', ABSENT], +])('if Cluster RemovalPolicy is \'%s\', the DBCluster has DeletionPolicy \'%s\', the DBInstance has \'%s\' and the DBSubnetGroup has \'%s\'', (clusterRemovalPolicy, clusterValue, instanceValue, subnetValue) => { + const stack = new cdk.Stack(); + + // WHEN + new DatabaseCluster(stack, 'Cluster', { + credentials: { username: 'admin' }, + engine: DatabaseClusterEngine.AURORA, + instanceProps: { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.M5, ec2.InstanceSize.LARGE), + vpc: new ec2.Vpc(stack, 'Vpc'), + }, + removalPolicy: clusterRemovalPolicy, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + DeletionPolicy: clusterValue, + UpdateReplacePolicy: clusterValue, + }, ResourcePart.CompleteDefinition); + + expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + DeletionPolicy: instanceValue, + UpdateReplacePolicy: instanceValue, + }, ResourcePart.CompleteDefinition); + + expect(stack).toHaveResourceLike('AWS::RDS::DBSubnetGroup', { + DeletionPolicy: subnetValue, + }, ResourcePart.CompleteDefinition); +}); + test.each([ [cdk.RemovalPolicy.RETAIN, 'Retain', 'Retain', 'Retain'], [cdk.RemovalPolicy.SNAPSHOT, 'Snapshot', 'Delete', ABSENT], @@ -1872,6 +1907,7 @@ test.each([ }, ResourcePart.CompleteDefinition); }); + function testStack(app?: cdk.App) { const stack = new cdk.Stack(app, undefined, { env: { account: '12345', region: 'us-test-1' } }); stack.node.setContext('availability-zones:12345:us-test-1', ['us-test-1a', 'us-test-1b']); diff --git a/packages/@aws-cdk/aws-rds/test/instance.test.ts b/packages/@aws-cdk/aws-rds/test/instance.test.ts index 67fc9fd3905ff..497bfbea36c32 100644 --- a/packages/@aws-cdk/aws-rds/test/instance.test.ts +++ b/packages/@aws-cdk/aws-rds/test/instance.test.ts @@ -8,6 +8,7 @@ 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 cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as rds from '../lib'; @@ -1038,7 +1039,7 @@ describe('instance', () => { }); describe('S3 Import/Export', () => { - testFutureBehavior('instance with s3 import and export buckets', { '@aws-cdk/aws-s3:grantWriteWithoutAcl': true }, cdk.App, (app) => { + testFutureBehavior('instance with s3 import and export buckets', { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true }, cdk.App, (app) => { stack = new cdk.Stack(app); vpc = new ec2.Vpc(stack, 'VPC'); new rds.DatabaseInstance(stack, 'DB', { @@ -1241,8 +1242,6 @@ describe('instance', () => { expect(stack).toHaveResource('AWS::RDS::DBInstance', { PubliclyAccessible: true, }); - - }); }); @@ -1275,4 +1274,4 @@ test.each([ DeletionPolicy: subnetValue, UpdateReplacePolicy: subnetValue, }, ResourcePart.CompleteDefinition); -}); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json index 8a05969830f29..0de11e4fd5c4f 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json @@ -909,7 +909,7 @@ ] } }, - "InstanceAvailabilityAllowEventRuleawscdkrdsinstanceInstanceAvailabilityCE39A6A7B066AA0D": { + "InstanceAvailabilityAllowEventRuleawscdkrdsinstanceFunctionD515EE1969208105": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", diff --git a/packages/@aws-cdk/aws-route53/test/integ.vpc-endpoint-service-domain-name.expected.json b/packages/@aws-cdk/aws-route53/test/integ.vpc-endpoint-service-domain-name.expected.json index df7ab45491d39..5c4f8ee9629df 100644 --- a/packages/@aws-cdk/aws-route53/test/integ.vpc-endpoint-service-domain-name.expected.json +++ b/packages/@aws-cdk/aws-route53/test/integ.vpc-endpoint-service-domain-name.expected.json @@ -606,40 +606,40 @@ ] }, "Create": { - "service": "EC2", - "action": "modifyVpcEndpointServiceConfiguration", - "parameters": { - "ServiceId": { - "Ref": "VPCES3AE7D565" - }, - "PrivateDnsName": "my-stuff.aws-cdk.dev" - }, - "physicalResourceId": { - "id": "awscdkvpcendpointdnsintegVPCES2D7BC258" - } + "Fn::Join": [ + "", + [ + "{\"service\":\"EC2\",\"action\":\"modifyVpcEndpointServiceConfiguration\",\"parameters\":{\"ServiceId\":\"", + { + "Ref": "VPCES3AE7D565" + }, + "\",\"PrivateDnsName\":\"my-stuff.aws-cdk.dev\"},\"physicalResourceId\":{\"id\":\"awscdkvpcendpointdnsintegVPCES2D7BC258\"}}" + ] + ] }, "Update": { - "service": "EC2", - "action": "modifyVpcEndpointServiceConfiguration", - "parameters": { - "ServiceId": { - "Ref": "VPCES3AE7D565" - }, - "PrivateDnsName": "my-stuff.aws-cdk.dev" - }, - "physicalResourceId": { - "id": "awscdkvpcendpointdnsintegVPCES2D7BC258" - } + "Fn::Join": [ + "", + [ + "{\"service\":\"EC2\",\"action\":\"modifyVpcEndpointServiceConfiguration\",\"parameters\":{\"ServiceId\":\"", + { + "Ref": "VPCES3AE7D565" + }, + "\",\"PrivateDnsName\":\"my-stuff.aws-cdk.dev\"},\"physicalResourceId\":{\"id\":\"awscdkvpcendpointdnsintegVPCES2D7BC258\"}}" + ] + ] }, "Delete": { - "service": "EC2", - "action": "modifyVpcEndpointServiceConfiguration", - "parameters": { - "ServiceId": { - "Ref": "VPCES3AE7D565" - }, - "RemovePrivateDnsName": "TRUE:BOOLEAN" - } + "Fn::Join": [ + "", + [ + "{\"service\":\"EC2\",\"action\":\"modifyVpcEndpointServiceConfiguration\",\"parameters\":{\"ServiceId\":\"", + { + "Ref": "VPCES3AE7D565" + }, + "\",\"RemovePrivateDnsName\":true}}" + ] + ] }, "InstallLatestAwsSdk": true }, @@ -686,32 +686,28 @@ ] }, "Create": { - "service": "EC2", - "action": "describeVpcEndpointServiceConfigurations", - "parameters": { - "ServiceIds": [ + "Fn::Join": [ + "", + [ + "{\"service\":\"EC2\",\"action\":\"describeVpcEndpointServiceConfigurations\",\"parameters\":{\"ServiceIds\":[\"", { "Ref": "VPCES3AE7D565" - } + }, + "\"]},\"physicalResourceId\":{\"id\":\"0b26ca4969ad06c279e229b1b55b9bc2\"}}" ] - }, - "physicalResourceId": { - "id": "0b26ca4969ad06c279e229b1b55b9bc2" - } + ] }, "Update": { - "service": "EC2", - "action": "describeVpcEndpointServiceConfigurations", - "parameters": { - "ServiceIds": [ + "Fn::Join": [ + "", + [ + "{\"service\":\"EC2\",\"action\":\"describeVpcEndpointServiceConfigurations\",\"parameters\":{\"ServiceIds\":[\"", { "Ref": "VPCES3AE7D565" - } + }, + "\"]},\"physicalResourceId\":{\"id\":\"0b26ca4969ad06c279e229b1b55b9bc2\"}}" ] - }, - "physicalResourceId": { - "id": "0b26ca4969ad06c279e229b1b55b9bc2" - } + ] }, "InstallLatestAwsSdk": true }, @@ -831,64 +827,68 @@ ] }, "Create": { - "service": "EC2", - "action": "startVpcEndpointServicePrivateDnsVerification", - "parameters": { - "ServiceId": { - "Ref": "VPCES3AE7D565" - } - }, - "physicalResourceId": { - "id": { - "Fn::Join": [ - ":", - [ - { - "Fn::GetAtt": [ - "EndpointDomainGetNames9E697ED2", - "ServiceConfigurations.0.PrivateDnsNameConfiguration.Name" - ] - }, - { - "Fn::GetAtt": [ - "EndpointDomainGetNames9E697ED2", - "ServiceConfigurations.0.PrivateDnsNameConfiguration.Value" - ] - } + "Fn::Join": [ + "", + [ + "{\"service\":\"EC2\",\"action\":\"startVpcEndpointServicePrivateDnsVerification\",\"parameters\":{\"ServiceId\":\"", + { + "Ref": "VPCES3AE7D565" + }, + "\"},\"physicalResourceId\":{\"id\":\"", + { + "Fn::Join": [ + ":", + [ + { + "Fn::GetAtt": [ + "EndpointDomainGetNames9E697ED2", + "ServiceConfigurations.0.PrivateDnsNameConfiguration.Name" + ] + }, + { + "Fn::GetAtt": [ + "EndpointDomainGetNames9E697ED2", + "ServiceConfigurations.0.PrivateDnsNameConfiguration.Value" + ] + } + ] ] - ] - } - } + }, + "\"}}" + ] + ] }, "Update": { - "service": "EC2", - "action": "startVpcEndpointServicePrivateDnsVerification", - "parameters": { - "ServiceId": { - "Ref": "VPCES3AE7D565" - } - }, - "physicalResourceId": { - "id": { - "Fn::Join": [ - ":", - [ - { - "Fn::GetAtt": [ - "EndpointDomainGetNames9E697ED2", - "ServiceConfigurations.0.PrivateDnsNameConfiguration.Name" - ] - }, - { - "Fn::GetAtt": [ - "EndpointDomainGetNames9E697ED2", - "ServiceConfigurations.0.PrivateDnsNameConfiguration.Value" - ] - } + "Fn::Join": [ + "", + [ + "{\"service\":\"EC2\",\"action\":\"startVpcEndpointServicePrivateDnsVerification\",\"parameters\":{\"ServiceId\":\"", + { + "Ref": "VPCES3AE7D565" + }, + "\"},\"physicalResourceId\":{\"id\":\"", + { + "Fn::Join": [ + ":", + [ + { + "Fn::GetAtt": [ + "EndpointDomainGetNames9E697ED2", + "ServiceConfigurations.0.PrivateDnsNameConfiguration.Name" + ] + }, + { + "Fn::GetAtt": [ + "EndpointDomainGetNames9E697ED2", + "ServiceConfigurations.0.PrivateDnsNameConfiguration.Value" + ] + } + ] ] - ] - } - } + }, + "\"}}" + ] + ] }, "InstallLatestAwsSdk": true }, @@ -936,7 +936,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904" }, "S3Key": { "Fn::Join": [ @@ -949,7 +949,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -962,7 +962,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -972,13 +972,13 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs12.x", "Timeout": 120 }, @@ -988,17 +988,17 @@ } }, "Parameters": { - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904": { "Type": "String", - "Description": "S3 bucket for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 bucket for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF": { "Type": "String", - "Description": "S3 key for asset version \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 key for asset version \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94ArtifactHash782948FC": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343ArtifactHash0C561FF5": { "Type": "String", - "Description": "Artifact hash for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "Artifact hash for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts b/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts index 86edd9992776d..c6aa0cdbc50c6 100644 --- a/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts +++ b/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts @@ -48,40 +48,40 @@ test('create domain name resource', () => { cdkExpect(stack).to(haveResourceLike('Custom::AWS', { Properties: { Create: { - action: 'modifyVpcEndpointServiceConfiguration', - service: 'EC2', - parameters: { - PrivateDnsName: 'my-stuff.aws-cdk.dev', - ServiceId: { - Ref: 'VPCES3AE7D565', - }, - }, - physicalResourceId: { - id: 'VPCES', - }, + 'Fn::Join': [ + '', + [ + '{"service":"EC2","action":"modifyVpcEndpointServiceConfiguration","parameters":{"ServiceId":"', + { + Ref: 'VPCES3AE7D565', + }, + '","PrivateDnsName":"my-stuff.aws-cdk.dev"},"physicalResourceId":{"id":"VPCES"}}', + ], + ], }, Update: { - action: 'modifyVpcEndpointServiceConfiguration', - service: 'EC2', - parameters: { - PrivateDnsName: 'my-stuff.aws-cdk.dev', - ServiceId: { - Ref: 'VPCES3AE7D565', - }, - }, - physicalResourceId: { - id: 'VPCES', - }, + 'Fn::Join': [ + '', + [ + '{"service":"EC2","action":"modifyVpcEndpointServiceConfiguration","parameters":{"ServiceId":"', + { + Ref: 'VPCES3AE7D565', + }, + '","PrivateDnsName":"my-stuff.aws-cdk.dev"},"physicalResourceId":{"id":"VPCES"}}', + ], + ], }, Delete: { - action: 'modifyVpcEndpointServiceConfiguration', - service: 'EC2', - parameters: { - RemovePrivateDnsName: 'TRUE:BOOLEAN', - ServiceId: { - Ref: 'VPCES3AE7D565', - }, - }, + 'Fn::Join': [ + '', + [ + '{"service":"EC2","action":"modifyVpcEndpointServiceConfiguration","parameters":{"ServiceId":"', + { + Ref: 'VPCES3AE7D565', + }, + '","RemovePrivateDnsName":true}}', + ], + ], }, }, DependsOn: [ @@ -94,22 +94,28 @@ test('create domain name resource', () => { cdkExpect(stack).to(haveResourceLike('Custom::AWS', { Properties: { Create: { - action: 'describeVpcEndpointServiceConfigurations', - service: 'EC2', - parameters: { - ServiceIds: [{ - Ref: 'VPCES3AE7D565', - }], - }, + 'Fn::Join': [ + '', + [ + '{"service":"EC2","action":"describeVpcEndpointServiceConfigurations","parameters":{"ServiceIds":["', + { + Ref: 'VPCES3AE7D565', + }, + '"]},"physicalResourceId":{"id":"fcd2563479244a851a9a59af60831b01"}}', + ], + ], }, Update: { - action: 'describeVpcEndpointServiceConfigurations', - service: 'EC2', - parameters: { - ServiceIds: [{ - Ref: 'VPCES3AE7D565', - }], - }, + 'Fn::Join': [ + '', + [ + '{"service":"EC2","action":"describeVpcEndpointServiceConfigurations","parameters":{"ServiceIds":["', + { + Ref: 'VPCES3AE7D565', + }, + '"]},"physicalResourceId":{"id":"fcd2563479244a851a9a59af60831b01"}}', + ], + ], }, }, DependsOn: [ @@ -167,64 +173,68 @@ test('create domain name resource', () => { cdkExpect(stack).to(haveResourceLike('Custom::AWS', { Properties: { Create: { - action: 'startVpcEndpointServicePrivateDnsVerification', - service: 'EC2', - parameters: { - ServiceId: { - Ref: 'VPCES3AE7D565', - }, - }, - physicalResourceId: { - id: { - 'Fn::Join': [ - ':', - [ - { - 'Fn::GetAtt': [ - 'EndpointDomainGetNames9E697ED2', - 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Name', - ], - }, - { - 'Fn::GetAtt': [ - 'EndpointDomainGetNames9E697ED2', - 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Value', - ], - }, + 'Fn::Join': [ + '', + [ + '{"service":"EC2","action":"startVpcEndpointServicePrivateDnsVerification","parameters":{"ServiceId":"', + { + Ref: 'VPCES3AE7D565', + }, + '"},"physicalResourceId":{"id":"', + { + 'Fn::Join': [ + ':', + [ + { + 'Fn::GetAtt': [ + 'EndpointDomainGetNames9E697ED2', + 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Name', + ], + }, + { + 'Fn::GetAtt': [ + 'EndpointDomainGetNames9E697ED2', + 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Value', + ], + }, + ], ], - ], - }, - }, + }, + '"}}', + ], + ], }, Update: { - action: 'startVpcEndpointServicePrivateDnsVerification', - service: 'EC2', - parameters: { - ServiceId: { - Ref: 'VPCES3AE7D565', - }, - }, - physicalResourceId: { - id: { - 'Fn::Join': [ - ':', - [ - { - 'Fn::GetAtt': [ - 'EndpointDomainGetNames9E697ED2', - 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Name', - ], - }, - { - 'Fn::GetAtt': [ - 'EndpointDomainGetNames9E697ED2', - 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Value', - ], - }, + 'Fn::Join': [ + '', + [ + '{"service":"EC2","action":"startVpcEndpointServicePrivateDnsVerification","parameters":{"ServiceId":"', + { + Ref: 'VPCES3AE7D565', + }, + '"},"physicalResourceId":{"id":"', + { + 'Fn::Join': [ + ':', + [ + { + 'Fn::GetAtt': [ + 'EndpointDomainGetNames9E697ED2', + 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Name', + ], + }, + { + 'Fn::GetAtt': [ + 'EndpointDomainGetNames9E697ED2', + 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Value', + ], + }, + ], ], - ], - }, - }, + }, + '"}}', + ], + ], }, }, DependsOn: [ diff --git a/packages/@aws-cdk/aws-s3-assets/README.md b/packages/@aws-cdk/aws-s3-assets/README.md index aab4c46d9c44d..7a751410a2b22 100644 --- a/packages/@aws-cdk/aws-s3-assets/README.md +++ b/packages/@aws-cdk/aws-s3-assets/README.md @@ -88,8 +88,8 @@ The following example uses custom asset bundling to convert a markdown file to h [Example of using asset bundling](./test/integ.assets.bundling.lit.ts). -The bundling docker image (`image`) can either come from a registry (`BundlingDockerImage.fromRegistry`) -or it can be built from a `Dockerfile` located inside your project (`BundlingDockerImage.fromAsset`). +The bundling docker image (`image`) can either come from a registry (`DockerImage.fromRegistry`) +or it can be built from a `Dockerfile` located inside your project (`DockerImage.fromBuild`). You can set the `CDK_DOCKER` environment variable in order to provide a custom docker program to execute. This may sometime be needed when building in @@ -114,7 +114,7 @@ new assets.Asset(this, 'BundledAsset', { }, }, // Docker bundling fallback - image: BundlingDockerImage.fromRegistry('alpine'), + image: DockerImage.fromRegistry('alpine'), entrypoint: ['/bin/sh', '-c'], command: ['bundle'], }, @@ -124,6 +124,27 @@ new assets.Asset(this, 'BundledAsset', { Although optional, it's recommended to provide a local bundling method which can greatly improve performance. +If the bundling output contains a single archive file (zip or jar) it will be +uploaded to S3 as-is and will not be zipped. Otherwise the contents of the +output directory will be zipped and the zip file will be uploaded to S3. This +is the default behavior for `bundling.outputType` (`BundlingOutput.AUTO_DISCOVER`). + +Use `BundlingOutput.NOT_ARCHIVED` if the bundling output must always be zipped: + +```ts +const asset = new assets.Asset(this, 'BundledAsset', { + path: '/path/to/asset', + bundling: { + image: DockerImage.fromRegistry('alpine'), + command: ['command-that-produces-an-archive.sh'], + outputType: BundlingOutput.NOT_ARCHIVED, // Bundling output will be zipped even though it produces a single archive file. + }, +}); +``` + +Use `BundlingOutput.ARCHIVED` if the bundling output contains a single archive file and +you don't want it to be zippped. + ## CloudFormation Resource Metadata > NOTE: This section is relevant for authors of AWS Resource Constructs. diff --git a/packages/@aws-cdk/aws-s3-assets/lib/asset.ts b/packages/@aws-cdk/aws-s3-assets/lib/asset.ts index 938778d1381f4..510834a61c634 100644 --- a/packages/@aws-cdk/aws-s3-assets/lib/asset.ts +++ b/packages/@aws-cdk/aws-s3-assets/lib/asset.ts @@ -1,4 +1,3 @@ -import * as fs from 'fs'; import * as path from 'path'; import * as assets from '@aws-cdk/assets'; import * as iam from '@aws-cdk/aws-iam'; @@ -13,8 +12,6 @@ import { toSymlinkFollow } from './compat'; // eslint-disable-next-line no-duplicate-imports, import/order import { Construct as CoreConstruct } from '@aws-cdk/core'; -const ARCHIVE_EXTENSIONS = ['.zip', '.jar']; - export interface AssetOptions extends assets.CopyOptions, cdk.AssetOptions { /** * A list of principals that should be able to read this asset from S3. @@ -139,17 +136,12 @@ export class Asset extends CoreConstruct implements cdk.IAsset { this.assetPath = staging.relativeStagedPath(stack); - const packaging = determinePackaging(staging.sourcePath); - - this.isFile = packaging === cdk.FileAssetPackaging.FILE; + this.isFile = staging.packaging === cdk.FileAssetPackaging.FILE; - // sets isZipArchive based on the type of packaging and file extension - this.isZipArchive = packaging === cdk.FileAssetPackaging.ZIP_DIRECTORY - ? true - : ARCHIVE_EXTENSIONS.some(ext => staging.sourcePath.toLowerCase().endsWith(ext)); + this.isZipArchive = staging.isArchive; const location = stack.synthesizer.addFileAsset({ - packaging, + packaging: staging.packaging, sourceHash: this.sourceHash, fileName: this.assetPath, }); @@ -210,19 +202,3 @@ export class Asset extends CoreConstruct implements cdk.IAsset { this.bucket.grantRead(grantee); } } - -function determinePackaging(assetPath: string): cdk.FileAssetPackaging { - if (!fs.existsSync(assetPath)) { - throw new Error(`Cannot find asset at ${assetPath}`); - } - - if (fs.statSync(assetPath).isDirectory()) { - return cdk.FileAssetPackaging.ZIP_DIRECTORY; - } - - if (fs.statSync(assetPath).isFile()) { - return cdk.FileAssetPackaging.FILE; - } - - throw new Error(`Asset ${assetPath} is expected to be either a directory or a regular file`); -} diff --git a/packages/@aws-cdk/aws-s3-deployment/package.json b/packages/@aws-cdk/aws-s3-deployment/package.json index fadd40d30149a..859b206963e0e 100644 --- a/packages/@aws-cdk/aws-s3-deployment/package.json +++ b/packages/@aws-cdk/aws-s3-deployment/package.json @@ -78,6 +78,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "@types/jest": "^26.0.20", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts b/packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts index 5cb7cdc1e7cdf..9639af37a2aa8 100644 --- a/packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts +++ b/packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts @@ -5,10 +5,14 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; +import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as s3deploy from '../lib'; /* eslint-disable max-len */ +const s3GrantWriteCtx = { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true }; + test('deploy from local directory asset', () => { // GIVEN const stack = new cdk.Stack(); @@ -449,9 +453,9 @@ test('fails if distribution paths provided but not distribution ID', () => { }); -test('lambda execution role gets permissions to read from the source bucket and read/write in destination', () => { +testFutureBehavior('lambda execution role gets permissions to read from the source bucket and read/write in destination', s3GrantWriteCtx, cdk.App, (app) => { // GIVEN - const stack = new cdk.Stack(); + const stack = new cdk.Stack(app); const source = new s3.Bucket(stack, 'Source'); const bucket = new s3.Bucket(stack, 'Dest'); @@ -501,7 +505,7 @@ test('lambda execution role gets permissions to read from the source bucket and 's3:GetBucket*', 's3:List*', 's3:DeleteObject*', - 's3:PutObject*', + 's3:PutObject', 's3:Abort*', ], Effect: 'Allow', diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index 1edbb9c7a5040..630aa9f02bfcc 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -1830,6 +1830,7 @@ export class Bucket extends BucketBase { const provider = CustomResourceProvider.getOrCreateProvider(this, AUTO_DELETE_OBJECTS_RESOURCE_TYPE, { codeDirectory: path.join(__dirname, 'auto-delete-objects-handler'), runtime: CustomResourceProviderRuntime.NODEJS_12, + description: `Lambda function for auto-deleting objects in ${this.bucketName} S3 bucket.`, }); // Use a bucket policy to allow the custom resource to delete diff --git a/packages/@aws-cdk/aws-s3/test/bucket.test.ts b/packages/@aws-cdk/aws-s3/test/bucket.test.ts index 5a5f89a3eba94..936c807e8da4a 100644 --- a/packages/@aws-cdk/aws-s3/test/bucket.test.ts +++ b/packages/@aws-cdk/aws-s3/test/bucket.test.ts @@ -5,12 +5,14 @@ import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; -import { testLegacyBehavior } from 'cdk-build-tools/lib/feature-flag'; +import { testFutureBehavior, testLegacyBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as s3 from '../lib'; // to make it easy to copy & paste from output: /* eslint-disable quote-props */ +const s3GrantWriteCtx = { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true }; + describe('bucket', () => { test('default bucket', () => { const stack = new cdk.Stack(); @@ -814,8 +816,8 @@ describe('bucket', () => { }); describe('grantReadWrite', () => { - test('can be used to grant reciprocal permissions to an identity', () => { - const stack = new cdk.Stack(); + testFutureBehavior('can be used to grant reciprocal permissions to an identity', s3GrantWriteCtx, cdk.App, (app) => { + const stack = new cdk.Stack(app); const bucket = new s3.Bucket(stack, 'MyBucket'); const user = new iam.User(stack, 'MyUser'); bucket.grantReadWrite(user); @@ -841,7 +843,7 @@ describe('bucket', () => { 's3:GetBucket*', 's3:List*', 's3:DeleteObject*', - 's3:PutObject*', + 's3:PutObject', 's3:Abort*', ], 'Effect': 'Allow', @@ -1104,12 +1106,7 @@ describe('bucket', () => { }); }); - test('does not grant PutObjectAcl when the S3_GRANT_WRITE_WITHOUT_ACL feature is enabled', () => { - const app = new cdk.App({ - context: { - [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true, - }, - }); + testFutureBehavior('does not grant PutObjectAcl when the S3_GRANT_WRITE_WITHOUT_ACL feature is enabled', s3GrantWriteCtx, cdk.App, (app) => { const stack = new cdk.Stack(app, 'Stack'); const bucket = new s3.Bucket(stack, 'MyBucket'); const user = new iam.User(stack, 'MyUser'); @@ -1147,8 +1144,8 @@ describe('bucket', () => { }); describe('grantWrite', () => { - test('with KMS key has appropriate permissions for multipart uploads', () => { - const stack = new cdk.Stack(); + testFutureBehavior('with KMS key has appropriate permissions for multipart uploads', s3GrantWriteCtx, cdk.App, (app) => { + const stack = new cdk.Stack(app); const bucket = new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.KMS }); const user = new iam.User(stack, 'MyUser'); bucket.grantWrite(user); @@ -1159,7 +1156,7 @@ describe('bucket', () => { { 'Action': [ 's3:DeleteObject*', - 's3:PutObject*', + 's3:PutObject', 's3:Abort*', ], 'Effect': 'Allow', @@ -1215,12 +1212,7 @@ describe('bucket', () => { }); - test('does not grant PutObjectAcl when the S3_GRANT_WRITE_WITHOUT_ACL feature is enabled', () => { - const app = new cdk.App({ - context: { - [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true, - }, - }); + testFutureBehavior('does not grant PutObjectAcl when the S3_GRANT_WRITE_WITHOUT_ACL feature is enabled', s3GrantWriteCtx, cdk.App, (app) => { const stack = new cdk.Stack(app, 'Stack'); const bucket = new s3.Bucket(stack, 'MyBucket'); const user = new iam.User(stack, 'MyUser'); @@ -1255,12 +1247,7 @@ describe('bucket', () => { }); describe('grantPut', () => { - test('does not grant PutObjectAcl when the S3_GRANT_WRITE_WITHOUT_ACL feature is enabled', () => { - const app = new cdk.App({ - context: { - [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true, - }, - }); + testFutureBehavior('does not grant PutObjectAcl when the S3_GRANT_WRITE_WITHOUT_ACL feature is enabled', s3GrantWriteCtx, cdk.App, (app) => { const stack = new cdk.Stack(app, 'Stack'); const bucket = new s3.Bucket(stack, 'MyBucket'); const user = new iam.User(stack, 'MyUser'); @@ -1290,8 +1277,8 @@ describe('bucket', () => { }); }); - test('more grants', () => { - const stack = new cdk.Stack(); + testFutureBehavior('more grants', s3GrantWriteCtx, cdk.App, (app) => { + const stack = new cdk.Stack(app); const bucket = new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.KMS }); const putter = new iam.User(stack, 'Putter'); const writer = new iam.User(stack, 'Writer'); @@ -1304,8 +1291,8 @@ describe('bucket', () => { const resources = SynthUtils.synthesize(stack).template.Resources; const actions = (id: string) => resources[id].Properties.PolicyDocument.Statement[0].Action; - expect(actions('WriterDefaultPolicyDC585BCE')).toEqual(['s3:DeleteObject*', 's3:PutObject*', 's3:Abort*']); - expect(actions('PutterDefaultPolicyAB138DD3')).toEqual(['s3:PutObject*', 's3:Abort*']); + expect(actions('WriterDefaultPolicyDC585BCE')).toEqual(['s3:DeleteObject*', 's3:PutObject', 's3:Abort*']); + expect(actions('PutterDefaultPolicyAB138DD3')).toEqual(['s3:PutObject', 's3:Abort*']); expect(actions('DeleterDefaultPolicyCD33B8A0')).toEqual('s3:DeleteObject*'); }); diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json b/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json index d9f263a8d840d..831d072339649 100644 --- a/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json @@ -148,7 +148,19 @@ "Arn" ] }, - "Runtime": "nodejs12.x" + "Runtime": "nodejs12.x", + "Description": { + "Fn::Join": [ + "", + [ + "Lambda function for auto-deleting objects in ", + { + "Ref": "Bucket83908E77" + }, + " S3 bucket." + ] + ] + } }, "DependsOn": [ "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092" diff --git a/packages/@aws-cdk/aws-ses-actions/test/integ.actions.expected.json b/packages/@aws-cdk/aws-ses-actions/test/integ.actions.expected.json index b58b770bcd426..4379649e02d69 100644 --- a/packages/@aws-cdk/aws-ses-actions/test/integ.actions.expected.json +++ b/packages/@aws-cdk/aws-ses-actions/test/integ.actions.expected.json @@ -40,13 +40,13 @@ "Code": { "ZipFile": "exports.handler = async (event) => event;" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "FunctionServiceRole675BB04A", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -341,16 +341,51 @@ "Type": "AWS::Lambda::Function", "Properties": { "Code": { - "ZipFile": "exports.handler = function dropSpamCode(event, _, callback) {\n console.log('Spam filter');\n const sesNotification = event.Records[0].ses;\n console.log('SES Notification:\\n', JSON.stringify(sesNotification, null, 2));\n // Check if any spam check failed\n if (sesNotification.receipt.spfVerdict.status === 'FAIL'\n || sesNotification.receipt.dkimVerdict.status === 'FAIL'\n || sesNotification.receipt.spamVerdict.status === 'FAIL'\n || sesNotification.receipt.virusVerdict.status === 'FAIL') {\n console.log('Dropping spam');\n // Stop processing rule set, dropping message\n callback(null, { disposition: 'STOP_RULE_SET' });\n }\n else {\n callback(null, null);\n }\n}" + "S3Bucket": { + "Ref": "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3Bucket6AFCBA5F" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3VersionKey02BA9086" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3VersionKey02BA9086" + } + ] + } + ] + } + ] + ] + } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "SingletonLambda224e77f9a32e4b4dac32983477abba16ServiceRole3037F5B4", "Arn" ] }, - "Runtime": "nodejs10.x" + "Handler": "index.handler", + "Runtime": "nodejs14.x" }, "DependsOn": [ "SingletonLambda224e77f9a32e4b4dac32983477abba16ServiceRole3037F5B4" @@ -372,5 +407,19 @@ } } } + }, + "Parameters": { + "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3Bucket6AFCBA5F": { + "Type": "String", + "Description": "S3 bucket for asset \"96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34\"" + }, + "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3VersionKey02BA9086": { + "Type": "String", + "Description": "S3 key for asset version \"96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34\"" + }, + "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34ArtifactHash6BE57680": { + "Type": "String", + "Description": "Artifact hash for asset \"96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34\"" + } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ses/lib/drop-spam-handler/index.ts b/packages/@aws-cdk/aws-ses/lib/drop-spam-handler/index.ts new file mode 100644 index 0000000000000..76a639acdf50e --- /dev/null +++ b/packages/@aws-cdk/aws-ses/lib/drop-spam-handler/index.ts @@ -0,0 +1,22 @@ +/* eslint-disable no-console */ + +// Adapted from https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-action-lambda-example-functions.html +export async function handler(event: AWSLambda.SESEvent): Promise<{ disposition: string } | null> { + console.log('Spam filter'); + + const sesNotification = event.Records[0].ses; + console.log('SES Notification: %j', sesNotification); + + // Check if any spam check failed + if (sesNotification.receipt.spfVerdict.status === 'FAIL' + || sesNotification.receipt.dkimVerdict.status === 'FAIL' + || sesNotification.receipt.spamVerdict.status === 'FAIL' + || sesNotification.receipt.virusVerdict.status === 'FAIL') { + console.log('Dropping spam'); + + // Stop processing rule set, dropping message + return { disposition: 'STOP_RULE_SET' }; + } + + return null; +} diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts b/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts index 5b6a276929c8e..7f6f1fe914a6b 100644 --- a/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts +++ b/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts @@ -1,3 +1,4 @@ +import * as path from 'path'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import { Aws, IResource, Lazy, Resource } from '@aws-cdk/core'; @@ -175,9 +176,9 @@ export class DropSpamReceiptRule extends CoreConstruct { super(scope, id); const fn = new lambda.SingletonFunction(this, 'Function', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler', - code: lambda.Code.fromInline(`exports.handler = ${dropSpamCode}`), + code: lambda.Code.fromAsset(path.join(__dirname, 'drop-spam-handler')), uuid: '224e77f9-a32e-4b4d-ac32-983477abba16', }); @@ -203,25 +204,3 @@ export class DropSpamReceiptRule extends CoreConstruct { }); } } - -// Adapted from https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-action-lambda-example-functions.html -/* eslint-disable no-console */ -function dropSpamCode(event: any, _: any, callback: any) { - console.log('Spam filter'); - - const sesNotification = event.Records[0].ses; - console.log('SES Notification:\n', JSON.stringify(sesNotification, null, 2)); - - // Check if any spam check failed - if (sesNotification.receipt.spfVerdict.status === 'FAIL' - || sesNotification.receipt.dkimVerdict.status === 'FAIL' - || sesNotification.receipt.spamVerdict.status === 'FAIL' - || sesNotification.receipt.virusVerdict.status === 'FAIL') { - console.log('Dropping spam'); - - // Stop processing rule set, dropping message - callback(null, { disposition: 'STOP_RULE_SET' }); - } else { - callback(null, null); - } -} diff --git a/packages/@aws-cdk/aws-ses/test/integ.receipt.expected.json b/packages/@aws-cdk/aws-ses/test/integ.receipt.expected.json index b4cdf79e60d44..2bb22f110b951 100644 --- a/packages/@aws-cdk/aws-ses/test/integ.receipt.expected.json +++ b/packages/@aws-cdk/aws-ses/test/integ.receipt.expected.json @@ -97,16 +97,51 @@ "Type": "AWS::Lambda::Function", "Properties": { "Code": { - "ZipFile": "exports.handler = function dropSpamCode(event, _, callback) {\n console.log('Spam filter');\n const sesNotification = event.Records[0].ses;\n console.log('SES Notification:\\n', JSON.stringify(sesNotification, null, 2));\n // Check if any spam check failed\n if (sesNotification.receipt.spfVerdict.status === 'FAIL'\n || sesNotification.receipt.dkimVerdict.status === 'FAIL'\n || sesNotification.receipt.spamVerdict.status === 'FAIL'\n || sesNotification.receipt.virusVerdict.status === 'FAIL') {\n console.log('Dropping spam');\n // Stop processing rule set, dropping message\n callback(null, { disposition: 'STOP_RULE_SET' });\n }\n else {\n callback(null, null);\n }\n}" + "S3Bucket": { + "Ref": "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3Bucket6AFCBA5F" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3VersionKey02BA9086" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3VersionKey02BA9086" + } + ] + } + ] + } + ] + ] + } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "SingletonLambda224e77f9a32e4b4dac32983477abba16ServiceRole3037F5B4", "Arn" ] }, - "Runtime": "nodejs10.x" + "Handler": "index.handler", + "Runtime": "nodejs14.x" }, "DependsOn": [ "SingletonLambda224e77f9a32e4b4dac32983477abba16ServiceRole3037F5B4" @@ -150,5 +185,19 @@ } } } + }, + "Parameters": { + "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3Bucket6AFCBA5F": { + "Type": "String", + "Description": "S3 bucket for asset \"96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34\"" + }, + "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3VersionKey02BA9086": { + "Type": "String", + "Description": "S3 key for asset version \"96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34\"" + }, + "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34ArtifactHash6BE57680": { + "Type": "String", + "Description": "Artifact hash for asset \"96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34\"" + } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-signer/README.md b/packages/@aws-cdk/aws-signer/README.md index 5482a0b23c900..925261fd4be52 100644 --- a/packages/@aws-cdk/aws-signer/README.md +++ b/packages/@aws-cdk/aws-signer/README.md @@ -9,12 +9,55 @@ > > [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + --- -This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. +AWS Signer is a fully managed code-signing service to ensure the trust and integrity of your code. Organizations validate code against +a digital signature to confirm that the code is unaltered and from a trusted publisher. For more information, see [What Is AWS +Signer?](https://docs.aws.amazon.com/signer/latest/developerguide/Welcome.html) + +## Table of Contents + +- [Signing Platform](#signing-platform) +- [Signing Profile](#signing-profile) + +## Signing Platform + +A signing platform is a predefined set of instructions that specifies the signature format and signing algorithms that AWS Signer should use +to sign a zip file. For more information go to [Signing Platforms in AWS Signer](https://docs.aws.amazon.com/signer/latest/developerguide/gs-platform.html). + +AWS Signer provides a pre-defined set of signing platforms. They are available in the CDK as - ```ts -import signer = require('@aws-cdk/aws-signer'); +Platform.AWS_IOT_DEVICE_MANAGEMENT_SHA256_ECDSA +Platform.AWS_LAMBDA_SHA384_ECDSA +Platform.AMAZON_FREE_RTOS_TI_CC3220SF +Platform.AMAZON_FREE_RTOS_DEFAULT ``` + +## Signing Profile + +A signing profile is a code-signing template that can be used to pre-define the signature specifications for a signing job. +A signing profile includes a signing platform to designate the file type to be signed, the signature format, and the signature algorithms. +For more information, visit [Signing Profiles in AWS Signer](https://docs.aws.amazon.com/signer/latest/developerguide/gs-profile.html). + +The following code sets up a signing profile for signing lambda code bundles - + +```ts +import * as signer from '@aws-cdk/aws-signer'; + +const signingProfile = new signer.SigningProfile(this, 'SigningProfile', { + platform: signer.Platform.AWS_LAMBDA_SHA384_ECDSA, +} ); +``` + +A signing profile is valid by default for 135 months. This can be modified by specifying the `signatureValidityPeriod` property. diff --git a/packages/@aws-cdk/aws-signer/lib/index.ts b/packages/@aws-cdk/aws-signer/lib/index.ts index 9c56379e86c19..090dec21fac3b 100644 --- a/packages/@aws-cdk/aws-signer/lib/index.ts +++ b/packages/@aws-cdk/aws-signer/lib/index.ts @@ -1,2 +1,3 @@ // AWS::Signer CloudFormation Resources: export * from './signer.generated'; +export * from './signing-profile'; diff --git a/packages/@aws-cdk/aws-signer/lib/signing-profile.ts b/packages/@aws-cdk/aws-signer/lib/signing-profile.ts new file mode 100644 index 0000000000000..8a0d14c3d194a --- /dev/null +++ b/packages/@aws-cdk/aws-signer/lib/signing-profile.ts @@ -0,0 +1,178 @@ +import { Duration, IResource, Resource, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnSigningProfile } from './signer.generated'; + +/** + * Platforms that are allowed with signing config. + * @see https://docs.aws.amazon.com/signer/latest/developerguide/gs-platform.html + */ +export class Platform { + /** + * Specification of signature format and signing algorithms for AWS IoT Device. + */ + public static readonly AWS_IOT_DEVICE_MANAGEMENT_SHA256_ECDSA = new Platform('AWSIoTDeviceManagement-SHA256-ECDSA'); + + /** + * Specification of signature format and signing algorithms for AWS Lambda. + */ + public static readonly AWS_LAMBDA_SHA384_ECDSA = new Platform('AWSLambda-SHA384-ECDSA'); + + /** + * Specification of signature format and signing algorithms with + * SHA1 hash and RSA encryption for Amazon FreeRTOS. + */ + public static readonly AMAZON_FREE_RTOS_TI_CC3220SF = new Platform('AmazonFreeRTOS-TI-CC3220SF'); + + /** + * Specification of signature format and signing algorithms with + * SHA256 hash and ECDSA encryption for Amazon FreeRTOS. + */ + public static readonly AMAZON_FREE_RTOS_DEFAULT = new Platform('AmazonFreeRTOS-Default'); + + /** + * The id of signing platform. + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-signer-signingprofile.html#cfn-signer-signingprofile-platformid + */ + public readonly platformId: string; + + private constructor(platformId: string) { + this.platformId = platformId; + } +} + +/** + * A Signer Profile + */ +export interface ISigningProfile extends IResource { + /** + * The ARN of the signing profile. + * @attribute + */ + readonly signingProfileArn: string; + + /** + * The name of signing profile. + * @attribute ProfileName + */ + readonly signingProfileName: string; + + /** + * The version of signing profile. + * @attribute ProfileVersion + */ + readonly signingProfileVersion: string; + + /** + * The ARN of signing profile version. + * @attribute ProfileVersionArn + */ + readonly signingProfileVersionArn: string; +} + +/** + * Construction properties for a Signing Profile object + */ +export interface SigningProfileProps { + /** + * The Signing Platform available for signing profile. + * @see https://docs.aws.amazon.com/signer/latest/developerguide/gs-platform.html + */ + readonly platform: Platform; + + /** + * The validity period for signatures generated using + * this signing profile. + * + * @default - 135 months + */ + readonly signatureValidity?: Duration; + + /** + * Physical name of this Signing Profile. + * + * @default - Assigned by CloudFormation (recommended). + */ + readonly signingProfileName?: string; +} + +/** + * A reference to a Signing Profile + */ +export interface SigningProfileAttributes { + /** + * The name of signing profile. + */ + readonly signingProfileName: string; + + /** + * The version of signing profile. + */ + readonly signingProfileVersion: string; +} + +/** + * Defines a Signing Profile. + * + * @resource AWS::Signer::SigningProfile + */ +export class SigningProfile extends Resource implements ISigningProfile { + /** + * Creates a Signing Profile construct that represents an external Signing Profile. + * + * @param scope The parent creating construct (usually `this`). + * @param id The construct's name. + * @param attrs A `SigningProfileAttributes` object. + */ + public static fromSigningProfileAttributes( scope: Construct, id: string, attrs: SigningProfileAttributes): ISigningProfile { + class Import extends Resource implements ISigningProfile { + public readonly signingProfileArn: string; + public readonly signingProfileName = attrs.signingProfileName; + public readonly signingProfileVersion = attrs.signingProfileVersion; + public readonly signingProfileVersionArn: string; + + constructor(signingProfileArn: string, signingProfileProfileVersionArn: string) { + super(scope, id); + this.signingProfileArn = signingProfileArn; + this.signingProfileVersionArn = signingProfileProfileVersionArn; + } + } + const signingProfileArn = Stack.of(scope).formatArn({ + service: 'signer', + resource: '', + resourceName: `/signing-profiles/${attrs.signingProfileName}`, + }); + const SigningProfileVersionArn = Stack.of(scope).formatArn({ + service: 'signer', + resource: '', + resourceName: `/signing-profiles/${attrs.signingProfileName}/${attrs.signingProfileVersion}`, + }); + return new Import(signingProfileArn, SigningProfileVersionArn); + } + + public readonly signingProfileArn: string; + public readonly signingProfileName: string; + public readonly signingProfileVersion: string; + public readonly signingProfileVersionArn: string; + + constructor(scope: Construct, id: string, props: SigningProfileProps) { + super(scope, id, { + physicalName: props.signingProfileName, + }); + + const resource = new CfnSigningProfile( this, 'Resource', { + platformId: props.platform.platformId, + signatureValidityPeriod: props.signatureValidity ? { + type: 'DAYS', + value: props.signatureValidity?.toDays(), + } : { + type: 'MONTHS', + value: 135, + }, + } ); + + this.signingProfileArn = resource.attrArn; + this.signingProfileName = resource.attrProfileName; + this.signingProfileVersion = resource.attrProfileVersion; + this.signingProfileVersionArn = resource.attrProfileVersionArn; + } +} diff --git a/packages/@aws-cdk/aws-signer/package.json b/packages/@aws-cdk/aws-signer/package.json index 40a8f5872b5b1..f01a984dfdd28 100644 --- a/packages/@aws-cdk/aws-signer/package.json +++ b/packages/@aws-cdk/aws-signer/package.json @@ -79,16 +79,18 @@ "pkglint": "0.0.0" }, "dependencies": { - "@aws-cdk/core": "0.0.0" + "@aws-cdk/core": "0.0.0", + "constructs": "^3.2.0" }, "peerDependencies": { - "@aws-cdk/core": "0.0.0" + "@aws-cdk/core": "0.0.0", + "constructs": "^3.2.0" }, "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", - "maturity": "cfn-only", + "maturity": "experimental", "awscdkio": { "announce": false }, diff --git a/packages/@aws-cdk/aws-signer/test/signer.test.ts b/packages/@aws-cdk/aws-signer/test/signer.test.ts deleted file mode 100644 index e394ef336bfb4..0000000000000 --- a/packages/@aws-cdk/aws-signer/test/signer.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -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-signer/test/signing-profile.test.ts b/packages/@aws-cdk/aws-signer/test/signing-profile.test.ts new file mode 100644 index 0000000000000..6148a6be70bda --- /dev/null +++ b/packages/@aws-cdk/aws-signer/test/signing-profile.test.ts @@ -0,0 +1,115 @@ +import '@aws-cdk/assert/jest'; +import * as cdk from '@aws-cdk/core'; +import * as signer from '../lib'; + +let app: cdk.App; +let stack: cdk.Stack; +beforeEach( () => { + app = new cdk.App( {} ); + stack = new cdk.Stack( app ); +} ); + +describe('signing profile', () => { + test( 'default', () => { + const platform = signer.Platform.AWS_LAMBDA_SHA384_ECDSA; + new signer.SigningProfile( stack, 'SigningProfile', { platform } ); + + expect(stack).toHaveResource('AWS::Signer::SigningProfile', { + PlatformId: platform.platformId, + SignatureValidityPeriod: { + Type: 'MONTHS', + Value: 135, + }, + }); + }); + + test( 'default with signature validity period', () => { + const platform = signer.Platform.AWS_LAMBDA_SHA384_ECDSA; + new signer.SigningProfile( stack, 'SigningProfile', { + platform, + signatureValidity: cdk.Duration.days( 7 ), + } ); + + expect(stack).toHaveResource('AWS::Signer::SigningProfile', { + PlatformId: platform.platformId, + SignatureValidityPeriod: { + Type: 'DAYS', + Value: 7, + }, + }); + }); + + test( 'default with some tags', () => { + const platform = signer.Platform.AWS_LAMBDA_SHA384_ECDSA; + const signing = new signer.SigningProfile( stack, 'SigningProfile', { platform } ); + + cdk.Tags.of(signing).add('tag1', 'value1'); + cdk.Tags.of(signing).add('tag2', 'value2'); + cdk.Tags.of(signing).add('tag3', ''); + + expect(stack).toHaveResource('AWS::Signer::SigningProfile', { + PlatformId: platform.platformId, + SignatureValidityPeriod: { + Type: 'MONTHS', + Value: 135, + }, + Tags: [ + { + Key: 'tag1', + Value: 'value1', + }, + { + Key: 'tag2', + Value: 'value2', + }, + { + Key: 'tag3', + Value: '', + }, + ], + }); + }); + + describe('import', () => { + test('from signingProfileProfileName and signingProfileProfileVersion', () => { + const signingProfileName = 'test'; + const signingProfileVersion = 'xxxxxxxx'; + const signingProfile = signer.SigningProfile.fromSigningProfileAttributes(stack, 'Imported', { + signingProfileName, + signingProfileVersion, + }); + + expect(stack.resolve(signingProfile.signingProfileArn)).toStrictEqual( + { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':signer:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + `://signing-profiles/${signingProfileName}`, + ], + ], + }, + ); + expect(stack.resolve(signingProfile.signingProfileVersionArn)).toStrictEqual({ + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':signer:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + `://signing-profiles/${signingProfileName}/${signingProfileVersion}`, + ], + ], + }); + expect(stack).toMatchTemplate({}); + }); + } ); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index 38305b270da69..d7c2d1498394d 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -189,7 +189,7 @@ const convertToSeconds = new tasks.EvaluateExpression(this, 'Convert to seconds' const createMessage = new tasks.EvaluateExpression(this, 'Create message', { // Note: this is a string inside a string. expression: '`Now waiting ${$.waitSeconds} seconds...`', - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_14_X, resultPath: '$.message', }); @@ -212,9 +212,8 @@ new sfn.StateMachine(this, 'StateMachine', { ``` The `EvaluateExpression` supports a `runtime` prop to specify the Lambda -runtime to use to evaluate the expression. Currently, the only runtime -supported is `lambda.Runtime.NODEJS_10_X`. - +runtime to use to evaluate the expression. Currently, only runtimes +of the Node.js family are supported. ## Athena diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/eval-nodejs10.x-handler/index.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/eval-nodejs-handler/index.ts similarity index 100% rename from packages/@aws-cdk/aws-stepfunctions-tasks/lib/eval-nodejs10.x-handler/index.ts rename to packages/@aws-cdk/aws-stepfunctions-tasks/lib/eval-nodejs-handler/index.ts diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/evaluate-expression.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/evaluate-expression.ts index 45457a1c377c8..64c25d5e3dd3a 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/evaluate-expression.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/evaluate-expression.ts @@ -20,7 +20,7 @@ export interface EvaluateExpressionProps extends sfn.TaskStateBaseProps { /** * The runtime language to use to evaluate the expression. * - * @default lambda.Runtime.NODEJS_10_X + * @default lambda.Runtime.NODEJS_14_X */ readonly runtime?: lambda.Runtime; } @@ -58,7 +58,7 @@ export class EvaluateExpression extends sfn.TaskStateBase { constructor(scope: Construct, id: string, private readonly props: EvaluateExpressionProps) { super(scope, id, props); - this.evalFn = createEvalFn(this.props.runtime || lambda.Runtime.NODEJS_10_X, this); + this.evalFn = createEvalFn(this.props.runtime ?? lambda.Runtime.NODEJS_14_X, this); this.taskPolicies = [ new iam.PolicyStatement({ @@ -97,17 +97,18 @@ export class EvaluateExpression extends sfn.TaskStateBase { } function createEvalFn(runtime: lambda.Runtime, scope: Construct) { - const code = lambda.Code.asset(path.join(__dirname, `eval-${runtime.name}-handler`)); const lambdaPurpose = 'Eval'; switch (runtime) { + case lambda.Runtime.NODEJS_14_X: + case lambda.Runtime.NODEJS_12_X: case lambda.Runtime.NODEJS_10_X: return new lambda.SingletonFunction(scope, 'EvalFunction', { runtime, handler: 'index.handler', uuid: 'a0d2ce44-871b-4e74-87a1-f5e63d7c3bdc', lambdaPurpose, - code, + code: lambda.Code.fromAsset(path.join(__dirname, 'eval-nodejs-handler')), }); // TODO: implement other runtimes default: diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eval-nodejs10.x-handler.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eval-nodejs-handler.test.ts similarity index 96% rename from packages/@aws-cdk/aws-stepfunctions-tasks/test/eval-nodejs10.x-handler.test.ts rename to packages/@aws-cdk/aws-stepfunctions-tasks/test/eval-nodejs-handler.test.ts index d42be11d7aaa4..8d69e9d3b8105 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eval-nodejs10.x-handler.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eval-nodejs-handler.test.ts @@ -1,5 +1,5 @@ import { Event } from '../lib'; -import { handler } from '../lib/eval-nodejs10.x-handler'; +import { handler } from '../lib/eval-nodejs-handler'; test('with numbers', async () => { // GIVEN diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/evaluate-expression.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/evaluate-expression.test.ts index 679e817dfbead..7a2aa196b3de2 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/evaluate-expression.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/evaluate-expression.test.ts @@ -34,7 +34,7 @@ test('Eval with Node.js', () => { }); expect(stack).toHaveResource('AWS::Lambda::Function', { - Runtime: 'nodejs10.x', + Runtime: 'nodejs14.x', }); }); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.evaluate-expression.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.evaluate-expression.expected.json index efdf3878e67e2..c48b04a826783 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.evaluate-expression.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.evaluate-expression.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0S3BucketA16CB30E" + "Ref": "AssetParametersbc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626bS3Bucket743A2950" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0S3VersionKey102DBBD9" + "Ref": "AssetParametersbc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626bS3VersionKey2DBCB833" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0S3VersionKey102DBBD9" + "Ref": "AssetParametersbc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626bS3VersionKey2DBCB833" } ] } @@ -72,14 +72,14 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "Evala0d2ce44871b4e7487a1f5e63d7c3bdcServiceRoleDC85DDD3", "Arn" ] }, - "Runtime": "nodejs10.x" + "Handler": "index.handler", + "Runtime": "nodejs14.x" }, "DependsOn": [ "Evala0d2ce44871b4e7487a1f5e63d7c3bdcServiceRoleDC85DDD3" @@ -185,17 +185,17 @@ } }, "Parameters": { - "AssetParameters640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0S3BucketA16CB30E": { + "AssetParametersbc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626bS3Bucket743A2950": { "Type": "String", - "Description": "S3 bucket for asset \"640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0\"" + "Description": "S3 bucket for asset \"bc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626b\"" }, - "AssetParameters640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0S3VersionKey102DBBD9": { + "AssetParametersbc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626bS3VersionKey2DBCB833": { "Type": "String", - "Description": "S3 key for asset version \"640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0\"" + "Description": "S3 key for asset version \"bc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626b\"" }, - "AssetParameters640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0ArtifactHash43D553D7": { + "AssetParametersbc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626bArtifactHashAD6C554B": { "Type": "String", - "Description": "Artifact hash for asset \"640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0\"" + "Description": "Artifact hash for asset \"bc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626b\"" } }, "Outputs": { @@ -205,4 +205,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/cfnspec/CHANGELOG.md b/packages/@aws-cdk/cfnspec/CHANGELOG.md index abcd6fa61a9c9..a952cac779b88 100644 --- a/packages/@aws-cdk/cfnspec/CHANGELOG.md +++ b/packages/@aws-cdk/cfnspec/CHANGELOG.md @@ -1,3 +1,126 @@ +# CloudFormation Resource Specification v29.0.0 + +## New Resource Types + +## Attribute Changes + +* AWS::EC2::TransitGatewayMulticastDomainAssociation State (__added__) +* AWS::EC2::TransitGatewayMulticastGroupMember TransitGatewayAttachmentId (__added__) +* AWS::EC2::TransitGatewayMulticastGroupSource TransitGatewayAttachmentId (__added__) +* AWS::IoTWireless::ServiceProfile LoRaWANResponse (__deleted__) +* AWS::IoTWireless::ServiceProfile ChannelMask (__added__) +* AWS::IoTWireless::ServiceProfile DevStatusReqFreq (__added__) +* AWS::IoTWireless::ServiceProfile DlBucketSize (__added__) +* AWS::IoTWireless::ServiceProfile DlRate (__added__) +* AWS::IoTWireless::ServiceProfile DlRatePolicy (__added__) +* AWS::IoTWireless::ServiceProfile DrMax (__added__) +* AWS::IoTWireless::ServiceProfile DrMin (__added__) +* AWS::IoTWireless::ServiceProfile HrAllowed (__added__) +* AWS::IoTWireless::ServiceProfile MinGwDiversity (__added__) +* AWS::IoTWireless::ServiceProfile NwkGeoLoc (__added__) +* AWS::IoTWireless::ServiceProfile PrAllowed (__added__) +* AWS::IoTWireless::ServiceProfile RaAllowed (__added__) +* AWS::IoTWireless::ServiceProfile ReportDevStatusBattery (__added__) +* AWS::IoTWireless::ServiceProfile ReportDevStatusMargin (__added__) +* AWS::IoTWireless::ServiceProfile TargetPer (__added__) +* AWS::IoTWireless::ServiceProfile UlBucketSize (__added__) +* AWS::IoTWireless::ServiceProfile UlRate (__added__) +* AWS::IoTWireless::ServiceProfile UlRatePolicy (__added__) +* AWS::IoTWireless::WirelessDevice ThingArn (__deleted__) +* AWS::IoTWireless::WirelessGateway ThingArn (__deleted__) +* AWS::IoTWireless::WirelessGateway ThingName (__added__) + +## Property Changes + +* AWS::CodeBuild::Project ConcurrentBuildLimit (__added__) +* AWS::DataBrew::Job JobSample (__added__) +* AWS::DynamoDB::Table KinesisStreamSpecification (__added__) +* AWS::EC2::TransitGatewayMulticastDomain TransitGatewayId.Required (__changed__) + * Old: false + * New: true +* AWS::EC2::TransitGatewayMulticastDomainAssociation State (__deleted__) +* AWS::EC2::TransitGatewayMulticastDomainAssociation SubnetId.Required (__changed__) + * Old: false + * New: true +* AWS::EC2::TransitGatewayMulticastDomainAssociation TransitGatewayAttachmentId.Required (__changed__) + * Old: false + * New: true +* AWS::EC2::TransitGatewayMulticastDomainAssociation TransitGatewayMulticastDomainId.Required (__changed__) + * Old: false + * New: true +* AWS::EC2::TransitGatewayMulticastGroupMember TransitGatewayAttachmentId (__deleted__) +* AWS::EC2::TransitGatewayMulticastGroupMember GroupIpAddress.Required (__changed__) + * Old: false + * New: true +* AWS::EC2::TransitGatewayMulticastGroupMember NetworkInterfaceId.Required (__changed__) + * Old: false + * New: true +* AWS::EC2::TransitGatewayMulticastGroupMember TransitGatewayMulticastDomainId.Required (__changed__) + * Old: false + * New: true +* AWS::EC2::TransitGatewayMulticastGroupSource TransitGatewayAttachmentId (__deleted__) +* AWS::EC2::TransitGatewayMulticastGroupSource GroupIpAddress.Required (__changed__) + * Old: false + * New: true +* AWS::EC2::TransitGatewayMulticastGroupSource NetworkInterfaceId.Required (__changed__) + * Old: false + * New: true +* AWS::EC2::TransitGatewayMulticastGroupSource TransitGatewayMulticastDomainId.Required (__changed__) + * Old: false + * New: true +* AWS::ElastiCache::GlobalReplicationGroup CacheParameterGroupName (__added__) +* AWS::ElasticLoadBalancingV2::TargetGroup ProtocolVersion (__added__) +* AWS::ImageBuilder::Image ContainerRecipeArn (__added__) +* AWS::ImageBuilder::Image ImageRecipeArn.Required (__changed__) + * Old: true + * New: false +* AWS::ImageBuilder::ImagePipeline ContainerRecipeArn (__added__) +* AWS::ImageBuilder::ImagePipeline ImageRecipeArn.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Dataset LateDataRules (__added__) +* AWS::IoTWireless::WirelessDevice ThingArn (__added__) +* AWS::IoTWireless::WirelessGateway ThingName (__deleted__) +* AWS::IoTWireless::WirelessGateway ThingArn (__added__) +* AWS::StepFunctions::StateMachine Definition (__added__) + +## Property Type Changes + +* AWS::IoTWireless::ServiceProfile.LoRaWANGetServiceProfileInfo (__removed__) +* AWS::DynamoDB::Table.KinesisStreamSpecification (__added__) +* AWS::IoTAnalytics::Dataset.DeltaTimeSessionWindowConfiguration (__added__) +* AWS::IoTAnalytics::Dataset.LateDataRule (__added__) +* AWS::IoTAnalytics::Dataset.LateDataRuleConfiguration (__added__) +* AWS::StepFunctions::StateMachine.Definition (__added__) +* AWS::CodeCommit::Repository.Code BranchName.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::ElasticLoadBalancingV2::TargetGroup.Matcher GrpcCode (__added__) +* AWS::FSx::FileSystem.WindowsConfiguration Aliases (__added__) +* AWS::FSx::FileSystem.WindowsConfiguration ThroughputCapacity.Required (__changed__) + * Old: false + * New: true +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile ChannelMask (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile DevStatusReqFreq (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile DlBucketSize (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile DlRate (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile DlRatePolicy (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile DrMax (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile DrMin (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile HrAllowed (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile MinGwDiversity (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile NwkGeoLoc (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile PrAllowed (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile RaAllowed (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile ReportDevStatusBattery (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile ReportDevStatusMargin (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile TargetPer (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile UlBucketSize (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile UlRate (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile UlRatePolicy (__added__) +* AWS::Pinpoint::Campaign.CampaignSmsMessage OriginationNumber (__added__) + + # CloudFormation Resource Specification v28.0.0 ## New Resource Types diff --git a/packages/@aws-cdk/cfnspec/cfn.version b/packages/@aws-cdk/cfnspec/cfn.version index 0b4dfcf095f29..f1e4903d1d8e0 100644 --- a/packages/@aws-cdk/cfnspec/cfn.version +++ b/packages/@aws-cdk/cfnspec/cfn.version @@ -1 +1 @@ -28.0.0 +29.0.0 diff --git a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json index eca5e7601bdb5..da0274f96bd19 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json +++ b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json @@ -11527,7 +11527,7 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codecommit-repository-code.html#cfn-codecommit-repository-code-branchname", "PrimitiveType": "String", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "S3": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codecommit-repository-code.html#cfn-codecommit-repository-code-s3", @@ -15702,6 +15702,17 @@ } } }, + "AWS::DynamoDB::Table.KinesisStreamSpecification": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-kinesisstreamspecification.html", + "Properties": { + "StreamArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-kinesisstreamspecification.html#cfn-dynamodb-kinesisstreamspecification-streamarn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::DynamoDB::Table.LocalSecondaryIndex": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-lsi.html", "Properties": { @@ -23137,6 +23148,12 @@ "AWS::ElasticLoadBalancingV2::TargetGroup.Matcher": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticloadbalancingv2-targetgroup-matcher.html", "Properties": { + "GrpcCode": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticloadbalancingv2-targetgroup-matcher.html#cfn-elasticloadbalancingv2-targetgroup-matcher-grpccode", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "HttpCode": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticloadbalancingv2-targetgroup-matcher.html#cfn-elasticloadbalancingv2-targetgroup-matcher-httpcode", "PrimitiveType": "String", @@ -24122,6 +24139,13 @@ "Required": false, "UpdateType": "Immutable" }, + "Aliases": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-windowsconfiguration.html#cfn-fsx-filesystem-windowsconfiguration-aliases", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, "AutomaticBackupRetentionDays": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-windowsconfiguration.html#cfn-fsx-filesystem-windowsconfiguration-automaticbackupretentiondays", "PrimitiveType": "Integer", @@ -24161,7 +24185,7 @@ "ThroughputCapacity": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-windowsconfiguration.html#cfn-fsx-filesystem-windowsconfiguration-throughputcapacity", "PrimitiveType": "Integer", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "WeeklyMaintenanceStartTime": { @@ -28797,6 +28821,17 @@ } } }, + "AWS::IoTAnalytics::Dataset.DeltaTimeSessionWindowConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-deltatimesessionwindowconfiguration.html", + "Properties": { + "TimeoutInMinutes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-deltatimesessionwindowconfiguration.html#cfn-iotanalytics-dataset-deltatimesessionwindowconfiguration-timeoutinminutes", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::IoTAnalytics::Dataset.Filter": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-filter.html", "Properties": { @@ -28842,6 +28877,34 @@ } } }, + "AWS::IoTAnalytics::Dataset.LateDataRule": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-latedatarule.html", + "Properties": { + "RuleConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-latedatarule.html#cfn-iotanalytics-dataset-latedatarule-ruleconfiguration", + "Required": true, + "Type": "LateDataRuleConfiguration", + "UpdateType": "Mutable" + }, + "RuleName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-latedatarule.html#cfn-iotanalytics-dataset-latedatarule-rulename", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::IoTAnalytics::Dataset.LateDataRuleConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-latedataruleconfiguration.html", + "Properties": { + "DeltaTimeSessionWindowConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-latedataruleconfiguration.html#cfn-iotanalytics-dataset-latedataruleconfiguration-deltatimesessionwindowconfiguration", + "Required": false, + "Type": "DeltaTimeSessionWindowConfiguration", + "UpdateType": "Mutable" + } + } + }, "AWS::IoTAnalytics::Dataset.OutputFileUriValue": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-variable-outputfileurivalue.html", "Properties": { @@ -30579,136 +30642,125 @@ } } }, - "AWS::IoTWireless::ServiceProfile.LoRaWANGetServiceProfileInfo": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html", + "AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html", "Properties": { "AddGwMetadata": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-addgwmetadata", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-addgwmetadata", "PrimitiveType": "Boolean", "Required": false, "UpdateType": "Mutable" }, "ChannelMask": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-channelmask", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-channelmask", "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" }, "DevStatusReqFreq": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-devstatusreqfreq", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-devstatusreqfreq", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "DlBucketSize": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-dlbucketsize", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-dlbucketsize", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "DlRate": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-dlrate", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-dlrate", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "DlRatePolicy": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-dlratepolicy", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-dlratepolicy", "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" }, "DrMax": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-drmax", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-drmax", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "DrMin": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-drmin", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-drmin", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "HrAllowed": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-hrallowed", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-hrallowed", "PrimitiveType": "Boolean", "Required": false, "UpdateType": "Mutable" }, "MinGwDiversity": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-mingwdiversity", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-mingwdiversity", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "NwkGeoLoc": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-nwkgeoloc", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-nwkgeoloc", "PrimitiveType": "Boolean", "Required": false, "UpdateType": "Mutable" }, "PrAllowed": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-prallowed", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-prallowed", "PrimitiveType": "Boolean", "Required": false, "UpdateType": "Mutable" }, "RaAllowed": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-raallowed", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-raallowed", "PrimitiveType": "Boolean", "Required": false, "UpdateType": "Mutable" }, "ReportDevStatusBattery": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-reportdevstatusbattery", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-reportdevstatusbattery", "PrimitiveType": "Boolean", "Required": false, "UpdateType": "Mutable" }, "ReportDevStatusMargin": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-reportdevstatusmargin", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-reportdevstatusmargin", "PrimitiveType": "Boolean", "Required": false, "UpdateType": "Mutable" }, "TargetPer": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-targetper", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-targetper", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "UlBucketSize": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-ulbucketsize", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-ulbucketsize", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "UlRate": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-ulrate", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-ulrate", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "UlRatePolicy": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-ulratepolicy", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-ulratepolicy", "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" } } }, - "AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html", - "Properties": { - "AddGwMetadata": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-addgwmetadata", - "PrimitiveType": "Boolean", - "Required": false, - "UpdateType": "Mutable" - } - } - }, "AWS::IoTWireless::WirelessDevice.AbpV10x": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-wirelessdevice-abpv10x.html", "Properties": { @@ -42925,6 +42977,12 @@ "Required": false, "UpdateType": "Mutable" }, + "OriginationNumber": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-pinpoint-campaign-campaignsmsmessage.html#cfn-pinpoint-campaign-campaignsmsmessage-originationnumber", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "SenderId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-pinpoint-campaign-campaignsmsmessage.html#cfn-pinpoint-campaign-campaignsmsmessage-senderid", "PrimitiveType": "String", @@ -50896,6 +50954,9 @@ } } }, + "AWS::StepFunctions::StateMachine.Definition": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stepfunctions-statemachine-definition.html" + }, "AWS::StepFunctions::StateMachine.LogDestination": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stepfunctions-statemachine-logdestination.html", "Properties": { @@ -53317,7 +53378,7 @@ } } }, - "ResourceSpecificationVersion": "28.0.0", + "ResourceSpecificationVersion": "29.0.0", "ResourceTypes": { "AWS::ACMPCA::Certificate": { "Attributes": { @@ -59664,6 +59725,12 @@ "Type": "ProjectCache", "UpdateType": "Mutable" }, + "ConcurrentBuildLimit": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codebuild-project.html#cfn-codebuild-project-concurrentbuildlimit", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, "Description": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codebuild-project.html#cfn-codebuild-project-description", "PrimitiveType": "String", @@ -62183,6 +62250,12 @@ "Required": false, "UpdateType": "Mutable" }, + "JobSample": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-databrew-job.html#cfn-databrew-job-jobsample", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Mutable" + }, "LogSubscription": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-databrew-job.html#cfn-databrew-job-logsubscription", "PrimitiveType": "String", @@ -63358,6 +63431,12 @@ "Type": "List", "UpdateType": "Immutable" }, + "KinesisStreamSpecification": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html#cfn-dynamodb-table-kinesisstreamspecification", + "Required": false, + "Type": "KinesisStreamSpecification", + "UpdateType": "Mutable" + }, "LocalSecondaryIndexes": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html#cfn-dynamodb-table-lsi", "DuplicatesAllowed": true, @@ -65596,7 +65675,7 @@ "TransitGatewayId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastdomain.html#cfn-ec2-transitgatewaymulticastdomain-transitgatewayid", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" } } @@ -65608,32 +65687,29 @@ }, "ResourceType": { "PrimitiveType": "String" + }, + "State": { + "PrimitiveType": "String" } }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastdomainassociation.html", "Properties": { - "State": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastdomainassociation.html#cfn-ec2-transitgatewaymulticastdomainassociation-state", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, "SubnetId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastdomainassociation.html#cfn-ec2-transitgatewaymulticastdomainassociation-subnetid", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" }, "TransitGatewayAttachmentId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastdomainassociation.html#cfn-ec2-transitgatewaymulticastdomainassociation-transitgatewayattachmentid", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" }, "TransitGatewayMulticastDomainId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastdomainassociation.html#cfn-ec2-transitgatewaymulticastdomainassociation-transitgatewaymulticastdomainid", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" } } @@ -65660,6 +65736,9 @@ }, "SubnetId": { "PrimitiveType": "String" + }, + "TransitGatewayAttachmentId": { + "PrimitiveType": "String" } }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastgroupmember.html", @@ -65667,25 +65746,19 @@ "GroupIpAddress": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastgroupmember.html#cfn-ec2-transitgatewaymulticastgroupmember-groupipaddress", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" }, "NetworkInterfaceId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastgroupmember.html#cfn-ec2-transitgatewaymulticastgroupmember-networkinterfaceid", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" }, - "TransitGatewayAttachmentId": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastgroupmember.html#cfn-ec2-transitgatewaymulticastgroupmember-transitgatewayattachmentid", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, "TransitGatewayMulticastDomainId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastgroupmember.html#cfn-ec2-transitgatewaymulticastgroupmember-transitgatewaymulticastdomainid", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" } } @@ -65712,6 +65785,9 @@ }, "SubnetId": { "PrimitiveType": "String" + }, + "TransitGatewayAttachmentId": { + "PrimitiveType": "String" } }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastgroupsource.html", @@ -65719,25 +65795,19 @@ "GroupIpAddress": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastgroupsource.html#cfn-ec2-transitgatewaymulticastgroupsource-groupipaddress", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" }, "NetworkInterfaceId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastgroupsource.html#cfn-ec2-transitgatewaymulticastgroupsource-networkinterfaceid", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" }, - "TransitGatewayAttachmentId": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastgroupsource.html#cfn-ec2-transitgatewaymulticastgroupsource-transitgatewayattachmentid", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, "TransitGatewayMulticastDomainId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastgroupsource.html#cfn-ec2-transitgatewaymulticastgroupsource-transitgatewaymulticastdomainid", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" } } @@ -67768,6 +67838,12 @@ "Required": false, "UpdateType": "Mutable" }, + "CacheParameterGroupName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticache-globalreplicationgroup.html#cfn-elasticache-globalreplicationgroup-cacheparametergroupname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "EngineVersion": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticache-globalreplicationgroup.html#cfn-elasticache-globalreplicationgroup-engineversion", "PrimitiveType": "String", @@ -68825,6 +68901,12 @@ "Required": false, "UpdateType": "Immutable" }, + "ProtocolVersion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-targetgroup.html#cfn-elasticloadbalancingv2-targetgroup-protocolversion", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-targetgroup.html#cfn-elasticloadbalancingv2-targetgroup-tags", "DuplicatesAllowed": true, @@ -72272,6 +72354,12 @@ }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-image.html", "Properties": { + "ContainerRecipeArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-image.html#cfn-imagebuilder-image-containerrecipearn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "DistributionConfigurationArn": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-image.html#cfn-imagebuilder-image-distributionconfigurationarn", "PrimitiveType": "String", @@ -72287,7 +72375,7 @@ "ImageRecipeArn": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-image.html#cfn-imagebuilder-image-imagerecipearn", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Immutable" }, "ImageTestsConfiguration": { @@ -72319,6 +72407,12 @@ }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html", "Properties": { + "ContainerRecipeArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-containerrecipearn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "Description": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-description", "PrimitiveType": "String", @@ -72340,7 +72434,7 @@ "ImageRecipeArn": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-imagerecipearn", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "ImageTestsConfiguration": { @@ -73103,6 +73197,13 @@ "Required": false, "UpdateType": "Immutable" }, + "LateDataRules": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-dataset.html#cfn-iotanalytics-dataset-latedatarules", + "ItemType": "LateDataRule", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, "RetentionPeriod": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-dataset.html#cfn-iotanalytics-dataset-retentionperiod", "Required": false, @@ -73673,11 +73774,62 @@ "Arn": { "PrimitiveType": "String" }, + "ChannelMask": { + "PrimitiveType": "String" + }, + "DevStatusReqFreq": { + "PrimitiveType": "Integer" + }, + "DlBucketSize": { + "PrimitiveType": "Integer" + }, + "DlRate": { + "PrimitiveType": "Integer" + }, + "DlRatePolicy": { + "PrimitiveType": "String" + }, + "DrMax": { + "PrimitiveType": "Integer" + }, + "DrMin": { + "PrimitiveType": "Integer" + }, + "HrAllowed": { + "PrimitiveType": "Boolean" + }, "Id": { "PrimitiveType": "String" }, - "LoRaWANResponse": { - "Type": "LoRaWANGetServiceProfileInfo" + "MinGwDiversity": { + "PrimitiveType": "Integer" + }, + "NwkGeoLoc": { + "PrimitiveType": "Boolean" + }, + "PrAllowed": { + "PrimitiveType": "Boolean" + }, + "RaAllowed": { + "PrimitiveType": "Boolean" + }, + "ReportDevStatusBattery": { + "PrimitiveType": "Boolean" + }, + "ReportDevStatusMargin": { + "PrimitiveType": "Boolean" + }, + "TargetPer": { + "PrimitiveType": "Integer" + }, + "UlBucketSize": { + "PrimitiveType": "Integer" + }, + "UlRate": { + "PrimitiveType": "Integer" + }, + "UlRatePolicy": { + "PrimitiveType": "String" } }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-serviceprofile.html", @@ -73712,9 +73864,6 @@ "Id": { "PrimitiveType": "String" }, - "ThingArn": { - "PrimitiveType": "String" - }, "ThingName": { "PrimitiveType": "String" } @@ -73759,6 +73908,12 @@ "Type": "List", "UpdateType": "Mutable" }, + "ThingArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-wirelessdevice.html#cfn-iotwireless-wirelessdevice-thingarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "Type": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-wirelessdevice.html#cfn-iotwireless-wirelessdevice-type", "PrimitiveType": "String", @@ -73775,7 +73930,7 @@ "Id": { "PrimitiveType": "String" }, - "ThingArn": { + "ThingName": { "PrimitiveType": "String" } }, @@ -73813,8 +73968,8 @@ "Type": "List", "UpdateType": "Mutable" }, - "ThingName": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-wirelessgateway.html#cfn-iotwireless-wirelessgateway-thingname", + "ThingArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-wirelessgateway.html#cfn-iotwireless-wirelessgateway-thingarn", "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" @@ -85120,6 +85275,12 @@ }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html", "Properties": { + "Definition": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html#cfn-stepfunctions-statemachine-definition", + "Required": false, + "Type": "Definition", + "UpdateType": "Mutable" + }, "DefinitionS3Location": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html#cfn-stepfunctions-statemachine-definitions3location", "Required": false, diff --git a/packages/@aws-cdk/cfnspec/spec-source/cfn-lint/StatefulResources/000.json b/packages/@aws-cdk/cfnspec/spec-source/cfn-lint/StatefulResources/000.json index 25c5e11c890ff..36a17855e22b7 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/cfn-lint/StatefulResources/000.json +++ b/packages/@aws-cdk/cfnspec/spec-source/cfn-lint/StatefulResources/000.json @@ -27,4 +27,3 @@ } } } - diff --git a/packages/@aws-cdk/core/README.md b/packages/@aws-cdk/core/README.md index 0e0b38943a803..714e7139f0807 100644 --- a/packages/@aws-cdk/core/README.md +++ b/packages/@aws-cdk/core/README.md @@ -428,6 +428,7 @@ stack-unique identifier and returns the service token: const serviceToken = CustomResourceProvider.getOrCreate(this, 'Custom::MyCustomResourceType', { codeDirectory: `${__dirname}/my-handler`, runtime: CustomResourceProviderRuntime.NODEJS_12, // currently the only supported runtime + description: "Lambda function created by the custom resource provider", }); new CustomResource(this, 'MyResource', { diff --git a/packages/@aws-cdk/core/lib/asset-staging.ts b/packages/@aws-cdk/core/lib/asset-staging.ts index 66c65e3d14864..6a34bd9b4b1ac 100644 --- a/packages/@aws-cdk/core/lib/asset-staging.ts +++ b/packages/@aws-cdk/core/lib/asset-staging.ts @@ -5,8 +5,8 @@ import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import * as fs from 'fs-extra'; import * as minimatch from 'minimatch'; -import { AssetHashType, AssetOptions } from './assets'; -import { BundlingOptions } from './bundling'; +import { AssetHashType, AssetOptions, FileAssetPackaging } from './assets'; +import { BundlingOptions, BundlingOutput } from './bundling'; import { FileSystem, FingerprintOptions } from './fs'; import { Names } from './names'; import { Cache } from './private/cache'; @@ -17,6 +17,8 @@ import { Stage } from './stage'; // eslint-disable-next-line import { Construct as CoreConstruct } from './construct-compat'; +const ARCHIVE_EXTENSIONS = ['.zip', '.jar']; + /** * A previously staged asset */ @@ -30,6 +32,16 @@ interface StagedAsset { * The hash we used previously */ readonly assetHash: string; + + /** + * The packaging of the asset + */ + readonly packaging: FileAssetPackaging, + + /** + * Whether this asset is an archive + */ + readonly isArchive: boolean; } /** @@ -124,6 +136,16 @@ export class AssetStaging extends CoreConstruct { */ public readonly assetHash: string; + /** + * How this asset should be packaged. + */ + public readonly packaging: FileAssetPackaging; + + /** + * Whether this asset is an archive (zip or jar). + */ + public readonly isArchive: boolean; + private readonly fingerprintOptions: FingerprintOptions; private readonly hashType: AssetHashType; @@ -138,12 +160,20 @@ export class AssetStaging extends CoreConstruct { private readonly cacheKey: string; + private readonly sourceStats: fs.Stats; + constructor(scope: Construct, id: string, props: AssetStagingProps) { super(scope, id); this.sourcePath = path.resolve(props.sourcePath); this.fingerprintOptions = props; + if (!fs.existsSync(this.sourcePath)) { + throw new Error(`Cannot find asset at ${this.sourcePath}`); + } + + this.sourceStats = fs.statSync(this.sourcePath); + const outdir = Stage.of(this)?.assetOutdir; if (!outdir) { throw new Error('unable to determine cloud assembly asset output directory. Assets must be defined indirectly within a "Stage" or an "App" scope'); @@ -192,6 +222,8 @@ export class AssetStaging extends CoreConstruct { this.stagedPath = staged.stagedPath; this.absoluteStagedPath = staged.stagedPath; this.assetHash = staged.assetHash; + this.packaging = staged.packaging; + this.isArchive = staged.isArchive; } /** @@ -248,8 +280,18 @@ export class AssetStaging extends CoreConstruct { ? this.sourcePath : path.resolve(this.assetOutdir, renderAssetFilename(assetHash, path.extname(this.sourcePath))); + if (!this.sourceStats.isDirectory() && !this.sourceStats.isFile()) { + throw new Error(`Asset ${this.sourcePath} is expected to be either a directory or a regular file`); + } + this.stageAsset(this.sourcePath, stagedPath, 'copy'); - return { assetHash, stagedPath }; + + return { + assetHash, + stagedPath, + packaging: this.sourceStats.isDirectory() ? FileAssetPackaging.ZIP_DIRECTORY : FileAssetPackaging.FILE, + isArchive: this.sourceStats.isDirectory() || ARCHIVE_EXTENSIONS.includes(path.extname(this.sourcePath).toLowerCase()), + }; } /** @@ -258,6 +300,10 @@ export class AssetStaging extends CoreConstruct { * Optionally skip, in which case we pretend we did something but we don't really. */ private stageByBundling(bundling: BundlingOptions, skip: boolean): StagedAsset { + if (!this.sourceStats.isDirectory()) { + throw new Error(`Asset ${this.sourcePath} is expected to be a directory when bundling`); + } + if (skip) { // We should have bundled, but didn't to save time. Still pretend to have a hash. // If the asset uses OUTPUT or BUNDLE, we use a CUSTOM hash to avoid fingerprinting @@ -270,6 +316,8 @@ export class AssetStaging extends CoreConstruct { return { assetHash: this.calculateHash(hashType, bundling), stagedPath: this.sourcePath, + packaging: FileAssetPackaging.ZIP_DIRECTORY, + isArchive: true, }; } @@ -281,12 +329,21 @@ export class AssetStaging extends CoreConstruct { const bundleDir = this.determineBundleDir(this.assetOutdir, assetHash); this.bundle(bundling, bundleDir); - // Calculate assetHash afterwards if we still must - assetHash = assetHash ?? this.calculateHash(this.hashType, bundling, bundleDir); - const stagedPath = path.resolve(this.assetOutdir, renderAssetFilename(assetHash)); + // Check bundling output content and determine if we will need to archive + const bundlingOutputType = bundling.outputType ?? BundlingOutput.AUTO_DISCOVER; + const bundledAsset = determineBundledAsset(bundleDir, bundlingOutputType); - this.stageAsset(bundleDir, stagedPath, 'move'); - return { assetHash, stagedPath }; + // Calculate assetHash afterwards if we still must + assetHash = assetHash ?? this.calculateHash(this.hashType, bundling, bundledAsset.path); + const stagedPath = path.resolve(this.assetOutdir, renderAssetFilename(assetHash, bundledAsset.extension)); + + this.stageAsset(bundledAsset.path, stagedPath, 'move'); + return { + assetHash, + stagedPath, + packaging: bundledAsset.packaging, + isArchive: true, // bundling always produces an archive + }; } /** @@ -320,10 +377,9 @@ export class AssetStaging extends CoreConstruct { } // Copy file/directory to staging directory - const stat = fs.statSync(sourcePath); - if (stat.isFile()) { + if (this.sourceStats.isFile()) { fs.copyFileSync(sourcePath, targetPath); - } else if (stat.isDirectory()) { + } else if (this.sourceStats.isDirectory()) { fs.mkdirSync(targetPath); FileSystem.copyDirectory(sourcePath, targetPath, this.fingerprintOptions); } else { @@ -502,3 +558,57 @@ function sortObject(object: { [key: string]: any }): { [key: string]: any } { } return ret; } + +/** + * Returns the single archive file of a directory or undefined + */ +function singleArchiveFile(directory: string): string | undefined { + if (!fs.existsSync(directory)) { + throw new Error(`Directory ${directory} does not exist.`); + } + + if (!fs.statSync(directory).isDirectory()) { + throw new Error(`${directory} is not a directory.`); + } + + const content = fs.readdirSync(directory); + if (content.length === 1) { + const file = path.join(directory, content[0]); + const extension = path.extname(content[0]).toLowerCase(); + if (fs.statSync(file).isFile() && ARCHIVE_EXTENSIONS.includes(extension)) { + return file; + } + } + + return undefined; +} + +interface BundledAsset { + path: string, + packaging: FileAssetPackaging, + extension?: string +} + +/** + * Returns the bundled asset to use based on the content of the bundle directory + * and the type of output. + */ +function determineBundledAsset(bundleDir: string, outputType: BundlingOutput): BundledAsset { + const archiveFile = singleArchiveFile(bundleDir); + + // auto-discover means that if there is an archive file, we take it as the + // bundle, otherwise, we will archive here. + if (outputType === BundlingOutput.AUTO_DISCOVER) { + outputType = archiveFile ? BundlingOutput.ARCHIVED : BundlingOutput.NOT_ARCHIVED; + } + + switch (outputType) { + case BundlingOutput.NOT_ARCHIVED: + return { path: bundleDir, packaging: FileAssetPackaging.ZIP_DIRECTORY }; + case BundlingOutput.ARCHIVED: + if (!archiveFile) { + throw new Error('Bundling output directory is expected to include only a single .zip or .jar file when `output` is set to `ARCHIVED`'); + } + return { path: archiveFile, packaging: FileAssetPackaging.FILE, extension: path.extname(archiveFile) }; + } +} diff --git a/packages/@aws-cdk/core/lib/bundling.ts b/packages/@aws-cdk/core/lib/bundling.ts index b1247fd913ea0..e3c1458aa0ab9 100644 --- a/packages/@aws-cdk/core/lib/bundling.ts +++ b/packages/@aws-cdk/core/lib/bundling.ts @@ -1,5 +1,6 @@ import { spawnSync, SpawnSyncOptions } from 'child_process'; import * as crypto from 'crypto'; +import { isAbsolute, join } from 'path'; import { FileSystem } from './fs'; /** @@ -79,6 +80,41 @@ export interface BundlingOptions { * @experimental */ readonly local?: ILocalBundling; + + /** + * The type of output that this bundling operation is producing. + * + * @default BundlingOutput.AUTO_DISCOVER + * + * @experimental + */ + readonly outputType?: BundlingOutput; +} + +/** + * The type of output that a bundling operation is producing. + * + * @experimental + */ +export enum BundlingOutput { + /** + * The bundling output directory includes a single .zip or .jar file which + * will be used as the final bundle. If the output directory does not + * include exactly a single archive, bundling will fail. + */ + ARCHIVED = 'archived', + + /** + * The bundling output directory contains one or more files which will be + * archived and uploaded as a .zip file to S3. + */ + NOT_ARCHIVED = 'not-archived', + + /** + * If the bundling output directory contains a single archive file (zip or jar) + * it will be used as the bundle output as-is. Otherwise all the files in the bundling output directory will be zipped. + */ + AUTO_DISCOVER = 'auto-discover', } /** @@ -100,6 +136,8 @@ export interface ILocalBundling { /** * A Docker image used for asset bundling + * + * @deprecated use DockerImage */ export class BundlingDockerImage { /** @@ -116,20 +154,24 @@ export class BundlingDockerImage { * * @param path The path to the directory containing the Docker file * @param options Docker build options + * + * @deprecated use DockerImage.fromBuild() */ public static fromAsset(path: string, options: DockerBuildOptions = {}) { const buildArgs = options.buildArgs || {}; + if (options.file && isAbsolute(options.file)) { + throw new Error(`"file" must be relative to the docker build directory. Got ${options.file}`); + } + // Image tag derived from path and build options - const tagHash = crypto.createHash('sha256').update(JSON.stringify({ - path, - ...options, - })).digest('hex'); + const input = JSON.stringify({ path, ...options }); + const tagHash = crypto.createHash('sha256').update(input).digest('hex'); const tag = `cdk-${tagHash}`; const dockerArgs: string[] = [ 'build', '-t', tag, - ...(options.file ? ['-f', options.file] : []), + ...(options.file ? ['-f', join(path, options.file)] : []), ...flatten(Object.entries(buildArgs).map(([k, v]) => ['--build-arg', `${k}=${v}`])), path, ]; @@ -146,7 +188,7 @@ export class BundlingDockerImage { } /** @param image The Docker image */ - private constructor(public readonly image: string, private readonly _imageHash?: string) {} + protected constructor(public readonly image: string, private readonly _imageHash?: string) {} /** * Provides a stable representation of this image for JSON serialization. @@ -194,10 +236,16 @@ export class BundlingDockerImage { } /** - * Copies a file or directory out of the Docker image to the local filesystem + * Copies a file or directory out of the Docker image to the local filesystem. + * + * If `outputPath` is omitted the destination path is a temporary directory. + * + * @param imagePath the path in the Docker image + * @param outputPath the destination path for the copy operation + * @returns the destination path */ - public cp(imagePath: string, outputPath: string) { - const { stdout } = dockerExec(['create', this.image]); + public cp(imagePath: string, outputPath?: string): string { + const { stdout } = dockerExec(['create', this.image], {}); // Empty options to avoid stdout redirect here const match = stdout.toString().match(/([0-9a-f]{16,})/); if (!match) { throw new Error('Failed to extract container ID from Docker create output'); @@ -205,16 +253,33 @@ export class BundlingDockerImage { const containerId = match[1]; const containerPath = `${containerId}:${imagePath}`; + const destPath = outputPath ?? FileSystem.mkdtemp('cdk-docker-cp-'); try { - dockerExec(['cp', containerPath, outputPath]); + dockerExec(['cp', containerPath, destPath]); + return destPath; } catch (err) { - throw new Error(`Failed to copy files from ${containerPath} to ${outputPath}: ${err}`); + throw new Error(`Failed to copy files from ${containerPath} to ${destPath}: ${err}`); } finally { dockerExec(['rm', '-v', containerId]); } } } +/** + * A Docker image + */ +export class DockerImage extends BundlingDockerImage { + /** + * Builds a Docker image + * + * @param path The path to the directory containing the Docker file + * @param options Docker build options + */ + public static fromBuild(path: string, options: DockerBuildOptions = {}) { + return BundlingDockerImage.fromAsset(path, options); + } +} + /** * A Docker volume */ @@ -315,9 +380,9 @@ export interface DockerBuildOptions { readonly buildArgs?: { [key: string]: string }; /** - * Name of the Dockerfile + * Name of the Dockerfile, must relative to the docker build path. * - * @default - The Dockerfile immediately within the build context path + * @default `Dockerfile` */ readonly file?: string; } diff --git a/packages/@aws-cdk/core/lib/cfn-json.ts b/packages/@aws-cdk/core/lib/cfn-json.ts index 0e5801ed8ade9..df6bc40ef9f9e 100644 --- a/packages/@aws-cdk/core/lib/cfn-json.ts +++ b/packages/@aws-cdk/core/lib/cfn-json.ts @@ -43,7 +43,7 @@ export class CfnJson extends CoreConstruct implements IResolvable { * Normally there is no need to use this property since `CfnJson` is an * IResolvable, so it can be simply used as a value. */ - private readonly value: Reference; + public readonly value: Reference; private readonly jsonString: string; diff --git a/packages/@aws-cdk/core/lib/custom-resource-provider/custom-resource-provider.ts b/packages/@aws-cdk/core/lib/custom-resource-provider/custom-resource-provider.ts index f7905fc51447b..d6b0a2db982c7 100644 --- a/packages/@aws-cdk/core/lib/custom-resource-provider/custom-resource-provider.ts +++ b/packages/@aws-cdk/core/lib/custom-resource-provider/custom-resource-provider.ts @@ -67,6 +67,13 @@ export interface CustomResourceProviderProps { * @default - No environment variables. */ readonly environment?: { [key: string]: string }; + + /** + * A description of the function. + * + * @default - No description. + */ + readonly description?: string; } /** @@ -78,7 +85,7 @@ export enum CustomResourceProviderRuntime { /** * Node.js 12.x */ - NODEJS_12 = 'nodejs12' + NODEJS_12 = 'nodejs12.x' } /** @@ -203,8 +210,9 @@ export class CustomResourceProvider extends CoreConstruct { MemorySize: memory.toMebibytes(), Handler: `${ENTRYPOINT_FILENAME}.handler`, Role: role.getAtt('Arn'), - Runtime: 'nodejs12.x', + Runtime: props.runtime, Environment: this.renderEnvironmentVariables(props.environment), + Description: props.description ?? undefined, }, }); diff --git a/packages/@aws-cdk/core/test/archive/archive.zip b/packages/@aws-cdk/core/test/archive/archive.zip new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/@aws-cdk/core/test/bundling.test.ts b/packages/@aws-cdk/core/test/bundling.test.ts index 258860d65585c..99548030c011a 100644 --- a/packages/@aws-cdk/core/test/bundling.test.ts +++ b/packages/@aws-cdk/core/test/bundling.test.ts @@ -3,7 +3,7 @@ import * as crypto from 'crypto'; import * as path from 'path'; import { nodeunitShim, Test } from 'nodeunit-shim'; import * as sinon from 'sinon'; -import { BundlingDockerImage, FileSystem } from '../lib'; +import { BundlingDockerImage, DockerImage, FileSystem } from '../lib'; nodeunitShim({ 'tearDown'(callback: any) { @@ -161,12 +161,14 @@ nodeunitShim({ signal: null, }); - BundlingDockerImage.fromAsset(path.join(__dirname, 'fs/fixtures/test1'), { + const imagePath = path.join(__dirname, 'fs/fixtures/test1'); + BundlingDockerImage.fromAsset(imagePath, { file: 'my-dockerfile', }); test.ok(spawnSyncStub.calledOnce); - test.ok(/-f my-dockerfile/.test(spawnSyncStub.firstCall.args[1]?.join(' ') ?? '')); + const expected = path.join(imagePath, 'my-dockerfile'); + test.ok(new RegExp(`-f ${expected}`).test(spawnSyncStub.firstCall.args[1]?.join(' ') ?? '')); test.done(); }, @@ -263,4 +265,25 @@ nodeunitShim({ test.ok(spawnSyncStub.calledWith(sinon.match.any, ['rm', '-v', containerId])); test.done(); }, + + 'cp utility copies to a temp dir of outputPath is omitted'(test: Test) { + // GIVEN + const containerId = '1234567890abcdef1234567890abcdef'; + sinon.stub(child_process, 'spawnSync').returns({ + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from(`${containerId}\n`), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }); + + // WHEN + const tempPath = DockerImage.fromRegistry('alpine').cp('/foo/bar'); + + // THEN + test.ok(/cdk-docker-cp-/.test(tempPath)); + + test.done(); + }, }); diff --git a/packages/@aws-cdk/core/test/custom-resource-provider/custom-resource-provider.test.ts b/packages/@aws-cdk/core/test/custom-resource-provider/custom-resource-provider.test.ts index b6c1e608e2f59..594f9c2936ff1 100644 --- a/packages/@aws-cdk/core/test/custom-resource-provider/custom-resource-provider.test.ts +++ b/packages/@aws-cdk/core/test/custom-resource-provider/custom-resource-provider.test.ts @@ -187,7 +187,7 @@ nodeunitShim({ test.done(); }, - 'memorySize and timeout'(test: Test) { + 'memorySize, timeout and description'(test: Test) { // GIVEN const stack = new Stack(); @@ -197,6 +197,7 @@ nodeunitShim({ runtime: CustomResourceProviderRuntime.NODEJS_12, memorySize: Size.gibibytes(2), timeout: Duration.minutes(5), + description: 'veni vidi vici', }); // THEN @@ -204,6 +205,7 @@ nodeunitShim({ const lambda = template.Resources.CustomMyResourceTypeCustomResourceProviderHandler29FBDD2A; test.deepEqual(lambda.Properties.MemorySize, 2048); test.deepEqual(lambda.Properties.Timeout, 300); + test.deepEqual(lambda.Properties.Description, 'veni vidi vici'); test.done(); }, diff --git a/packages/@aws-cdk/core/test/docker-stub.sh b/packages/@aws-cdk/core/test/docker-stub.sh index fe48e93d4a207..94f806f69a120 100755 --- a/packages/@aws-cdk/core/test/docker-stub.sh +++ b/packages/@aws-cdk/core/test/docker-stub.sh @@ -24,5 +24,18 @@ if echo "$@" | grep "DOCKER_STUB_SUCCESS"; then exit 0 fi -echo "Docker mock only supports one of the following commands: DOCKER_STUB_SUCCESS_NO_OUTPUT,DOCKER_STUB_FAIL,DOCKER_STUB_SUCCESS" +if echo "$@" | grep "DOCKER_STUB_MULTIPLE_FILES"; then + outdir=$(echo "$@" | xargs -n1 | grep "/asset-output" | head -n1 | cut -d":" -f1) + touch ${outdir}/test1.txt + touch ${outdir}/test2.txt + exit 0 +fi + +if echo "$@" | grep "DOCKER_STUB_SINGLE_ARCHIVE"; then + outdir=$(echo "$@" | xargs -n1 | grep "/asset-output" | head -n1 | cut -d":" -f1) + touch ${outdir}/test.zip + exit 0 +fi + +echo "Docker mock only supports one of the following commands: DOCKER_STUB_SUCCESS_NO_OUTPUT,DOCKER_STUB_FAIL,DOCKER_STUB_SUCCESS,DOCKER_STUB_MULTIPLE_FILES,DOCKER_SINGLE_ARCHIVE" exit 1 diff --git a/packages/@aws-cdk/core/test/staging.test.ts b/packages/@aws-cdk/core/test/staging.test.ts index 347c5fcea3b63..ee87780a0957e 100644 --- a/packages/@aws-cdk/core/test/staging.test.ts +++ b/packages/@aws-cdk/core/test/staging.test.ts @@ -1,10 +1,11 @@ import * as os from 'os'; import * as path from 'path'; +import { FileAssetPackaging } from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs-extra'; import { nodeunitShim, Test } from 'nodeunit-shim'; import * as sinon from 'sinon'; -import { App, AssetHashType, AssetStaging, BundlingDockerImage, BundlingOptions, FileSystem, Stack, Stage } from '../lib'; +import { App, AssetHashType, AssetStaging, BundlingDockerImage, BundlingOptions, BundlingOutput, FileSystem, Stack, Stage } from '../lib'; const STUB_INPUT_FILE = '/tmp/docker-stub.input'; const STUB_INPUT_CONCAT_FILE = '/tmp/docker-stub.input.concat'; @@ -12,7 +13,9 @@ const STUB_INPUT_CONCAT_FILE = '/tmp/docker-stub.input.concat'; enum DockerStubCommand { SUCCESS = 'DOCKER_STUB_SUCCESS', FAIL = 'DOCKER_STUB_FAIL', - SUCCESS_NO_OUTPUT = 'DOCKER_STUB_SUCCESS_NO_OUTPUT' + SUCCESS_NO_OUTPUT = 'DOCKER_STUB_SUCCESS_NO_OUTPUT', + MULTIPLE_FILES = 'DOCKER_STUB_MULTIPLE_FILES', + SINGLE_ARCHIVE = 'DOCKER_STUB_SINGLE_ARCHIVE', } const FIXTURE_TEST1_DIR = path.join(__dirname, 'fs', 'fixtures', 'test1'); @@ -50,6 +53,84 @@ nodeunitShim({ test.deepEqual(staging.sourcePath, sourcePath); test.deepEqual(path.basename(staging.stagedPath), 'asset.2f37f937c51e2c191af66acf9b09f548926008ec68c575bd2ee54b6e997c0e00'); test.deepEqual(path.basename(staging.relativeStagedPath(stack)), 'asset.2f37f937c51e2c191af66acf9b09f548926008ec68c575bd2ee54b6e997c0e00'); + test.deepEqual(staging.packaging, FileAssetPackaging.ZIP_DIRECTORY); + test.deepEqual(staging.isArchive, true); + test.done(); + }, + + 'staging of an archive file correctly sets packaging and isArchive'(test: Test) { + // GIVEN + const stack = new Stack(); + const sourcePath = path.join(__dirname, 'archive', 'archive.zip'); + + // WHEN + const staging = new AssetStaging(stack, 's1', { sourcePath }); + + test.deepEqual(staging.packaging, FileAssetPackaging.FILE); + test.deepEqual(staging.isArchive, true); + test.done(); + }, + + 'asset packaging type is correct when staging is skipped because of memory cache'(test: Test) { + // GIVEN + const stack = new Stack(); + const sourcePath = path.join(__dirname, 'archive', 'archive.zip'); + + // WHEN + const staging1 = new AssetStaging(stack, 's1', { sourcePath }); + const staging2 = new AssetStaging(stack, 's2', { sourcePath }); + + test.deepEqual(staging1.packaging, FileAssetPackaging.FILE); + test.deepEqual(staging1.isArchive, true); + test.deepEqual(staging2.packaging, staging1.packaging); + test.deepEqual(staging2.isArchive, staging1.isArchive); + test.done(); + }, + + 'asset packaging type is correct when staging is skipped because of disk cache'(test: Test) { + // GIVEN + const TEST_OUTDIR = path.join(__dirname, 'cdk.out'); + if (fs.existsSync(TEST_OUTDIR)) { + fs.removeSync(TEST_OUTDIR); + } + + const sourcePath = path.join(__dirname, 'archive', 'archive.zip'); + + const app1 = new App({ outdir: TEST_OUTDIR }); + const stack1 = new Stack(app1, 'Stack'); + + const app2 = new App({ outdir: TEST_OUTDIR }); // same OUTDIR + const stack2 = new Stack(app2, 'stack'); + + // WHEN + const staging1 = new AssetStaging(stack1, 'Asset', { sourcePath }); + + // Now clear asset hash cache to show that during the second staging + // even though the asset is already available on disk it will correctly + // be considered as a FileAssetPackaging.FILE. + AssetStaging.clearAssetHashCache(); + + const staging2 = new AssetStaging(stack2, 'Asset', { sourcePath }); + + // THEN + test.deepEqual(staging1.packaging, FileAssetPackaging.FILE); + test.deepEqual(staging1.isArchive, true); + test.deepEqual(staging2.packaging, staging1.packaging); + test.deepEqual(staging2.isArchive, staging1.isArchive); + + test.done(); + }, + + 'staging of a non-archive file correctly sets packaging and isArchive'(test: Test) { + // GIVEN + const stack = new Stack(); + const sourcePath = __filename; + + // WHEN + const staging = new AssetStaging(stack, 's1', { sourcePath }); + + test.deepEqual(staging.packaging, FileAssetPackaging.FILE); + test.deepEqual(staging.isArchive, false); test.done(); }, @@ -785,6 +866,89 @@ nodeunitShim({ ); test.equal(asset.assetHash, '33cbf2cae5432438e0f046bc45ba8c3cef7b6afcf47b59d1c183775c1918fb1f'); // hash of MyStack/Asset + test.done(); + }, + + 'bundling that produces a single archive file is autodiscovered'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN + const staging = new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.SINGLE_ARCHIVE], + }, + }); + + // THEN + const assembly = app.synth(); + test.deepEqual(fs.readdirSync(assembly.directory), [ + 'asset.f43148c61174f444925231b5849b468f21e93b5d1469cd07c53625ffd039ef48', // this is the bundle dir but it's empty + 'asset.f43148c61174f444925231b5849b468f21e93b5d1469cd07c53625ffd039ef48.zip', + 'cdk.out', + 'manifest.json', + 'stack.template.json', + 'tree.json', + ]); + test.equal(fs.readdirSync(path.join(assembly.directory, 'asset.f43148c61174f444925231b5849b468f21e93b5d1469cd07c53625ffd039ef48')).length, 0); // empty bundle dir + test.deepEqual(staging.packaging, FileAssetPackaging.FILE); + test.deepEqual(staging.isArchive, true); + + test.done(); + }, + + 'bundling that produces a single archive file with NOT_ARCHIVED'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN + const staging = new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.SINGLE_ARCHIVE], + outputType: BundlingOutput.NOT_ARCHIVED, + }, + }); + + // THEN + const assembly = app.synth(); + test.deepEqual(fs.readdirSync(assembly.directory), [ + 'asset.86ec07746e1d859290cfd8b9c648e581555649c75f51f741f11e22cab6775abc', + 'cdk.out', + 'manifest.json', + 'stack.template.json', + 'tree.json', + ]); + test.deepEqual(staging.packaging, FileAssetPackaging.ZIP_DIRECTORY); + test.deepEqual(staging.isArchive, true); + + test.done(); + }, + + 'throws with ARCHIVED and bundling that does not produce a single archive file'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN + test.throws(() => new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.MULTIPLE_FILES], + outputType: BundlingOutput.ARCHIVED, + }, + }), /Bundling output directory is expected to include only a single .zip or .jar file when `output` is set to `ARCHIVED`/); + + test.done(); }, }); diff --git a/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts index 3a92062b779cb..bc86cea1c9d43 100644 --- a/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts +++ b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts @@ -5,7 +5,7 @@ import * as lambda from '@aws-cdk/aws-lambda'; import * as logs from '@aws-cdk/aws-logs'; import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { PHYSICAL_RESOURCE_ID_REFERENCE, flatten } from './runtime'; +import { PHYSICAL_RESOURCE_ID_REFERENCE } from './runtime'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main // eslint-disable-next-line no-duplicate-imports, import/order @@ -14,13 +14,23 @@ import { Construct as CoreConstruct } from '@aws-cdk/core'; /** * Reference to the physical resource id that can be passed to the AWS operation as a parameter. */ -export class PhysicalResourceIdReference { +export class PhysicalResourceIdReference implements cdk.IResolvable { + public readonly creationStack: string[] = cdk.captureStackTrace(); + /** * toJSON serialization to replace `PhysicalResourceIdReference` with a magic string. */ public toJSON() { return PHYSICAL_RESOURCE_ID_REFERENCE; } + + public resolve(_: cdk.IResolveContext): any { + return PHYSICAL_RESOURCE_ID_REFERENCE; + } + + public toString(): string { + return PHYSICAL_RESOURCE_ID_REFERENCE; + } } /** @@ -316,13 +326,8 @@ export class AwsCustomResource extends CoreConstruct implements iam.IGrantable { } } - if (props.onCreate?.parameters) { - const flattenedOnCreateParams = flatten(JSON.parse(JSON.stringify(props.onCreate.parameters))); - for (const param in flattenedOnCreateParams) { - if (flattenedOnCreateParams[param] === PHYSICAL_RESOURCE_ID_REFERENCE) { - throw new Error('`PhysicalResourceIdReference` must not be specified in `onCreate` parameters.'); - } - } + if (includesPhysicalResourceIdRef(props.onCreate?.parameters)) { + throw new Error('`PhysicalResourceIdReference` must not be specified in `onCreate` parameters.'); } this.props = props; @@ -371,9 +376,9 @@ export class AwsCustomResource extends CoreConstruct implements iam.IGrantable { serviceToken: provider.functionArn, pascalCaseProperties: true, properties: { - create: create && encodeBooleans(create), - update: props.onUpdate && encodeBooleans(props.onUpdate), - delete: props.onDelete && encodeBooleans(props.onDelete), + create: create && this.encodeJson(create), + update: props.onUpdate && this.encodeJson(props.onUpdate), + delete: props.onDelete && this.encodeJson(props.onDelete), installLatestAwsSdk: props.installLatestAwsSdk ?? true, }, }); @@ -418,6 +423,9 @@ export class AwsCustomResource extends CoreConstruct implements iam.IGrantable { return this.customResource.getAttString(dataPath); } + private encodeJson(obj: any) { + return cdk.Lazy.uncachedString({ produce: () => cdk.Stack.of(this).toJsonString(obj) }); + } } /** @@ -439,6 +447,30 @@ let getAwsSdkMetadata = (() => { }; })(); +/** + * Returns true if `obj` includes a `PhysicalResourceIdReference` in one of the + * values. + * @param obj Any object. + */ +function includesPhysicalResourceIdRef(obj: any | undefined) { + if (obj === undefined) { + return false; + } + + let foundRef = false; + + // we use JSON.stringify as a way to traverse all values in the object. + JSON.stringify(obj, (_, v) => { + if (v === PHYSICAL_RESOURCE_ID_REFERENCE) { + foundRef = true; + } + + return v; + }); + + return foundRef; +} + /** * Transform SDK service/action to IAM action using metadata from aws-sdk module. * Example: CloudWatchLogs with putRetentionPolicy => logs:PutRetentionPolicy @@ -452,19 +484,3 @@ function awsSdkToIamAction(service: string, action: string): string { const iamAction = action.charAt(0).toUpperCase() + action.slice(1); return `${iamService}:${iamAction}`; } - -/** - * Encodes booleans as special strings - */ -function encodeBooleans(object: object) { - return JSON.parse(JSON.stringify(object), (_k, v) => { - switch (v) { - case true: - return 'TRUE:BOOLEAN'; - case false: - return 'FALSE:BOOLEAN'; - default: - return v; - } - }); -} diff --git a/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/index.ts b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/index.ts index 9f90c8f629f74..5fafd6db214e2 100644 --- a/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/index.ts +++ b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/index.ts @@ -13,7 +13,7 @@ export const PHYSICAL_RESOURCE_ID_REFERENCE = 'PHYSICAL:RESOURCEID:'; * @param object the object to be flattened * @returns a flat object with path as keys */ -export function flatten(object: object): { [key: string]: string } { +export function flatten(object: object): { [key: string]: any } { return Object.assign( {}, ...function _flatten(child: any, path: string[] = []): any { @@ -29,15 +29,11 @@ export function flatten(object: object): { [key: string]: string } { } /** - * Decodes encoded special values (booleans and physicalResourceId) + * Decodes encoded special values (physicalResourceId) */ function decodeSpecialValues(object: object, physicalResourceId: string) { return JSON.parse(JSON.stringify(object), (_k, v) => { switch (v) { - case 'TRUE:BOOLEAN': - return true; - case 'FALSE:BOOLEAN': - return false; case PHYSICAL_RESOURCE_ID_REFERENCE: return physicalResourceId; default: @@ -96,6 +92,9 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent console.log(JSON.stringify(event)); console.log('AWS SDK VERSION: ' + AWS.VERSION); + event.ResourceProperties.Create = decodeCall(event.ResourceProperties.Create); + event.ResourceProperties.Update = decodeCall(event.ResourceProperties.Update); + event.ResourceProperties.Delete = decodeCall(event.ResourceProperties.Delete); // Default physical resource id let physicalResourceId: string; switch (event.RequestType) { @@ -185,3 +184,8 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent }); } } + +function decodeCall(call: string | undefined) { + if (!call) { return undefined; } + return JSON.parse(call); +} \ No newline at end of file diff --git a/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource-provider.test.ts b/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource-provider.test.ts index 0a7e5ec220c28..66f4fbb5dfc08 100644 --- a/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource-provider.test.ts +++ b/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource-provider.test.ts @@ -60,14 +60,14 @@ test('create event with physical resource id path', async () => { RequestType: 'Create', ResourceProperties: { ServiceToken: 'token', - Create: { + Create: JSON.stringify({ service: 'S3', action: 'listObjects', parameters: { Bucket: 'my-bucket', }, physicalResourceId: PhysicalResourceId.fromResponse('Contents.1.ETag'), - } as AwsSdkCall, + } as AwsSdkCall), }, }; @@ -98,7 +98,7 @@ test('update event with physical resource id', async () => { OldResourceProperties: {}, ResourceProperties: { ServiceToken: 'token', - Update: { + Update: JSON.stringify({ service: 'SNS', action: 'publish', parameters: { @@ -106,7 +106,7 @@ test('update event with physical resource id', async () => { TopicArn: 'topicarn', }, physicalResourceId: PhysicalResourceId.of('topicarn'), - } as AwsSdkCall, + } as AwsSdkCall), }, }; @@ -131,14 +131,14 @@ test('delete event', async () => { PhysicalResourceId: 'physicalResourceId', ResourceProperties: { ServiceToken: 'token', - Create: { + Create: JSON.stringify({ service: 'S3', action: 'listObjects', parameters: { Bucket: 'my-bucket', }, physicalResourceId: PhysicalResourceId.fromResponse('Contents.1.ETag'), - } as AwsSdkCall, + } as AwsSdkCall), }, }; @@ -166,13 +166,13 @@ test('delete event with Delete call and no physical resource id in call', async PhysicalResourceId: 'physicalResourceId', ResourceProperties: { ServiceToken: 'token', - Delete: { + Delete: JSON.stringify({ service: 'SSM', action: 'deleteParameter', parameters: { Name: 'my-param', }, - } as AwsSdkCall, + } as AwsSdkCall), }, }; @@ -200,13 +200,13 @@ test('create event with Delete call only', async () => { RequestType: 'Create', ResourceProperties: { ServiceToken: 'token', - Delete: { + Delete: JSON.stringify({ service: 'SSM', action: 'deleteParameter', parameters: { Name: 'my-param', }, - } as AwsSdkCall, + } as AwsSdkCall), }, }; @@ -234,7 +234,7 @@ test('catch errors', async () => { RequestType: 'Create', ResourceProperties: { ServiceToken: 'token', - Create: { + Create: JSON.stringify({ service: 'S3', action: 'listObjects', parameters: { @@ -242,7 +242,7 @@ test('catch errors', async () => { }, physicalResourceId: PhysicalResourceId.of('physicalResourceId'), ignoreErrorCodesMatching: 'NoSuchBucket', - } as AwsSdkCall, + } as AwsSdkCall), }, }; @@ -257,68 +257,6 @@ test('catch errors', async () => { expect(request.isDone()).toBeTruthy(); }); -test('decodes booleans', async () => { - const putItemFake = sinon.fake.resolves({}); - - AWS.mock('DynamoDB', 'putItem', putItemFake); - - const event: AWSLambda.CloudFormationCustomResourceCreateEvent = { - ...eventCommon, - RequestType: 'Create', - ResourceProperties: { - ServiceToken: 'token', - Create: { - service: 'DynamoDB', - action: 'putItem', - parameters: { - TableName: 'table', - Item: { - True: { - BOOL: 'TRUE:BOOLEAN', - }, - TrueString: { - S: 'true', - }, - False: { - BOOL: 'FALSE:BOOLEAN', - }, - FalseString: { - S: 'false', - }, - }, - }, - physicalResourceId: PhysicalResourceId.of('put-item'), - } as AwsSdkCall, - }, - }; - - const request = createRequest(body => - body.Status === 'SUCCESS', - ); - - await handler(event, {} as AWSLambda.Context); - - sinon.assert.calledWith(putItemFake, { - TableName: 'table', - Item: { - True: { - BOOL: true, - }, - TrueString: { - S: 'true', - }, - False: { - BOOL: false, - }, - FalseString: { - S: 'false', - }, - }, - }); - - expect(request.isDone()).toBeTruthy(); -}); - test('restrict output path', async () => { const listObjectsFake = sinon.fake.resolves({ Contents: [ @@ -340,7 +278,7 @@ test('restrict output path', async () => { RequestType: 'Create', ResourceProperties: { ServiceToken: 'token', - Create: { + Create: JSON.stringify({ service: 'S3', action: 'listObjects', parameters: { @@ -348,7 +286,7 @@ test('restrict output path', async () => { }, physicalResourceId: PhysicalResourceId.of('id'), outputPath: 'Contents.0', - } as AwsSdkCall, + } as AwsSdkCall), }, }; @@ -374,7 +312,7 @@ test('can specify apiVersion and region', async () => { RequestType: 'Create', ResourceProperties: { ServiceToken: 'token', - Create: { + Create: JSON.stringify({ service: 'SNS', action: 'publish', parameters: { @@ -384,7 +322,7 @@ test('can specify apiVersion and region', async () => { apiVersion: '2010-03-31', region: 'eu-west-1', physicalResourceId: PhysicalResourceId.of('id'), - } as AwsSdkCall, + } as AwsSdkCall), }, }; @@ -454,7 +392,7 @@ test('installs the latest SDK', async () => { RequestType: 'Create', ResourceProperties: { ServiceToken: 'token', - Create: { + Create: JSON.stringify({ service: 'SNS', action: 'publish', parameters: { @@ -462,7 +400,7 @@ test('installs the latest SDK', async () => { TopicArn: 'topic', }, physicalResourceId: PhysicalResourceId.of('id'), - } as AwsSdkCall, + } as AwsSdkCall), InstallLatestAwsSdk: 'true', }, }; diff --git a/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource.test.ts b/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource.test.ts index 8bf6a667f5c83..086ef301ea41c 100644 --- a/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource.test.ts +++ b/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource.test.ts @@ -34,7 +34,7 @@ test('aws sdk js custom resource with onCreate and onDelete', () => { // THEN expect(stack).toHaveResource('Custom::LogRetentionPolicy', { - 'Create': { + 'Create': JSON.stringify({ 'service': 'CloudWatchLogs', 'action': 'putRetentionPolicy', 'parameters': { @@ -44,14 +44,14 @@ test('aws sdk js custom resource with onCreate and onDelete', () => { 'physicalResourceId': { 'id': 'loggroup', }, - }, - 'Delete': { + }), + 'Delete': JSON.stringify({ 'service': 'CloudWatchLogs', 'action': 'deleteRetentionPolicy', 'parameters': { 'logGroupName': '/aws/lambda/loggroup', }, - }, + }), 'InstallLatestAwsSdk': true, }); @@ -96,7 +96,7 @@ test('onCreate defaults to onUpdate', () => { // THEN expect(stack).toHaveResource('Custom::S3PutObject', { - 'Create': { + 'Create': JSON.stringify({ 'service': 's3', 'action': 'putObject', 'parameters': { @@ -107,8 +107,8 @@ test('onCreate defaults to onUpdate', () => { 'physicalResourceId': { 'responsePath': 'ETag', }, - }, - 'Update': { + }), + 'Update': JSON.stringify({ 'service': 's3', 'action': 'putObject', 'parameters': { @@ -119,7 +119,7 @@ test('onCreate defaults to onUpdate', () => { 'physicalResourceId': { 'responsePath': 'ETag', }, - }, + }), }); }); @@ -185,7 +185,7 @@ test('fails when no physical resource method is specified', () => { })).toThrow(/`physicalResourceId`/); }); -test('encodes booleans', () => { +test('booleans are encoded in the stringified parameters object', () => { // GIVEN const stack = new cdk.Stack(); @@ -208,19 +208,19 @@ test('encodes booleans', () => { // THEN expect(stack).toHaveResource('Custom::ServiceAction', { - 'Create': { + 'Create': JSON.stringify({ 'service': 'service', 'action': 'action', 'parameters': { - 'trueBoolean': 'TRUE:BOOLEAN', + 'trueBoolean': true, 'trueString': 'true', - 'falseBoolean': 'FALSE:BOOLEAN', + 'falseBoolean': false, 'falseString': 'false', }, 'physicalResourceId': { 'id': 'id', }, - }, + }), }); }); @@ -264,20 +264,20 @@ test('encodes physical resource id reference', () => { // THEN expect(stack).toHaveResource('Custom::ServiceAction', { - 'Create': { + 'Create': JSON.stringify({ 'service': 'service', 'action': 'action', 'parameters': { - 'trueBoolean': 'TRUE:BOOLEAN', + 'trueBoolean': true, 'trueString': 'true', - 'falseBoolean': 'FALSE:BOOLEAN', + 'falseBoolean': false, 'falseString': 'false', 'physicalResourceIdReference': 'PHYSICAL:RESOURCEID:', }, 'physicalResourceId': { 'id': 'id', }, - }, + }), }); }); @@ -526,19 +526,19 @@ test('getDataString', () => { // THEN expect(stack).toHaveResource('Custom::AWS', { Create: { - service: 'service', - action: 'action', - parameters: { - a: { - 'Fn::GetAtt': [ - 'AwsSdk155B91071', - 'Data', - ], - }, - }, - physicalResourceId: { - 'id': 'id', - }, + 'Fn::Join': [ + '', + [ + '{"service":"service","action":"action","parameters":{"a":"', + { + 'Fn::GetAtt': [ + 'AwsSdk155B91071', + 'Data', + ], + }, + '"},"physicalResourceId":{"id":"id"}}', + ], + ], }, }); }); @@ -665,3 +665,50 @@ test('separate policies per custom resource', () => { }, }); }); + +test('tokens can be used as dictionary keys', () => { + // GIVEN + const stack = new cdk.Stack(); + const dummy = new cdk.CfnResource(stack, 'MyResource', { + type: 'AWS::My::Resource', + }); + + // WHEN + new AwsCustomResource(stack, 'Custom1', { + onCreate: { + service: 'service1', + action: 'action1', + physicalResourceId: PhysicalResourceId.of('id1'), + parameters: { + [dummy.ref]: { + Foo: 1234, + Bar: dummy.getAtt('Foorz'), + }, + }, + }, + policy: AwsCustomResourcePolicy.fromSdkCalls({ resources: AwsCustomResourcePolicy.ANY_RESOURCE }), + }); + + // THEN + expect(stack).toHaveResource('Custom::AWS', { + Create: { + 'Fn::Join': [ + '', + [ + '{"service":"service1","action":"action1","physicalResourceId":{"id":"id1"},"parameters":{"', + { + 'Ref': 'MyResource', + }, + '":{"Foo":1234,"Bar":"', + { + 'Fn::GetAtt': [ + 'MyResource', + 'Foorz', + ], + }, + '"}}}', + ], + ], + }, + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/custom-resources/test/aws-custom-resource/integ.aws-custom-resource.expected.json b/packages/@aws-cdk/custom-resources/test/aws-custom-resource/integ.aws-custom-resource.expected.json index 81da053efe11d..2cad60974266d 100644 --- a/packages/@aws-cdk/custom-resources/test/aws-custom-resource/integ.aws-custom-resource.expected.json +++ b/packages/@aws-cdk/custom-resources/test/aws-custom-resource/integ.aws-custom-resource.expected.json @@ -3,6 +3,27 @@ "TopicBFC7AF6E": { "Type": "AWS::SNS::Topic" }, + "PublishCustomResourcePolicyDF696FCA": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PublishCustomResourcePolicyDF696FCA", + "Roles": [ + { + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + } + ] + } + }, "Publish2E9BDF73": { "Type": "Custom::SNSPublisher", "Properties": { @@ -13,42 +34,44 @@ ] }, "Create": { - "service": "SNS", - "action": "publish", - "parameters": { - "Message": "hello", - "TopicArn": { - "Ref": "TopicBFC7AF6E" - } - }, - "physicalResourceId": { - "id": { - "Ref": "TopicBFC7AF6E" - } - } + "Fn::Join": [ + "", + [ + "{\"service\":\"SNS\",\"action\":\"publish\",\"parameters\":{\"Message\":\"hello\",\"TopicArn\":\"", + { + "Ref": "TopicBFC7AF6E" + }, + "\"},\"physicalResourceId\":{\"id\":\"", + { + "Ref": "TopicBFC7AF6E" + }, + "\"}}" + ] + ] }, "Update": { - "service": "SNS", - "action": "publish", - "parameters": { - "Message": "hello", - "TopicArn": { - "Ref": "TopicBFC7AF6E" - } - }, - "physicalResourceId": { - "id": { - "Ref": "TopicBFC7AF6E" - } - } + "Fn::Join": [ + "", + [ + "{\"service\":\"SNS\",\"action\":\"publish\",\"parameters\":{\"Message\":\"hello\",\"TopicArn\":\"", + { + "Ref": "TopicBFC7AF6E" + }, + "\"},\"physicalResourceId\":{\"id\":\"", + { + "Ref": "TopicBFC7AF6E" + }, + "\"}}" + ] + ] }, "InstallLatestAwsSdk": true }, - "UpdateReplacePolicy": "Delete", - "DeletionPolicy": "Delete", "DependsOn": [ "PublishCustomResourcePolicyDF696FCA" - ] + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" }, "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2": { "Type": "AWS::IAM::Role", @@ -86,7 +109,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters0fb8d27cb9854a09fcc93f73882cc5df12ac4108ab02581ab449da4df345f68bS3BucketA4761502" + "Ref": "AssetParametersbd060cb930079c194320bc9a045d159066215c3a4858c45bdb12a79ef9a1edbaS3BucketACF45CC2" }, "S3Key": { "Fn::Join": [ @@ -99,7 +122,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters0fb8d27cb9854a09fcc93f73882cc5df12ac4108ab02581ab449da4df345f68bS3VersionKeyB049B315" + "Ref": "AssetParametersbd060cb930079c194320bc9a045d159066215c3a4858c45bdb12a79ef9a1edbaS3VersionKeyBCA0A3F3" } ] } @@ -112,7 +135,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters0fb8d27cb9854a09fcc93f73882cc5df12ac4108ab02581ab449da4df345f68bS3VersionKeyB049B315" + "Ref": "AssetParametersbd060cb930079c194320bc9a045d159066215c3a4858c45bdb12a79ef9a1edbaS3VersionKeyBCA0A3F3" } ] } @@ -122,13 +145,13 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs12.x", "Timeout": 120 }, @@ -136,6 +159,30 @@ "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" ] }, + "ListTopicsCustomResourcePolicy31A8396A": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:ListTopics", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ListTopicsCustomResourcePolicy31A8396A", + "Roles": [ + { + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + } + ] + }, + "DependsOn": [ + "TopicBFC7AF6E" + ] + }, "ListTopicsCE1E0341": { "Type": "Custom::AWS", "Properties": { @@ -145,20 +192,8 @@ "Arn" ] }, - "Create": { - "service": "SNS", - "action": "listTopics", - "physicalResourceId": { - "responsePath": "Topics.0.TopicArn" - } - }, - "Update": { - "service": "SNS", - "action": "listTopics", - "physicalResourceId": { - "responsePath": "Topics.0.TopicArn" - } - }, + "Create": "{\"service\":\"SNS\",\"action\":\"listTopics\",\"physicalResourceId\":{\"responsePath\":\"Topics.0.TopicArn\"}}", + "Update": "{\"service\":\"SNS\",\"action\":\"listTopics\",\"physicalResourceId\":{\"responsePath\":\"Topics.0.TopicArn\"}}", "InstallLatestAwsSdk": true }, "DependsOn": [ @@ -175,6 +210,27 @@ "Value": "1337" } }, + "GetParameterCustomResourcePolicyD8E5D455": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "ssm:GetParameter", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "GetParameterCustomResourcePolicyD8E5D455", + "Roles": [ + { + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + } + ] + } + }, "GetParameter42B0A00E": { "Type": "Custom::SSMParameter", "Properties": { @@ -185,86 +241,50 @@ ] }, "Create": { - "service": "SSM", - "action": "getParameter", - "parameters": { - "Name": { - "Ref": "DummyParameter53662B67" - }, - "WithDecryption": "TRUE:BOOLEAN" - }, - "physicalResourceId": { - "responsePath": "Parameter.ARN" - } + "Fn::Join": [ + "", + [ + "{\"service\":\"SSM\",\"action\":\"getParameter\",\"parameters\":{\"Name\":\"", + { + "Ref": "DummyParameter53662B67" + }, + "\",\"WithDecryption\":true},\"physicalResourceId\":{\"responsePath\":\"Parameter.ARN\"}}" + ] + ] }, "Update": { - "service": "SSM", - "action": "getParameter", - "parameters": { - "Name": { - "Ref": "DummyParameter53662B67" - }, - "WithDecryption": "TRUE:BOOLEAN" - }, - "physicalResourceId": { - "responsePath": "Parameter.ARN" - } + "Fn::Join": [ + "", + [ + "{\"service\":\"SSM\",\"action\":\"getParameter\",\"parameters\":{\"Name\":\"", + { + "Ref": "DummyParameter53662B67" + }, + "\",\"WithDecryption\":true},\"physicalResourceId\":{\"responsePath\":\"Parameter.ARN\"}}" + ] + ] }, "InstallLatestAwsSdk": true }, - "UpdateReplacePolicy": "Delete", - "DeletionPolicy": "Delete", "DependsOn": [ "GetParameterCustomResourcePolicyD8E5D455" - ] - }, - "PublishCustomResourcePolicyDF696FCA": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [{"Action":"sns:Publish","Effect":"Allow","Resource":"*"}], - "Version": "2012-10-17" - }, - "PolicyName": "PublishCustomResourcePolicyDF696FCA", - "Roles": [{"Ref":"AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2"}] - } - }, - "ListTopicsCustomResourcePolicy31A8396A": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [{"Action":"sns:ListTopics","Effect":"Allow","Resource":"*"}], - "Version": "2012-10-17" - }, - "PolicyName": "ListTopicsCustomResourcePolicy31A8396A", - "Roles": [{"Ref":"AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2"}] - }, - "DependsOn": ["TopicBFC7AF6E"] - }, - "GetParameterCustomResourcePolicyD8E5D455": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [{"Action":"ssm:GetParameter","Effect":"Allow","Resource":"*"}], - "Version": "2012-10-17" - }, - "PolicyName": "GetParameterCustomResourcePolicyD8E5D455", - "Roles": [{"Ref":"AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2"}] - } + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" } }, "Parameters": { - "AssetParameters0fb8d27cb9854a09fcc93f73882cc5df12ac4108ab02581ab449da4df345f68bS3BucketA4761502": { + "AssetParametersbd060cb930079c194320bc9a045d159066215c3a4858c45bdb12a79ef9a1edbaS3BucketACF45CC2": { "Type": "String", - "Description": "S3 bucket for asset \"0fb8d27cb9854a09fcc93f73882cc5df12ac4108ab02581ab449da4df345f68b\"" + "Description": "S3 bucket for asset \"bd060cb930079c194320bc9a045d159066215c3a4858c45bdb12a79ef9a1edba\"" }, - "AssetParameters0fb8d27cb9854a09fcc93f73882cc5df12ac4108ab02581ab449da4df345f68bS3VersionKeyB049B315": { + "AssetParametersbd060cb930079c194320bc9a045d159066215c3a4858c45bdb12a79ef9a1edbaS3VersionKeyBCA0A3F3": { "Type": "String", - "Description": "S3 key for asset version \"0fb8d27cb9854a09fcc93f73882cc5df12ac4108ab02581ab449da4df345f68b\"" + "Description": "S3 key for asset version \"bd060cb930079c194320bc9a045d159066215c3a4858c45bdb12a79ef9a1edba\"" }, - "AssetParameters0fb8d27cb9854a09fcc93f73882cc5df12ac4108ab02581ab449da4df345f68bArtifactHash840E5880": { + "AssetParametersbd060cb930079c194320bc9a045d159066215c3a4858c45bdb12a79ef9a1edbaArtifactHashF3AE56EF": { "Type": "String", - "Description": "Artifact hash for asset \"0fb8d27cb9854a09fcc93f73882cc5df12ac4108ab02581ab449da4df345f68b\"" + "Description": "Artifact hash for asset \"bd060cb930079c194320bc9a045d159066215c3a4858c45bdb12a79ef9a1edba\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/cx-api/lib/features.ts b/packages/@aws-cdk/cx-api/lib/features.ts index 24ba471882644..162ec0de9d7f4 100644 --- a/packages/@aws-cdk/cx-api/lib/features.ts +++ b/packages/@aws-cdk/cx-api/lib/features.ts @@ -89,6 +89,21 @@ export const KMS_DEFAULT_KEY_POLICIES = '@aws-cdk/aws-kms:defaultKeyPolicies'; */ export const S3_GRANT_WRITE_WITHOUT_ACL = '@aws-cdk/aws-s3:grantWriteWithoutAcl'; +/** + * ApplicationLoadBalancedServiceBase, ApplicationMultipleTargetGroupServiceBase, + * NetworkLoadBalancedServiceBase, NetworkMultipleTargetGroupServiceBase, and + * QueueProcessingServiceBase currently determine a default value for the desired count of + * a CfnService if a desiredCount is not provided. + * + * If this flag is not set, the default behaviour for CfnService.desiredCount is to set a + * desiredCount of 1, if one is not provided. If true, a default will not be defined for + * CfnService.desiredCount and as such desiredCount will be undefined, if one is not provided. + * + * This is a feature flag as the old behavior was technically incorrect, but + * users may have come to depend on it. + */ +export const ECS_REMOVE_DEFAULT_DESIRED_COUNT = '@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount'; + /** * This map includes context keys and values for feature flags that enable * capabilities "from the future", which we could not introduce as the default @@ -110,6 +125,7 @@ export const FUTURE_FLAGS: { [key: string]: any } = { [SECRETS_MANAGER_PARSE_OWNED_SECRET_NAME]: true, [KMS_DEFAULT_KEY_POLICIES]: true, [S3_GRANT_WRITE_WITHOUT_ACL]: true, + [ECS_REMOVE_DEFAULT_DESIRED_COUNT]: true, // We will advertise this flag when the feature is complete // [NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: 'true', @@ -135,6 +151,7 @@ const FUTURE_FLAGS_DEFAULTS: { [key: string]: boolean } = { [SECRETS_MANAGER_PARSE_OWNED_SECRET_NAME]: false, [KMS_DEFAULT_KEY_POLICIES]: false, [S3_GRANT_WRITE_WITHOUT_ACL]: false, + [ECS_REMOVE_DEFAULT_DESIRED_COUNT]: false, }; export function futureFlagDefault(flag: string): boolean { diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index 718ba9b503690..512ee5e97c92b 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -655,6 +655,12 @@ These command lines explained: > Make sure you trust all the code and dependencies that make up your CDK app. > Check with the appropriate department within your organization to decide on the > proper policy to use. +> +> If your policy includes permissions to create on attach permission to a role, +> developers can escalate their privilege with more permissive permission. +> Thus, we recommend implementing [permissions boundary](https://aws.amazon.com/premiumsupport/knowledge-center/iam-permission-boundaries/) +> in the CDK Execution role. To do this, you can bootstrap with the `--template` option with +> [a customized template](https://github.com/aws-samples/aws-bootstrap-kit-examples/blob/ba28a97d289128281bc9483bcba12c1793f2c27a/source/1-SDLC-organization/lib/cdk-bootstrap-template.yml#L395) that contains a permission boundary. ### Migrating from old bootstrap stack diff --git a/packages/@monocdk-experiment/assert/package.json b/packages/@monocdk-experiment/assert/package.json index 86806f041880f..dbd0d792e8a60 100644 --- a/packages/@monocdk-experiment/assert/package.json +++ b/packages/@monocdk-experiment/assert/package.json @@ -47,7 +47,7 @@ "@aws-cdk/cloudformation-diff": "0.0.0" }, "peerDependencies": { - "constructs": "^3.0.4", + "constructs": "^3.2.0", "jest": "^26.6.3", "monocdk": "^0.0.0" }, diff --git a/packages/aws-cdk-lib/package.json b/packages/aws-cdk-lib/package.json index 6eadd958185df..89dbc64979387 100644 --- a/packages/aws-cdk-lib/package.json +++ b/packages/aws-cdk-lib/package.json @@ -293,7 +293,7 @@ "ubergen": "0.0.0" }, "peerDependencies": { - "constructs": "^3.0.4" + "constructs": "^3.2.0" }, "homepage": "https://github.com/aws/aws-cdk", "engines": { diff --git a/packages/aws-cdk/lib/init-templates/v1/app/java/pom.template.xml b/packages/aws-cdk/lib/init-templates/v1/app/java/pom.template.xml index 37be67aa2c7a5..fb0b6cf828aaf 100644 --- a/packages/aws-cdk/lib/init-templates/v1/app/java/pom.template.xml +++ b/packages/aws-cdk/lib/init-templates/v1/app/java/pom.template.xml @@ -10,7 +10,7 @@ UTF-8 %cdk-version% - 5.7.0 + 5.7.1 diff --git a/packages/aws-cdk/lib/init-templates/v1/app/python/%name.PythonModule%/%name.PythonModule%_stack.template.py b/packages/aws-cdk/lib/init-templates/v1/app/python/%name.PythonModule%/%name.PythonModule%_stack.template.py index 9d86ad16906e6..6b4ed6e8ea6ed 100644 --- a/packages/aws-cdk/lib/init-templates/v1/app/python/%name.PythonModule%/%name.PythonModule%_stack.template.py +++ b/packages/aws-cdk/lib/init-templates/v1/app/python/%name.PythonModule%/%name.PythonModule%_stack.template.py @@ -1,9 +1,15 @@ +from aws_cdk import core as cdk + +# For consistency with other languages, `cdk` is the preferred import name for +# the CDK's core module. The following line also imports it as `core` for use +# with examples from the CDK Developer's Guide, which are in the process of +# being updated to use `cdk`. You may delete this import if you don't need it. from aws_cdk import core -class %name.PascalCased%Stack(core.Stack): +class %name.PascalCased%Stack(cdk.Stack): - def __init__(self, scope: core.Construct, construct_id: str, **kwargs) -> None: + def __init__(self, scope: cdk.Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) # The code that defines your stack goes here diff --git a/packages/aws-cdk/lib/init-templates/v1/app/python/app.template.py b/packages/aws-cdk/lib/init-templates/v1/app/python/app.template.py index 808bc22af32e4..bc43099fd2026 100644 --- a/packages/aws-cdk/lib/init-templates/v1/app/python/app.template.py +++ b/packages/aws-cdk/lib/init-templates/v1/app/python/app.template.py @@ -1,11 +1,17 @@ #!/usr/bin/env python3 +from aws_cdk import core as cdk + +# For consistency with TypeScript code, `cdk` is the preferred import name for +# the CDK's core module. The following line also imports it as `core` for use +# with examples from the CDK Developer's Guide, which are in the process of +# being updated to use `cdk`. You may delete this import if you don't need it. from aws_cdk import core from %name.PythonModule%.%name.PythonModule%_stack import %name.PascalCased%Stack -app = core.App() +app = cdk.App() %name.PascalCased%Stack(app, "%name.StackName%") app.synth() diff --git a/packages/aws-cdk/lib/init-templates/v1/sample-app/java/pom.template.xml b/packages/aws-cdk/lib/init-templates/v1/sample-app/java/pom.template.xml index 0355825bf0a7a..5d679d2570040 100644 --- a/packages/aws-cdk/lib/init-templates/v1/sample-app/java/pom.template.xml +++ b/packages/aws-cdk/lib/init-templates/v1/sample-app/java/pom.template.xml @@ -8,7 +8,7 @@ UTF-8 %cdk-version% - 5.7.0 + 5.7.1 diff --git a/packages/aws-cdk/lib/init-templates/v2/app/java/pom.template.xml b/packages/aws-cdk/lib/init-templates/v2/app/java/pom.template.xml index f49fe7b60e963..5defab0b3a0b6 100644 --- a/packages/aws-cdk/lib/init-templates/v2/app/java/pom.template.xml +++ b/packages/aws-cdk/lib/init-templates/v2/app/java/pom.template.xml @@ -10,7 +10,7 @@ UTF-8 %cdk-version% - 5.7.0 + 5.7.1 diff --git a/packages/aws-cdk/lib/init-templates/v2/app/python/.template.gitignore b/packages/aws-cdk/lib/init-templates/v2/app/python/.template.gitignore index 383cdd5040f7e..37833f8beb2a3 100644 --- a/packages/aws-cdk/lib/init-templates/v2/app/python/.template.gitignore +++ b/packages/aws-cdk/lib/init-templates/v2/app/python/.template.gitignore @@ -2,7 +2,7 @@ package-lock.json __pycache__ .pytest_cache -.env +.venv *.egg-info # CDK asset staging directory diff --git a/packages/aws-cdk/lib/init-templates/v2/sample-app/java/pom.template.xml b/packages/aws-cdk/lib/init-templates/v2/sample-app/java/pom.template.xml index 1236d332e9bab..7f10c89a54dd4 100644 --- a/packages/aws-cdk/lib/init-templates/v2/sample-app/java/pom.template.xml +++ b/packages/aws-cdk/lib/init-templates/v2/sample-app/java/pom.template.xml @@ -8,7 +8,7 @@ UTF-8 %cdk-version% - 5.7.0 + 5.7.1 diff --git a/scripts/check-pack-prerequisites.sh b/scripts/check-pack-prerequisites.sh index 8dca0902114e5..6b648054fe253 100755 --- a/scripts/check-pack-prerequisites.sh +++ b/scripts/check-pack-prerequisites.sh @@ -54,12 +54,12 @@ app_v=$(${app} -version 2>&1) echo -e "Checking javac version... \c" # 1.8 if [ $(echo $app_v | grep -c -E "1\.8\.[0-9].*") -eq 1 ] -then +then echo "Ok" else # 11 or 14 or 15 if [ $(echo $app_v | grep -c -E "1[145]\.[0-9]\.[0-9].*") -eq 1 ] - then + then echo "Ok" else wrong_version @@ -73,7 +73,7 @@ check_which $app $app_min app_v=$(${app} --version) echo -e "Checking mvn version... \c" if [ $(echo $app_v | grep -c -E "3\.[6789]\.[0-9].*") -eq 1 ] -then +then echo "Ok" else wrong_version @@ -85,8 +85,8 @@ app_min="3.1.0" check_which $app $app_min app_v=$(${app} --version) echo -e "Checking $app version... \c" -if [ $(echo $app_v | grep -c -E "3\.1\.[0-9].*") -eq 1 ] -then +if [ $(echo $app_v | grep -c -E "3\.1\.[0-9].*|[4-9]\..*") -eq 1 ] +then echo "Ok" else wrong_version @@ -99,7 +99,7 @@ check_which $app $app_min app_v=$(${app} --version) echo -e "Checking $app version... \c" if [ $(echo $app_v | grep -c -E "3\.[6789]\.[0-9].*") -eq 1 ] -then +then echo "Ok" else wrong_version diff --git a/tools/pkglint/lib/rules.ts b/tools/pkglint/lib/rules.ts index 67e26c9634712..1dd32c1c96392 100644 --- a/tools/pkglint/lib/rules.ts +++ b/tools/pkglint/lib/rules.ts @@ -617,6 +617,38 @@ export class NoPeerDependenciesMonocdk extends ValidationRule { } } +/** + * Validates that the same version of `constructs` is used wherever a dependency + * is specified, so that they must all be udpated at the same time (through an + * update to this rule). + * + * Note: v1 and v2 use different versions respectively. + */ +export class ConstructsVersion extends ValidationRule { + public readonly name = 'deps/constructs'; + private readonly expectedRange = cdkMajorVersion() === 2 + ? '10.0.0-pre.5' + : '^3.2.0'; + + public validate(pkg: PackageJson) { + const toCheck = new Array(); + + if ('constructs' in pkg.dependencies) { + toCheck.push('dependencies'); + } + if ('constructs' in pkg.devDependencies) { + toCheck.push('devDependencies'); + } + if ('constructs' in pkg.peerDependencies) { + toCheck.push('peerDependencies'); + } + + for (const cfg of toCheck) { + expectJSON(this.name, pkg, `${cfg}.constructs`, this.expectedRange); + } + } +} + /** * JSII Java package is required and must look sane */ diff --git a/version.v1.json b/version.v1.json index eca0b33c11e76..aa566c6d404ff 100644 --- a/version.v1.json +++ b/version.v1.json @@ -1,3 +1,3 @@ { - "version": "1.90.1" + "version": "1.91.0" }