diff --git a/.github/workflows/issue-label-assign.yml b/.github/workflows/issue-label-assign.yml index 8c077aa836951..9e2f2ce7e3e34 100644 --- a/.github/workflows/issue-label-assign.yml +++ b/.github/workflows/issue-label-assign.yml @@ -181,5 +181,7 @@ jobs: {"keywords":["(@aws-cdk/custom-resources)","(custom-resources)","(custom resources)"],"labels":["@aws-cdk/custom-resources"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/cx-api)","(cx-api)","(cx api)"],"labels":["@aws-cdk/cx-api"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/pipelines)","(pipelines)","(cdk pipelines)","(cdk-pipelines)"],"labels":["@aws-cdk/pipelines"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/region-info)","(region-info)","(region info)"],"labels":["@aws-cdk/region-info"],"assignees":["RomainMuller"]} + {"keywords":["(@aws-cdk/region-info)","(region-info)","(region info)"],"labels":["@aws-cdk/region-info"],"assignees":["RomainMuller"]}, + {"keywords":["(aws-cdk-lib)","(cdk-v2)", "(v2)", "(ubergen)"],"labels":["aws-cdk-lib"],"assignees":["nija-at"]}, + {"keywords":["(monocdk)","(monocdk-experiment)"],"labels":["monocdk"],"assignees":["nija-at"]} ] diff --git a/.mergify.yml b/.mergify.yml index 97ca6d16de18b..358575303ca74 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -6,7 +6,7 @@ pull_request_rules: label: add: [ contribution/core ] conditions: - - author~=^(eladb|RomainMuller|garnaat|nija-at|shivlaks|skinny85|rix0rrr|NGL321|Jerry-AWS|SomayaB|MrArnoldPalmer|NetaNir|iliapolo|njlynch|ericzbeard|ccfife|fulghum|pkandasamy91|SoManyHs|uttarasridhar)$ + - author~=^(eladb|RomainMuller|garnaat|nija-at|skinny85|rix0rrr|NGL321|Jerry-AWS|MrArnoldPalmer|NetaNir|iliapolo|njlynch|ericzbeard|ccfife|fulghum|pkandasamy91|SoManyHs|uttarasridhar)$ - -label~="contribution/core" - name: automatic merge actions: @@ -118,4 +118,4 @@ pull_request_rules: - "#approved-reviews-by>=1" - "#changes-requested-reviews-by=0" - status-success~=AWS CodeBuild us-east-1 - - status-success=validate-pr \ No newline at end of file + - status-success=validate-pr diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f5ba6592ea70..5c6ffbb423465 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,37 @@ 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.84.0](https://github.com/aws/aws-cdk/compare/v1.83.0...v1.84.0) (2021-01-12) + + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **apigatewayv2:** `subnets` prop in `VpcLink` resource now takes `SubnetSelection` instead of `ISubnet[]` + +### Features + +* **aws-lambda-nodejs:** add esbuild `define` bundling option ([#12424](https://github.com/aws/aws-cdk/issues/12424)) ([581f6af](https://github.com/aws/aws-cdk/commit/581f6af3d1f71737ca93b6ecb9b004bdade149a8)), closes [#12423](https://github.com/aws/aws-cdk/issues/12423) +* **cdk-assets:** add external asset support ([#12259](https://github.com/aws/aws-cdk/issues/12259)) ([05a9980](https://github.com/aws/aws-cdk/commit/05a998065b3333854715c456b20b7cc5d5daac67)) +* **cli:** `--quiet` does not print template in `cdk synth` ([#12178](https://github.com/aws/aws-cdk/issues/12178)) ([74458a0](https://github.com/aws/aws-cdk/commit/74458a0e9eebce4ee254673aad8933d39588d843)), closes [#11970](https://github.com/aws/aws-cdk/issues/11970) +* **codebuild:** support Standard 5.0 ([#12434](https://github.com/aws/aws-cdk/issues/12434)) ([422dc8e](https://github.com/aws/aws-cdk/commit/422dc8e9d50105af4e710d409a4f301079d43f3f)), closes [#12433](https://github.com/aws/aws-cdk/issues/12433) +* **core:** validate maximum amount of resources in a stack ([#12193](https://github.com/aws/aws-cdk/issues/12193)) ([26121c8](https://github.com/aws/aws-cdk/commit/26121c81abf0fb92de97567c758a1ecf60f85f63)), closes [#276](https://github.com/aws/aws-cdk/issues/276) +* **eks:** spot interruption handler can be disabled for self managed nodes ([#12453](https://github.com/aws/aws-cdk/issues/12453)) ([6ac1f4f](https://github.com/aws/aws-cdk/commit/6ac1f4fdef5853785d8e57652ec4c4e1d770844d)), closes [#12451](https://github.com/aws/aws-cdk/issues/12451) +* **synthetics:** Update Cloudwatch Synthetics canaries NodeJS runtimes ([#11866](https://github.com/aws/aws-cdk/issues/11866)) ([4f6e377](https://github.com/aws/aws-cdk/commit/4f6e377ae3f35c3fa010e1597c3d71ef6e6e9a04)), closes [#11870](https://github.com/aws/aws-cdk/issues/11870) + + +### Bug Fixes + +* **apigatewayv2:** vpclink - explicit subnet specification still causes private subnets to be included ([#12401](https://github.com/aws/aws-cdk/issues/12401)) ([336a58f](https://github.com/aws/aws-cdk/commit/336a58f06a3b3a9f5db2a79350f8721244767e3b)), closes [#12083](https://github.com/aws/aws-cdk/issues/12083) +* **cli:** CLI doesn't read context from ~/.cdk.json ([#12394](https://github.com/aws/aws-cdk/issues/12394)) ([2389a9b](https://github.com/aws/aws-cdk/commit/2389a9b5742583f1d58c66a4f513ee4d833baab5)), closes [#10823](https://github.com/aws/aws-cdk/issues/10823) [#4802](https://github.com/aws/aws-cdk/issues/4802) +* **core:** DefaultStackSynthesizer bucket prefix missing for template assets ([#11855](https://github.com/aws/aws-cdk/issues/11855)) ([50a3d3a](https://github.com/aws/aws-cdk/commit/50a3d3acf3e413d9b4e51197d2be4ea1349c0955)), closes [#10710](https://github.com/aws/aws-cdk/issues/10710) [#11327](https://github.com/aws/aws-cdk/issues/11327) +* **dynamodb:** missing grantRead for ConditionCheckItem ([#12313](https://github.com/aws/aws-cdk/issues/12313)) ([e157007](https://github.com/aws/aws-cdk/commit/e1570072440b07b6b82219c1a4371386c541fb1c)) +* **ec2:** interface endpoint AZ lookup does not guard against broken situations ([#12033](https://github.com/aws/aws-cdk/issues/12033)) ([80f0bfd](https://github.com/aws/aws-cdk/commit/80f0bfd167430a015e71b00506e0ecc280068e86)) +* **eks:** nodegroup synthesis fails when configured with an AMI type that is not compatible to the default instance type ([#12441](https://github.com/aws/aws-cdk/issues/12441)) ([5f6f0f9](https://github.com/aws/aws-cdk/commit/5f6f0f9d46dbd460ac03dd5f9f4874eaa41611d8)), closes [#12389](https://github.com/aws/aws-cdk/issues/12389) +* **elasticsearch:** domain fails due to log publishing keys on unsupported cluster versions ([#11622](https://github.com/aws/aws-cdk/issues/11622)) ([e6bb96f](https://github.com/aws/aws-cdk/commit/e6bb96ff6bae96e3167c82f6de97807217ddb3be)) +* **elbv2:** can't import two application listeners into the same scope ([#12373](https://github.com/aws/aws-cdk/issues/12373)) ([6534dcf](https://github.com/aws/aws-cdk/commit/6534dcf3e04a55f5c6d28203192cbbddb5d119e6)), closes [#12132](https://github.com/aws/aws-cdk/issues/12132) +* **logs:** custom resource Lambda uses old NodeJS version ([#12228](https://github.com/aws/aws-cdk/issues/12228)) ([29c4943](https://github.com/aws/aws-cdk/commit/29c4943466f4a911f65a2a13cf9e776ade9b8dfe)) +* **stepfunctions-tasks:** EvaluateExpression does not support JSON paths with dash ([#12248](https://github.com/aws/aws-cdk/issues/12248)) ([da1ed08](https://github.com/aws/aws-cdk/commit/da1ed08a6a2de584f5ddf43dab4efbb530541419)), closes [#12221](https://github.com/aws/aws-cdk/issues/12221) + ## [1.83.0](https://github.com/aws/aws-cdk/compare/v1.82.0...v1.83.0) (2021-01-06) diff --git a/allowed-breaking-changes.txt b/allowed-breaking-changes.txt index 9120903b01912..2ca2ca5b6067f 100644 --- a/allowed-breaking-changes.txt +++ b/allowed-breaking-changes.txt @@ -52,3 +52,7 @@ incompatible-argument:@aws-cdk/aws-ecs.FargateTaskDefinition. incompatible-argument:@aws-cdk/aws-ecs.FargateTaskDefinition.addVolume incompatible-argument:@aws-cdk/aws-ecs.TaskDefinition. incompatible-argument:@aws-cdk/aws-ecs.TaskDefinition.addVolume + +# 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 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 3abc980a7fae3..6a9f04c284047 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 @@ -675,6 +675,7 @@ "dynamodb:Query", "dynamodb:GetItem", "dynamodb:Scan", + "dynamodb:ConditionCheckItem", "dynamodb:BatchWriteItem", "dynamodb:PutItem", "dynamodb:UpdateItem", diff --git a/packages/@aws-cdk/app-delivery/package.json b/packages/@aws-cdk/app-delivery/package.json index b8ca6275939df..703d259f6ecbc 100644 --- a/packages/@aws-cdk/app-delivery/package.json +++ b/packages/@aws-cdk/app-delivery/package.json @@ -64,7 +64,7 @@ "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "fast-check": "^2.7.0", + "fast-check": "^2.11.0", "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 875ed1df174c3..5e670032ef158 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -109,7 +109,7 @@ item.addMethod('DELETE', new apigateway.HttpIntegration('http://amazon.com')); ### Breaking up Methods and Resources across Stacks It is fairly common for REST APIs with a large number of Resources and Methods to hit the [CloudFormation -limit](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cloudformation-limits.html) of 200 resources per +limit](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cloudformation-limits.html) of 500 resources per stack. To help with this, Resources and Methods for the same REST API can be re-organized across multiple stacks. A common diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/vpc-link.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/vpc-link.ts index 27d478c335963..a9c6604b43485 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/vpc-link.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/vpc-link.ts @@ -39,7 +39,7 @@ export interface VpcLinkProps { * * @default - private subnets of the provided VPC. Use `addSubnets` to add more subnets */ - readonly subnets?: ec2.ISubnet[]; + readonly subnets?: ec2.SubnetSelection; /** * A list of security groups for the VPC link. @@ -99,11 +99,8 @@ export class VpcLink extends Resource implements IVpcLink { this.vpcLinkId = cfnResource.ref; - this.addSubnets(...props.vpc.privateSubnets); - - if (props.subnets) { - this.addSubnets(...props.subnets); - } + const { subnets } = props.vpc.selectSubnets(props.subnets ?? { subnetType: ec2.SubnetType.PRIVATE }); + this.addSubnets(...subnets); if (props.securityGroups) { this.addSecurityGroups(...props.securityGroups); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/vpc-link.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/vpc-link.test.ts index 1571ceef39f6d..c6b68d7477bd7 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/vpc-link.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/vpc-link.test.ts @@ -51,7 +51,7 @@ describe('VpcLink', () => { // WHEN new VpcLink(stack, 'VpcLink', { vpc, - subnets: [subnet1, subnet2], + subnets: { subnets: [subnet1, subnet2] }, securityGroups: [sg1, sg2, sg3], }); @@ -59,12 +59,6 @@ describe('VpcLink', () => { expect(stack).toHaveResource('AWS::ApiGatewayV2::VpcLink', { Name: 'VpcLink', SubnetIds: [ - { - Ref: 'VPCPrivateSubnet1Subnet8BCA10E0', - }, - { - Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A', - }, { Ref: 'subnet1Subnet16A4B3BD', }, diff --git a/packages/@aws-cdk/aws-applicationautoscaling/package.json b/packages/@aws-cdk/aws-applicationautoscaling/package.json index 9089e16947251..f9b8ce510249c 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/package.json +++ b/packages/@aws-cdk/aws-applicationautoscaling/package.json @@ -76,7 +76,7 @@ "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", - "fast-check": "^2.7.0", + "fast-check": "^2.11.0", "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-appsync/test/integ.api-import.expected.json b/packages/@aws-cdk/aws-appsync/test/integ.api-import.expected.json index 1a760c0d715a1..87f6776cc79d1 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.api-import.expected.json +++ b/packages/@aws-cdk/aws-appsync/test/integ.api-import.expected.json @@ -81,6 +81,7 @@ "dynamodb:Query", "dynamodb:GetItem", "dynamodb:Scan", + "dynamodb:ConditionCheckItem", "dynamodb:BatchWriteItem", "dynamodb:PutItem", "dynamodb:UpdateItem", diff --git a/packages/@aws-cdk/aws-appsync/test/integ.auth-apikey.expected.json b/packages/@aws-cdk/aws-appsync/test/integ.auth-apikey.expected.json index 62c6b75e725d4..390d55bd9bb38 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.auth-apikey.expected.json +++ b/packages/@aws-cdk/aws-appsync/test/integ.auth-apikey.expected.json @@ -64,6 +64,7 @@ "dynamodb:Query", "dynamodb:GetItem", "dynamodb:Scan", + "dynamodb:ConditionCheckItem", "dynamodb:BatchWriteItem", "dynamodb:PutItem", "dynamodb:UpdateItem", diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.expected.json b/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.expected.json index 1788c675b2c67..4cd9043b28c4b 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.expected.json +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.expected.json @@ -93,6 +93,7 @@ "dynamodb:Query", "dynamodb:GetItem", "dynamodb:Scan", + "dynamodb:ConditionCheckItem", "dynamodb:BatchWriteItem", "dynamodb:PutItem", "dynamodb:UpdateItem", diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql-schema.expected.json b/packages/@aws-cdk/aws-appsync/test/integ.graphql-schema.expected.json index 60e71fe2c0293..a6e5ff5764331 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.graphql-schema.expected.json +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql-schema.expected.json @@ -63,6 +63,7 @@ "dynamodb:Query", "dynamodb:GetItem", "dynamodb:Scan", + "dynamodb:ConditionCheckItem", "dynamodb:BatchWriteItem", "dynamodb:PutItem", "dynamodb:UpdateItem", diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json b/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json index be40ad91870b7..d540f3777ed00 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json @@ -141,6 +141,7 @@ "dynamodb:Query", "dynamodb:GetItem", "dynamodb:Scan", + "dynamodb:ConditionCheckItem", "dynamodb:BatchWriteItem", "dynamodb:PutItem", "dynamodb:UpdateItem", @@ -227,6 +228,7 @@ "dynamodb:Query", "dynamodb:GetItem", "dynamodb:Scan", + "dynamodb:ConditionCheckItem", "dynamodb:BatchWriteItem", "dynamodb:PutItem", "dynamodb:UpdateItem", @@ -324,6 +326,7 @@ "dynamodb:Query", "dynamodb:GetItem", "dynamodb:Scan", + "dynamodb:ConditionCheckItem", "dynamodb:BatchWriteItem", "dynamodb:PutItem", "dynamodb:UpdateItem", diff --git a/packages/@aws-cdk/aws-autoscaling-common/package.json b/packages/@aws-cdk/aws-autoscaling-common/package.json index eb987c30fd0f4..1a733ec3c7541 100644 --- a/packages/@aws-cdk/aws-autoscaling-common/package.json +++ b/packages/@aws-cdk/aws-autoscaling-common/package.json @@ -68,7 +68,7 @@ "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "fast-check": "^2.7.0", + "fast-check": "^2.11.0", "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-chatbot/test/integ.chatbot-logretention.expected.json b/packages/@aws-cdk/aws-chatbot/test/integ.chatbot-logretention.expected.json index 727be77a25991..23c51da03a661 100644 --- a/packages/@aws-cdk/aws-chatbot/test/integ.chatbot-logretention.expected.json +++ b/packages/@aws-cdk/aws-chatbot/test/integ.chatbot-logretention.expected.json @@ -170,7 +170,7 @@ "Arn" ] }, - "Runtime": "nodejs10.x" + "Runtime": "nodejs12.x" }, "DependsOn": [ "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB", diff --git a/packages/@aws-cdk/aws-cloudfront-origins/package.json b/packages/@aws-cdk/aws-cloudfront-origins/package.json index d9bf9c1198769..0feacddfa4f91 100644 --- a/packages/@aws-cdk/aws-cloudfront-origins/package.json +++ b/packages/@aws-cdk/aws-cloudfront-origins/package.json @@ -73,7 +73,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", - "aws-sdk": "^2.822.0", + "aws-sdk": "^2.824.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "pkglint": "0.0.0" diff --git a/packages/@aws-cdk/aws-cloudfront/package.json b/packages/@aws-cdk/aws-cloudfront/package.json index 1f06c66a87fe2..aa8791dcdf953 100644 --- a/packages/@aws-cdk/aws-cloudfront/package.json +++ b/packages/@aws-cdk/aws-cloudfront/package.json @@ -74,7 +74,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "aws-sdk": "^2.822.0", + "aws-sdk": "^2.824.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudtrail/package.json b/packages/@aws-cdk/aws-cloudtrail/package.json index e9a5abdba7e16..e2b4e7c0146a0 100644 --- a/packages/@aws-cdk/aws-cloudtrail/package.json +++ b/packages/@aws-cdk/aws-cloudtrail/package.json @@ -74,7 +74,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "aws-sdk": "^2.822.0", + "aws-sdk": "^2.824.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index 461bb8fd5e233..13df5688ee151 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -1436,6 +1436,8 @@ export class LinuxBuildImage implements IBuildImage { public static readonly STANDARD_3_0 = LinuxBuildImage.codeBuildImage('aws/codebuild/standard:3.0'); /** The `aws/codebuild/standard:4.0` build image. */ public static readonly STANDARD_4_0 = LinuxBuildImage.codeBuildImage('aws/codebuild/standard:4.0'); + /** The `aws/codebuild/standard:5.0` build image. */ + public static readonly STANDARD_5_0 = LinuxBuildImage.codeBuildImage('aws/codebuild/standard:5.0'); public static readonly AMAZON_LINUX_2 = LinuxBuildImage.codeBuildImage('aws/codebuild/amazonlinux2-x86_64-standard:1.0'); public static readonly AMAZON_LINUX_2_2 = LinuxBuildImage.codeBuildImage('aws/codebuild/amazonlinux2-x86_64-standard:2.0'); diff --git a/packages/@aws-cdk/aws-codebuild/package.json b/packages/@aws-cdk/aws-codebuild/package.json index 2d407253b3739..a41ea21bae2a0 100644 --- a/packages/@aws-cdk/aws-codebuild/package.json +++ b/packages/@aws-cdk/aws-codebuild/package.json @@ -80,7 +80,7 @@ "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.822.0", + "aws-sdk": "^2.824.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codecommit/package.json b/packages/@aws-cdk/aws-codecommit/package.json index 28996a5c3b380..922bb02012b61 100644 --- a/packages/@aws-cdk/aws-codecommit/package.json +++ b/packages/@aws-cdk/aws-codecommit/package.json @@ -80,7 +80,7 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.822.0", + "aws-sdk": "^2.824.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/package.json b/packages/@aws-cdk/aws-codepipeline-actions/package.json index bb959f76910bb..4ec325a8c2382 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/package.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/package.json @@ -70,7 +70,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-cloudtrail": "0.0.0", - "@types/lodash": "^4.14.165", + "@types/lodash": "^4.14.167", "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-dynamodb/lib/perms.ts b/packages/@aws-cdk/aws-dynamodb/lib/perms.ts index 56b20a2220912..af385e57209f5 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/perms.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/perms.ts @@ -5,6 +5,7 @@ export const READ_DATA_ACTIONS = [ 'dynamodb:Query', 'dynamodb:GetItem', 'dynamodb:Scan', + 'dynamodb:ConditionCheckItem', ]; export const KEY_READ_ACTIONS = [ 'kms:Decrypt', diff --git a/packages/@aws-cdk/aws-dynamodb/package.json b/packages/@aws-cdk/aws-dynamodb/package.json index c0d33b0cefee8..19ca930e30c48 100644 --- a/packages/@aws-cdk/aws-dynamodb/package.json +++ b/packages/@aws-cdk/aws-dynamodb/package.json @@ -75,7 +75,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/jest": "^26.0.15", - "aws-sdk": "^2.822.0", + "aws-sdk": "^2.824.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts index 119043b54464a..ae77a3024adbd 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts @@ -765,6 +765,7 @@ test('if an encryption key is included, encrypt/decrypt permissions are also add 'dynamodb:Query', 'dynamodb:GetItem', 'dynamodb:Scan', + 'dynamodb:ConditionCheckItem', 'dynamodb:BatchWriteItem', 'dynamodb:PutItem', 'dynamodb:UpdateItem', @@ -1837,7 +1838,7 @@ describe('grants', () => { test('"grantReadData" allows the principal to read data from the table', () => { testGrant( - ['BatchGetItem', 'GetRecords', 'GetShardIterator', 'Query', 'GetItem', 'Scan'], (p, t) => t.grantReadData(p)); + ['BatchGetItem', 'GetRecords', 'GetShardIterator', 'Query', 'GetItem', 'Scan', 'ConditionCheckItem'], (p, t) => t.grantReadData(p)); }); test('"grantWriteData" allows the principal to write data to the table', () => { @@ -1848,7 +1849,7 @@ describe('grants', () => { test('"grantReadWriteData" allows the principal to read/write data', () => { testGrant([ 'BatchGetItem', 'GetRecords', 'GetShardIterator', 'Query', 'GetItem', 'Scan', - 'BatchWriteItem', 'PutItem', 'UpdateItem', 'DeleteItem', + 'ConditionCheckItem', 'BatchWriteItem', 'PutItem', 'UpdateItem', 'DeleteItem', ], (p, t) => t.grantReadWriteData(p)); }); @@ -2009,6 +2010,7 @@ describe('grants', () => { 'dynamodb:Query', 'dynamodb:GetItem', 'dynamodb:Scan', + 'dynamodb:ConditionCheckItem', ], 'Effect': 'Allow', 'Resource': [ @@ -2160,6 +2162,7 @@ describe('import', () => { 'dynamodb:Query', 'dynamodb:GetItem', 'dynamodb:Scan', + 'dynamodb:ConditionCheckItem', ], 'Effect': 'Allow', 'Resource': [ @@ -2201,6 +2204,7 @@ describe('import', () => { 'dynamodb:Query', 'dynamodb:GetItem', 'dynamodb:Scan', + 'dynamodb:ConditionCheckItem', 'dynamodb:BatchWriteItem', 'dynamodb:PutItem', 'dynamodb:UpdateItem', @@ -2346,6 +2350,7 @@ describe('import', () => { 'dynamodb:Query', 'dynamodb:GetItem', 'dynamodb:Scan', + 'dynamodb:ConditionCheckItem', ], Resource: [ { @@ -2479,6 +2484,7 @@ describe('global', () => { 'dynamodb:Query', 'dynamodb:GetItem', 'dynamodb:Scan', + 'dynamodb:ConditionCheckItem', ], Effect: 'Allow', Resource: [ @@ -2632,6 +2638,7 @@ describe('global', () => { 'dynamodb:Query', 'dynamodb:GetItem', 'dynamodb:Scan', + 'dynamodb:ConditionCheckItem', ], Effect: 'Allow', Resource: [ diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.expected.json b/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.expected.json index ba625281f2166..d20e923f3d55e 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.expected.json +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.expected.json @@ -386,7 +386,8 @@ "dynamodb:GetShardIterator", "dynamodb:Query", "dynamodb:GetItem", - "dynamodb:Scan" + "dynamodb:Scan", + "dynamodb:ConditionCheckItem" ], "Effect": "Allow", "Resource": [ @@ -408,7 +409,8 @@ "dynamodb:GetShardIterator", "dynamodb:Query", "dynamodb:GetItem", - "dynamodb:Scan" + "dynamodb:Scan", + "dynamodb:ConditionCheckItem" ], "Effect": "Allow", "Resource": [ diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.sse.expected.json b/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.sse.expected.json index c8e4ada3c14bd..ee7c8f9988f9e 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.sse.expected.json +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.sse.expected.json @@ -544,7 +544,8 @@ "dynamodb:GetShardIterator", "dynamodb:Query", "dynamodb:GetItem", - "dynamodb:Scan" + "dynamodb:Scan", + "dynamodb:ConditionCheckItem" ], "Effect": "Allow", "Resource": [ @@ -579,7 +580,8 @@ "dynamodb:GetShardIterator", "dynamodb:Query", "dynamodb:GetItem", - "dynamodb:Scan" + "dynamodb:Scan", + "dynamodb:ConditionCheckItem" ], "Effect": "Allow", "Resource": [ diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts index 83ef63d885b57..197946716f969 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts @@ -501,52 +501,72 @@ export class InterfaceVpcEndpoint extends VpcEndpoint implements IInterfaceVpcEn private endpointSubnets(props: InterfaceVpcEndpointProps) { const lookupSupportedAzs = props.lookupSupportedAzs ?? false; const subnetSelection = props.vpc.selectSubnets({ ...props.subnets, onePerAz: true }); + const subnets = subnetSelection.subnets; - // If we don't have an account/region, we will not be able to do filtering on AZs since - // they will be undefined - const agnosticAcct = Token.isUnresolved(this.stack.account); - const agnosticRegion = Token.isUnresolved(this.stack.region); + // Sanity check the subnet count + if (subnetSelection.subnets.length == 0) { + throw new Error('Cannot create a VPC Endpoint with no subnets'); + } + + // If we aren't going to lookup supported AZs we'll exit early, returning the subnetIds from the provided subnet selection + if (!lookupSupportedAzs) { + return subnetSelection.subnetIds; + } - // Some service names, such as AWS service name references, use Tokens to automatically - // fill in the region - // If it is an InterfaceVpcEndpointAwsService, then the reference will be resolvable since - // only references the region + // Some service names, such as AWS service name references, use Tokens to automatically fill in the region + // If it is an InterfaceVpcEndpointAwsService, then the reference will be resolvable since it only references the region const isAwsService = Token.isUnresolved(props.service.name) && props.service instanceof InterfaceVpcEndpointAwsService; - // Determine what name we pass to the context provider, either the verbatim name - // or a resolved version if it is an AWS service reference - let lookupServiceName = props.service.name; - if (isAwsService && !agnosticRegion) { - lookupServiceName = Stack.of(this).resolve(props.service.name); - } else { - // It's an agnostic service and we don't know how to resolve it. - // This is ok if the stack is region agnostic and we're not looking up - // AZs - lookupServiceName = props.service.name; + // Determine what service name gets pass to the context provider + // If it is an AWS service it will have a REGION token + const lookupServiceName = isAwsService ? Stack.of(this).resolve(props.service.name) : props.service.name; + + // Check that the lookup will work + this.validateCanLookupSupportedAzs(subnets, lookupServiceName); + + // Do the actual lookup for AZs + const availableAZs = this.availableAvailabilityZones(lookupServiceName); + const filteredSubnets = subnets.filter(s => availableAZs.includes(s.availabilityZone)); + + // Throw an error if the lookup filtered out all subnets + // VpcEndpoints must be created with at least one AZ + if (filteredSubnets.length == 0) { + throw new Error(`lookupSupportedAzs returned ${availableAZs} but subnets have AZs ${subnets.map(s => s.availabilityZone)}`); } + return filteredSubnets.map(s => s.subnetId); + } + + /** + * Sanity checking when looking up AZs for an endpoint service, to make sure it won't fail + */ + private validateCanLookupSupportedAzs(subnets: ISubnet[], serviceName: string) { + + // Having any of these be true will cause the AZ lookup to fail at synthesis time + const agnosticAcct = Token.isUnresolved(this.stack.account); + const agnosticRegion = Token.isUnresolved(this.stack.region); + const agnosticService = Token.isUnresolved(serviceName); + + // Having subnets with Token AZs can cause the endpoint to be created with no subnets, failing at deployment time + const agnosticSubnets = subnets.some(s => Token.isUnresolved(s.availabilityZone)); + const agnosticSubnetList = Token.isUnresolved(subnets.map(s => s.availabilityZone)); - // Check if lookup is impossible and throw an appropriate error // Context provider cannot make an AWS call without an account/region - if ((agnosticAcct || agnosticRegion) && lookupSupportedAzs) { + if (agnosticAcct || agnosticRegion) { throw new Error('Cannot look up VPC endpoint availability zones if account/region are not specified'); } - // Context provider doesn't know the name of the service if there is a Token - // in the name - const agnosticService = Token.isUnresolved(lookupServiceName); - if (agnosticService && lookupSupportedAzs) { - throw new Error(`Cannot lookup AZs for a service name with a Token: ${props.service.name}`); + + // The AWS call will fail if there is a Token in the service name + if (agnosticService) { + throw new Error(`Cannot lookup AZs for a service name with a Token: ${serviceName}`); } - // Here we do the actual lookup for AZs, if told to do so - let subnets; - if (lookupSupportedAzs) { - const availableAZs = this.availableAvailabilityZones(lookupServiceName); - subnets = subnetSelection.subnets.filter(s => availableAZs.includes(s.availabilityZone)); - } else { - subnets = subnetSelection.subnets; + // The AWS call return strings for AZs, like us-east-1a, us-east-1b, etc + // If the subnet AZs are Tokens, a string comparison between the subnet AZs and the AZs from the AWS call + // will not match + if (agnosticSubnets || agnosticSubnetList) { + const agnostic = subnets.filter(s => Token.isUnresolved(s.availabilityZone)); + throw new Error(`lookupSupportedAzs cannot filter on subnets with Token AZs: ${agnostic}`); } - const subnetIds = subnets.map(s => s.subnetId); - return subnetIds; } private availableAvailabilityZones(serviceName: string): string[] { diff --git a/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts b/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts index 68ff81c7f8b86..fa51431963fe5 100644 --- a/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts @@ -1,7 +1,7 @@ import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; import { AnyPrincipal, PolicyStatement } from '@aws-cdk/aws-iam'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; -import { ContextProvider, Stack } from '@aws-cdk/core'; +import { ContextProvider, Fn, Stack } from '@aws-cdk/core'; import { nodeunitShim, Test } from 'nodeunit-shim'; // eslint-disable-next-line max-len import { GatewayVpcEndpoint, GatewayVpcEndpointAwsService, InterfaceVpcEndpoint, InterfaceVpcEndpointAwsService, InterfaceVpcEndpointService, SecurityGroup, SubnetType, Vpc } from '../lib'; @@ -537,5 +537,75 @@ nodeunitShim({ test.done(); }, + 'lookupSupportedAzs fails if account is unresolved'(test: Test) { + // GIVEN + const stack = new Stack(undefined, 'TestStack', { env: { region: 'us-east-1' } }); + const vpc = new Vpc(stack, 'VPC'); + // WHEN + test.throws(() =>vpc.addInterfaceEndpoint('YourService', { + service: { + name: 'com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc', + port: 443, + }, + lookupSupportedAzs: true, + })); + test.done(); + }, + 'lookupSupportedAzs fails if region is unresolved'(test: Test) { + // GIVEN + const stack = new Stack(undefined, 'TestStack', { env: { account: '123456789012' } }); + const vpc = new Vpc(stack, 'VPC'); + // WHEN + test.throws(() =>vpc.addInterfaceEndpoint('YourService', { + service: { + name: 'com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc', + port: 443, + }, + lookupSupportedAzs: true, + })); + test.done(); + }, + 'lookupSupportedAzs fails if subnet AZs are tokens'(test: Test) { + // GIVEN + const stack = new Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' } }); + const tokenAZs = [ + 'us-east-1a', + Fn.select(1, Fn.getAzs()), + Fn.select(2, Fn.getAzs()), + ]; + // Setup context for stack AZs + stack.node.setContext( + ContextProvider.getKey(stack, { + provider: cxschema.ContextProvider.AVAILABILITY_ZONE_PROVIDER, + }).key, + tokenAZs); + const vpc = new Vpc(stack, 'VPC'); + + // WHEN + test.throws(() =>vpc.addInterfaceEndpoint('YourService', { + service: { + name: 'com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc', + port: 443, + }, + lookupSupportedAzs: true, + })); + test.done(); + }, + 'vpc endpoint fails if no subnets provided'(test: Test) { + // GIVEN + const stack = new Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' } }); + const vpc = new Vpc(stack, 'VPC'); + // WHEN + test.throws(() =>vpc.addInterfaceEndpoint('YourService', { + service: { + name: 'com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc', + port: 443, + }, + subnets: vpc.selectSubnets({ + subnets: [], + }), + })); + test.done(); + }, }, }); diff --git a/packages/@aws-cdk/aws-eks/README.md b/packages/@aws-cdk/aws-eks/README.md index 0b92c183fb786..27f9eda6f0d16 100644 --- a/packages/@aws-cdk/aws-eks/README.md +++ b/packages/@aws-cdk/aws-eks/README.md @@ -401,6 +401,8 @@ terminated. > > Chart Version: [0.9.5](https://github.com/aws/eks-charts/blob/v0.0.28/stable/aws-node-termination-handler/Chart.yaml) +To disable the installation of the termination handler, set the `spotInterruptHandler` property to `false`. This applies both to `addAutoScalingGroupCapacity` and `connectAutoScalingGroupCapacity`. + #### Bottlerocket [Bottlerocket](https://aws.amazon.com/bottlerocket/) is a Linux-based open-source operating system that is purpose-built by Amazon Web Services for running containers on virtual machines or bare metal hosts. diff --git a/packages/@aws-cdk/aws-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts index 5e7fb32ecacd3..92a32d3f0747a 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -1181,6 +1181,7 @@ export class Cluster extends ClusterBase { bootstrapOptions: options.bootstrapOptions, bootstrapEnabled: options.bootstrapEnabled, machineImageType: options.machineImageType, + spotInterruptHandler: options.spotInterruptHandler, }); if (nodeTypeForInstanceType(options.instanceType) === NodeType.INFERENTIA) { @@ -1290,8 +1291,9 @@ export class Cluster extends ClusterBase { }); } + const addSpotInterruptHandler = options.spotInterruptHandler ?? true; // if this is an ASG with spot instances, install the spot interrupt handler (only if kubectl is enabled). - if (autoScalingGroup.spotPrice) { + if (autoScalingGroup.spotPrice && addSpotInterruptHandler) { this.addSpotInterruptHandler(); } } @@ -1580,6 +1582,14 @@ export interface AutoScalingGroupCapacityOptions extends autoscaling.CommonAutoS * @default MachineImageType.AMAZON_LINUX_2 */ readonly machineImageType?: MachineImageType; + + /** + * Installs the AWS spot instance interrupt handler on the cluster if it's not + * already added. Only relevant if `spotPrice` is used. + * + * @default true + */ + readonly spotInterruptHandler?: boolean; } /** @@ -1671,6 +1681,14 @@ export interface AutoScalingGroupOptions { * @default MachineImageType.AMAZON_LINUX_2 */ readonly machineImageType?: MachineImageType; + + /** + * Installs the AWS spot instance interrupt handler on the cluster if it's not + * already added. Only relevant if `spotPrice` is configured on the auto-scaling group. + * + * @default true + */ + readonly spotInterruptHandler?: boolean; } /** diff --git a/packages/@aws-cdk/aws-eks/lib/managed-nodegroup.ts b/packages/@aws-cdk/aws-eks/lib/managed-nodegroup.ts index 576957512cc6d..7d46fb00c6e92 100644 --- a/packages/@aws-cdk/aws-eks/lib/managed-nodegroup.ts +++ b/packages/@aws-cdk/aws-eks/lib/managed-nodegroup.ts @@ -226,10 +226,6 @@ export interface NodegroupProps extends NodegroupOptions { * The Nodegroup resource class */ export class Nodegroup extends Resource implements INodegroup { - /** - * Default instanceTypes - */ - public static readonly DEFAULT_INSTANCE_TYPES = [new InstanceType('t3.medium')]; /** * Import the Nodegroup from attributes */ @@ -291,16 +287,17 @@ export class Nodegroup extends Resource implements INodegroup { if (props.instanceType) { Annotations.of(this).addWarning('"instanceType" is deprecated and will be removed in the next major version. please use "instanceTypes" instead'); } - const instanceTypes = props.instanceTypes ?? (props.instanceType ? [props.instanceType] : Nodegroup.DEFAULT_INSTANCE_TYPES); - // get unique AMI types from instanceTypes - const uniqAmiTypes = getAmiTypes(instanceTypes); - // uniqAmiTypes.length should be at least 1 - if (uniqAmiTypes.length > 1) { - throw new Error('instanceTypes of different CPU architectures is not allowed'); - } - const determinedAmiType = uniqAmiTypes[0]; - if (props.amiType && props.amiType !== determinedAmiType) { - throw new Error(`The specified AMI does not match the instance types architecture, either specify ${determinedAmiType} or dont specify any`); + const instanceTypes = props.instanceTypes ?? (props.instanceType ? [props.instanceType] : undefined); + let expectedAmiType = undefined; + + if (instanceTypes && instanceTypes.length > 0) { + // if the user explicitly configured instance types, we can calculate the expected ami type. + expectedAmiType = getAmiType(instanceTypes); + + // if the user explicitly configured an ami type, make sure its the same as the expected one. + if (props.amiType && props.amiType !== expectedAmiType) { + throw new Error(`The specified AMI does not match the instance types architecture, either specify ${expectedAmiType} or dont specify any`); + } } if (!props.nodeRole) { @@ -321,13 +318,18 @@ export class Nodegroup extends Resource implements INodegroup { nodegroupName: props.nodegroupName, nodeRole: this.role.roleArn, subnets: this.cluster.vpc.selectSubnets(props.subnets).subnetIds, - // AmyType is not allowed by CFN when specifying an image id in your launch template. - amiType: props.launchTemplateSpec === undefined ? determinedAmiType : undefined, + + // if a launch template is configured, we cannot apply a default since it + // might exist in the launch template as well, causing a deployment failure. + amiType: props.launchTemplateSpec !== undefined ? props.amiType : (props.amiType ?? expectedAmiType), + capacityType: props.capacityType ? props.capacityType.valueOf() : undefined, diskSize: props.diskSize, forceUpdateEnabled: props.forceUpdate ?? true, - instanceTypes: props.instanceTypes ? props.instanceTypes.map(t => t.toString()) : - props.instanceType ? [props.instanceType.toString()] : undefined, + + // note that we don't check if a launch template is configured here (even though it might configure instance types as well) + // because this doesn't have a default value, meaning the user had to explicitly configure this. + instanceTypes: instanceTypes?.map(t => t.toString()), labels: props.labels, releaseVersion: props.releaseVersion, remoteAccess: props.remoteAccess ? { @@ -392,8 +394,16 @@ function getAmiTypeForInstanceType(instanceType: InstanceType) { NodegroupAmiType.AL2_X86_64; } -function getAmiTypes(instanceType: InstanceType[]) { - const amiTypes = instanceType.map(i =>getAmiTypeForInstanceType(i)); - // retuen unique AMI types - return [...new Set(amiTypes)]; +// this function examines the CPU architecture of every instance type and determines +// what ami type is compatible for all of them. it either throws or produces a single value because +// instance types of different CPU architectures are not supported. +function getAmiType(instanceTypes: InstanceType[]) { + const amiTypes = new Set(instanceTypes.map(i => getAmiTypeForInstanceType(i))); + if (amiTypes.size == 0) { // protective code, the current implementation will never result in this. + throw new Error(`Cannot determine any ami type comptaible with instance types: ${instanceTypes.map(i => i.toString).join(',')}`); + } + if (amiTypes.size > 1) { + throw new Error('instanceTypes of different CPU architectures is not allowed'); + } + return amiTypes.values().next().value; } diff --git a/packages/@aws-cdk/aws-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index 19187f4844c0b..d1d449e2c2606 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -75,7 +75,7 @@ "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", "@types/yaml": "1.9.6", - "aws-sdk": "^2.822.0", + "aws-sdk": "^2.824.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-eks/test/test.cluster.ts b/packages/@aws-cdk/aws-eks/test/test.cluster.ts index 4306d042acf1b..907cd12ec0adb 100644 --- a/packages/@aws-cdk/aws-eks/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-eks/test/test.cluster.ts @@ -188,6 +188,31 @@ export = { }, + 'spot interrupt handler is not added if spotInterruptHandler is false when connecting self-managed nodes'(test: Test) { + + // GIVEN + const { stack, vpc } = testFixture(); + const cluster = new eks.Cluster(stack, 'Cluster', { + vpc, + defaultCapacity: 0, + version: CLUSTER_VERSION, + prune: false, + }); + + const selfManaged = new asg.AutoScalingGroup(stack, 'self-managed', { + instanceType: new ec2.InstanceType('t2.medium'), + vpc: vpc, + machineImage: new ec2.AmazonLinuxImage(), + spotPrice: '0.1', + }); + + // WHEN + cluster.connectAutoScalingGroupCapacity(selfManaged, { spotInterruptHandler: false }); + + test.equal(cluster.node.findAll().filter(c => c.node.id === 'chart-spot-interrupt-handler').length, 0); + test.done(); + }, + 'throws when a non cdk8s chart construct is added as cdk8s chart'(test: Test) { const { stack } = testFixture(); @@ -1292,6 +1317,23 @@ export = { test.done(); }, + 'interrupt handler is not added when spotInterruptHandler is false'(test: Test) { + // GIVEN + const { stack } = testFixtureNoVpc(); + const cluster = new eks.Cluster(stack, 'Cluster', { defaultCapacity: 0, version: CLUSTER_VERSION, prune: false }); + + // WHEN + cluster.addAutoScalingGroupCapacity('MyCapcity', { + instanceType: new ec2.InstanceType('m3.xlarge'), + spotPrice: '0.01', + spotInterruptHandler: false, + }); + + // THEN + test.equal(cluster.node.findAll().filter(c => c.node.id === 'chart-spot-interrupt-handler').length, 0); + test.done(); + }, + 'its possible to add two capacities with spot instances and only one stop handler will be installed'(test: Test) { // GIVEN const { stack } = testFixtureNoVpc(); diff --git a/packages/@aws-cdk/aws-eks/test/test.nodegroup.ts b/packages/@aws-cdk/aws-eks/test/test.nodegroup.ts index ff962572a6f92..241482d469c65 100644 --- a/packages/@aws-cdk/aws-eks/test/test.nodegroup.ts +++ b/packages/@aws-cdk/aws-eks/test/test.nodegroup.ts @@ -10,6 +10,92 @@ import { testFixture } from './util'; const CLUSTER_VERSION = eks.KubernetesVersion.V1_18; export = { + + 'default ami type is not applied when launch template is configured'(test: Test) { + + // GIVEN + const { stack, vpc } = testFixture(); + + const launchTemplate = new ec2.CfnLaunchTemplate(stack, 'LaunchTemplate', { + launchTemplateData: { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.C5, ec2.InstanceSize.MEDIUM).toString(), + }, + }); + + // WHEN + const cluster = new eks.Cluster(stack, 'Cluster', { + vpc, + defaultCapacity: 0, + version: CLUSTER_VERSION, + }); + new eks.Nodegroup(stack, 'Nodegroup', { + cluster, + instanceTypes: [ec2.InstanceType.of(ec2.InstanceClass.C5, ec2.InstanceSize.LARGE)], + launchTemplateSpec: { + id: launchTemplate.ref, + version: launchTemplate.attrLatestVersionNumber, + }, + }); + + // THEN + test.equal(expect(stack).value.Resources.Nodegroup62B4B2C1.Properties.AmiType, undefined); + test.done(); + }, + + 'explicit ami type is applied even when launch template is configured'(test: Test) { + + // GIVEN + const { stack, vpc } = testFixture(); + + const launchTemplate = new ec2.CfnLaunchTemplate(stack, 'LaunchTemplate', { + launchTemplateData: { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.C5, ec2.InstanceSize.MEDIUM).toString(), + }, + }); + + // WHEN + const cluster = new eks.Cluster(stack, 'Cluster', { + vpc, + defaultCapacity: 0, + version: CLUSTER_VERSION, + }); + new eks.Nodegroup(stack, 'Nodegroup', { + cluster, + amiType: eks.NodegroupAmiType.AL2_X86_64, + launchTemplateSpec: { + id: launchTemplate.ref, + version: launchTemplate.attrLatestVersionNumber, + }, + }); + + // THEN + test.equal(expect(stack).value.Resources.Nodegroup62B4B2C1.Properties.AmiType, 'AL2_x86_64'); + test.done(); + }, + + 'ami type is taken as is when no instance types are configured'(test: Test) { + + // GIVEN + const { stack, vpc } = testFixture(); + + // WHEN + const cluster = new eks.Cluster(stack, 'Cluster', { + vpc, + defaultCapacity: 0, + version: CLUSTER_VERSION, + }); + new eks.Nodegroup(stack, 'Nodegroup', { + cluster, + amiType: eks.NodegroupAmiType.AL2_X86_64_GPU, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::EKS::Nodegroup', { + AmiType: 'AL2_x86_64_GPU', + })); + test.done(); + }, + 'create nodegroup correctly'(test: Test) { // GIVEN const { stack, vpc } = testFixture(); 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 230c02f7ab84f..1cd8a91c932aa 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -653,7 +653,7 @@ class ImportedApplicationListener extends ExternalApplicationListener { if (props.securityGroup) { securityGroup = props.securityGroup; } else if (props.securityGroupId) { - securityGroup = ec2.SecurityGroup.fromSecurityGroupId(scope, 'SecurityGroup', props.securityGroupId, { + securityGroup = ec2.SecurityGroup.fromSecurityGroupId(this, 'SecurityGroup', props.securityGroupId, { allowAllOutbound: props.securityGroupAllowsAllOutbound, }); } else { diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 480361c1358f6..668086dab5372 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -1474,6 +1474,36 @@ export class Domain extends DomainBase implements IDomain { }); } + const logPublishing: Record = {}; + + if (this.appLogGroup) { + logPublishing.ES_APPLICATION_LOGS = { + enabled: true, + cloudWatchLogsLogGroupArn: this.appLogGroup.logGroupArn, + }; + } + + if (this.slowSearchLogGroup) { + logPublishing.SEARCH_SLOW_LOGS = { + enabled: true, + cloudWatchLogsLogGroupArn: this.slowSearchLogGroup.logGroupArn, + }; + } + + if (this.slowIndexLogGroup) { + logPublishing.INDEX_SLOW_LOGS = { + enabled: true, + cloudWatchLogsLogGroupArn: this.slowIndexLogGroup.logGroupArn, + }; + } + + if (this.auditLogGroup) { + logPublishing.AUDIT_LOGS = { + enabled: this.auditLogGroup != null, + cloudWatchLogsLogGroupArn: this.auditLogGroup?.logGroupArn, + }; + } + // Create the domain this.domain = new CfnDomain(this, 'Resource', { domainName: this.physicalName, @@ -1506,24 +1536,7 @@ export class Domain extends DomainBase implements IDomain { : undefined, }, nodeToNodeEncryptionOptions: { enabled: nodeToNodeEncryptionEnabled }, - logPublishingOptions: { - AUDIT_LOGS: { - enabled: this.auditLogGroup != null, - cloudWatchLogsLogGroupArn: this.auditLogGroup?.logGroupArn, - }, - ES_APPLICATION_LOGS: { - enabled: this.appLogGroup != null, - cloudWatchLogsLogGroupArn: this.appLogGroup?.logGroupArn, - }, - SEARCH_SLOW_LOGS: { - enabled: this.slowSearchLogGroup != null, - cloudWatchLogsLogGroupArn: this.slowSearchLogGroup?.logGroupArn, - }, - INDEX_SLOW_LOGS: { - enabled: this.slowIndexLogGroup != null, - cloudWatchLogsLogGroupArn: this.slowIndexLogGroup?.logGroupArn, - }, - }, + logPublishingOptions: logPublishing, cognitoOptions: { enabled: props.cognitoKibanaAuth != null, identityPoolId: props.cognitoKibanaAuth?.identityPoolId, diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index affa1d45e4477..ff85a85e218f8 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -89,18 +89,10 @@ test('minimal example renders correctly', () => { Enabled: false, }, LogPublishingOptions: { - AUDIT_LOGS: { - Enabled: false, - }, - ES_APPLICATION_LOGS: { - Enabled: false, - }, - SEARCH_SLOW_LOGS: { - Enabled: false, - }, - INDEX_SLOW_LOGS: { - Enabled: false, - }, + AUDIT_LOGS: assert.ABSENT, + ES_APPLICATION_LOGS: assert.ABSENT, + SEARCH_SLOW_LOGS: assert.ABSENT, + INDEX_SLOW_LOGS: assert.ABSENT, }, NodeToNodeEncryptionOptions: { Enabled: false, @@ -133,9 +125,6 @@ describe('log groups', () => { expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { LogPublishingOptions: { - ES_APPLICATION_LOGS: { - Enabled: false, - }, SEARCH_SLOW_LOGS: { CloudWatchLogsLogGroupArn: { 'Fn::GetAtt': [ @@ -145,9 +134,9 @@ describe('log groups', () => { }, Enabled: true, }, - INDEX_SLOW_LOGS: { - Enabled: false, - }, + AUDIT_LOGS: assert.ABSENT, + ES_APPLICATION_LOGS: assert.ABSENT, + INDEX_SLOW_LOGS: assert.ABSENT, }, }); }); @@ -162,12 +151,6 @@ describe('log groups', () => { expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { LogPublishingOptions: { - ES_APPLICATION_LOGS: { - Enabled: false, - }, - SEARCH_SLOW_LOGS: { - Enabled: false, - }, INDEX_SLOW_LOGS: { CloudWatchLogsLogGroupArn: { 'Fn::GetAtt': [ @@ -177,6 +160,9 @@ describe('log groups', () => { }, Enabled: true, }, + AUDIT_LOGS: assert.ABSENT, + ES_APPLICATION_LOGS: assert.ABSENT, + SEARCH_SLOW_LOGS: assert.ABSENT, }, }); }); @@ -200,12 +186,9 @@ describe('log groups', () => { }, Enabled: true, }, - SEARCH_SLOW_LOGS: { - Enabled: false, - }, - INDEX_SLOW_LOGS: { - Enabled: false, - }, + AUDIT_LOGS: assert.ABSENT, + SEARCH_SLOW_LOGS: assert.ABSENT, + INDEX_SLOW_LOGS: assert.ABSENT, }, }); }); @@ -237,15 +220,9 @@ describe('log groups', () => { }, Enabled: true, }, - ES_APPLICATION_LOGS: { - Enabled: false, - }, - SEARCH_SLOW_LOGS: { - Enabled: false, - }, - INDEX_SLOW_LOGS: { - Enabled: false, - }, + ES_APPLICATION_LOGS: assert.ABSENT, + SEARCH_SLOW_LOGS: assert.ABSENT, + INDEX_SLOW_LOGS: assert.ABSENT, }, }); }); @@ -296,6 +273,7 @@ describe('log groups', () => { }, Enabled: true, }, + AUDIT_LOGS: assert.ABSENT, }, }); expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { @@ -327,6 +305,7 @@ describe('log groups', () => { }, Enabled: true, }, + AUDIT_LOGS: assert.ABSENT, }, }); }); @@ -385,12 +364,6 @@ describe('log groups', () => { expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { LogPublishingOptions: { - AUDIT_LOGS: { - Enabled: false, - }, - ES_APPLICATION_LOGS: { - Enabled: false, - }, SEARCH_SLOW_LOGS: { CloudWatchLogsLogGroupArn: { 'Fn::GetAtt': [ @@ -400,9 +373,9 @@ describe('log groups', () => { }, Enabled: true, }, - INDEX_SLOW_LOGS: { - Enabled: false, - }, + AUDIT_LOGS: assert.ABSENT, + ES_APPLICATION_LOGS: assert.ABSENT, + INDEX_SLOW_LOGS: assert.ABSENT, }, }); }); @@ -420,15 +393,6 @@ describe('log groups', () => { expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { LogPublishingOptions: { - AUDIT_LOGS: { - Enabled: false, - }, - ES_APPLICATION_LOGS: { - Enabled: false, - }, - SEARCH_SLOW_LOGS: { - Enabled: false, - }, INDEX_SLOW_LOGS: { CloudWatchLogsLogGroupArn: { 'Fn::GetAtt': [ @@ -438,6 +402,9 @@ describe('log groups', () => { }, Enabled: true, }, + AUDIT_LOGS: assert.ABSENT, + ES_APPLICATION_LOGS: assert.ABSENT, + SEARCH_SLOW_LOGS: assert.ABSENT, }, }); }); @@ -455,9 +422,6 @@ describe('log groups', () => { expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { LogPublishingOptions: { - AUDIT_LOGS: { - Enabled: false, - }, ES_APPLICATION_LOGS: { CloudWatchLogsLogGroupArn: { 'Fn::GetAtt': [ @@ -467,12 +431,9 @@ describe('log groups', () => { }, Enabled: true, }, - SEARCH_SLOW_LOGS: { - Enabled: false, - }, - INDEX_SLOW_LOGS: { - Enabled: false, - }, + AUDIT_LOGS: assert.ABSENT, + SEARCH_SLOW_LOGS: assert.ABSENT, + INDEX_SLOW_LOGS: assert.ABSENT, }, }); }); @@ -507,15 +468,9 @@ describe('log groups', () => { }, Enabled: true, }, - ES_APPLICATION_LOGS: { - Enabled: false, - }, - SEARCH_SLOW_LOGS: { - Enabled: false, - }, - INDEX_SLOW_LOGS: { - Enabled: false, - }, + ES_APPLICATION_LOGS: assert.ABSENT, + SEARCH_SLOW_LOGS: assert.ABSENT, + INDEX_SLOW_LOGS: assert.ABSENT, }, }); }); diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.advancedsecurity.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.advancedsecurity.expected.json index a4ec48af68521..e919ee6365e8e 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.advancedsecurity.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.advancedsecurity.expected.json @@ -40,20 +40,7 @@ "EncryptionAtRestOptions": { "Enabled": true }, - "LogPublishingOptions": { - "AUDIT_LOGS": { - "Enabled": false - }, - "ES_APPLICATION_LOGS": { - "Enabled": false - }, - "SEARCH_SLOW_LOGS": { - "Enabled": false - }, - "INDEX_SLOW_LOGS": { - "Enabled": false - } - }, + "LogPublishingOptions": {}, "NodeToNodeEncryptionOptions": { "Enabled": true } 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 f987bec734004..fafc653e73740 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 @@ -211,9 +211,6 @@ } }, "LogPublishingOptions": { - "AUDIT_LOGS": { - "Enabled": false - }, "ES_APPLICATION_LOGS": { "CloudWatchLogsLogGroupArn": { "Fn::GetAtt": [ @@ -231,9 +228,6 @@ ] }, "Enabled": true - }, - "INDEX_SLOW_LOGS": { - "Enabled": false } }, "NodeToNodeEncryptionOptions": { @@ -442,13 +436,13 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs12.x", "Timeout": 120 }, 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 a6a6dd2b0d37f..6c782aee20cc9 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json @@ -157,9 +157,6 @@ "Enabled": true }, "LogPublishingOptions": { - "AUDIT_LOGS": { - "Enabled": false - }, "ES_APPLICATION_LOGS": { "CloudWatchLogsLogGroupArn": { "Fn::GetAtt": [ @@ -177,9 +174,6 @@ ] }, "Enabled": true - }, - "INDEX_SLOW_LOGS": { - "Enabled": false } }, "NodeToNodeEncryptionOptions": { @@ -358,13 +352,13 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs12.x", "Timeout": 120 }, @@ -529,9 +523,6 @@ "Enabled": true }, "LogPublishingOptions": { - "AUDIT_LOGS": { - "Enabled": false - }, "ES_APPLICATION_LOGS": { "CloudWatchLogsLogGroupArn": { "Fn::GetAtt": [ @@ -549,9 +540,6 @@ ] }, "Enabled": true - }, - "INDEX_SLOW_LOGS": { - "Enabled": false } }, "NodeToNodeEncryptionOptions": { 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 99ca282a3469a..b55ac9e14df69 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 @@ -4,8 +4,8 @@ "Type": "AWS::SecretsManager::Secret", "Properties": { "GenerateSecretString": { - "GenerateStringKey": "password", "ExcludeCharacters": "{}'\\*[]()`", + "GenerateStringKey": "password", "SecretStringTemplate": "{\"username\":\"admin\"}" } } @@ -54,20 +54,7 @@ "EncryptionAtRestOptions": { "Enabled": true }, - "LogPublishingOptions": { - "AUDIT_LOGS": { - "Enabled": false - }, - "ES_APPLICATION_LOGS": { - "Enabled": false - }, - "SEARCH_SLOW_LOGS": { - "Enabled": false - }, - "INDEX_SLOW_LOGS": { - "Enabled": false - } - }, + "LogPublishingOptions": {}, "NodeToNodeEncryptionOptions": { "Enabled": true } @@ -297,4 +284,4 @@ "Description": "Artifact hash for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/package.json b/packages/@aws-cdk/aws-events-targets/package.json index 35fd3ce5e0034..affbfc3d5c1c0 100644 --- a/packages/@aws-cdk/aws-events-targets/package.json +++ b/packages/@aws-cdk/aws-events-targets/package.json @@ -76,7 +76,7 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", - "aws-sdk": "^2.822.0", + "aws-sdk": "^2.824.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-iam/README.md b/packages/@aws-cdk/aws-iam/README.md index a676af6352cf2..d9488e7d081c8 100644 --- a/packages/@aws-cdk/aws-iam/README.md +++ b/packages/@aws-cdk/aws-iam/README.md @@ -320,6 +320,34 @@ const provider = new iam.OpenIdConnectProvider(this, 'MyProvider', { const principal = new iam.OpenIdConnectPrincipal(provider); ``` +## Users + +IAM manages users for your AWS account. To create a new user: + +```ts +const user = new User(this, 'MyUser'); +``` + +To import an existing user by name [with path](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-friendly-names): + +```ts +const user = User.fromUserName(stack, 'MyImportedUserByName', 'johnsmith'); +``` + +To import an existing user by ARN: + +```ts +const user = User.fromUserArn(this, 'MyImportedUserByArn', 'arn:aws:iam::123456789012:user/johnsmith'); +``` + +To import an existing user by attributes: + +```ts +const user = User.fromUserAttributes(stack, 'MyImportedUserByAttributes', { + userArn: 'arn:aws:iam::123456789012:user/johnsmith', +}); +``` + ## Features * Policy name uniqueness is enforced. If two policies by the same name are attached to the same diff --git a/packages/@aws-cdk/aws-iam/lib/user.ts b/packages/@aws-cdk/aws-iam/lib/user.ts index a8c3b61443771..5c8f6418a9bb8 100644 --- a/packages/@aws-cdk/aws-iam/lib/user.ts +++ b/packages/@aws-cdk/aws-iam/lib/user.ts @@ -1,4 +1,4 @@ -import { Aws, Lazy, Resource, SecretValue, Stack } from '@aws-cdk/core'; +import { Arn, Aws, Lazy, Resource, SecretValue, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { IGroup } from './group'; import { CfnUser } from './iam.generated'; @@ -119,6 +119,18 @@ export interface UserProps { readonly passwordResetRequired?: boolean; } +/** + * Represents a user defined outside of this stack. + */ +export interface UserAttributes { + /** + * The ARN of the user. + * + * Format: arn::iam:::user/ + */ + readonly userArn: string; +} + /** * Define a new IAM user */ @@ -131,20 +143,42 @@ export class User extends Resource implements IIdentity, IUser { * @param userName the username of the existing user to import */ public static fromUserName(scope: Construct, id: string, userName: string): IUser { - const arn = Stack.of(scope).formatArn({ + const userArn = Stack.of(scope).formatArn({ service: 'iam', region: '', resource: 'user', resourceName: userName, }); + return User.fromUserAttributes(scope, id, { userArn }); + } + + /** + * Import an existing user given a user ARN. + * + * @param scope construct scope + * @param id construct id + * @param userArn the ARN of an existing user to import + */ + public static fromUserArn(scope: Construct, id: string, userArn: string): IUser { + return User.fromUserAttributes(scope, id, { userArn }); + } + + /** + * Import an existing user given user attributes. + * + * @param scope construct scope + * @param id construct id + * @param attrs the attributes of the user to import + */ + public static fromUserAttributes(scope: Construct, id: string, attrs: UserAttributes): IUser { class Import extends Resource implements IUser { public readonly grantPrincipal: IPrincipal = this; public readonly principalAccount = Aws.ACCOUNT_ID; - public readonly userName: string = userName; - public readonly userArn: string = arn; + public readonly userName: string = Arn.extractResourceName(attrs.userArn, 'user'); + public readonly userArn: string = attrs.userArn; public readonly assumeRoleAction: string = 'sts:AssumeRole'; - public readonly policyFragment: PrincipalPolicyFragment = new ArnPrincipal(arn).policyFragment; + public readonly policyFragment: PrincipalPolicyFragment = new ArnPrincipal(attrs.userArn).policyFragment; private readonly attachedPolicies = new AttachedPolicies(); private defaultPolicy?: Policy; diff --git a/packages/@aws-cdk/aws-iam/test/integ.user.expected.json b/packages/@aws-cdk/aws-iam/test/integ.user.expected.json index 2c4bc6c9b52c0..a57b3db4c6f32 100644 --- a/packages/@aws-cdk/aws-iam/test/integ.user.expected.json +++ b/packages/@aws-cdk/aws-iam/test/integ.user.expected.json @@ -4,11 +4,22 @@ "Type": "AWS::IAM::User", "Properties": { "LoginProfile": { - "Password": "1234", + "Password": "Test1234567890!", "PasswordResetRequired": true }, "UserName": "benisrae" } } + }, + "Outputs": { + "NameForUserImportedByArn": { + "Value": "rossrhodes" + }, + "NameForUserImportedByAttributes": { + "Value": "johndoe" + }, + "NameForUserImportedByName": { + "Value": "janedoe" + } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/test/integ.user.ts b/packages/@aws-cdk/aws-iam/test/integ.user.ts index 198f3ecb77c4c..7f8d00695742c 100644 --- a/packages/@aws-cdk/aws-iam/test/integ.user.ts +++ b/packages/@aws-cdk/aws-iam/test/integ.user.ts @@ -1,4 +1,4 @@ -import { App, SecretValue, Stack } from '@aws-cdk/core'; +import { App, CfnOutput, SecretValue, Stack } from '@aws-cdk/core'; import { User } from '../lib'; const app = new App(); @@ -7,8 +7,18 @@ const stack = new Stack(app, 'aws-cdk-iam-user'); new User(stack, 'MyUser', { userName: 'benisrae', - password: SecretValue.plainText('1234'), + password: SecretValue.plainText('Test1234567890!'), passwordResetRequired: true, }); +const userImportedByArn = User.fromUserArn(stack, 'ImportedUserByArn', 'arn:aws:iam::123456789012:user/rossrhodes'); +const userImportedByAttributes = User.fromUserAttributes(stack, 'ImportedUserByAttributes', { + userArn: 'arn:aws:iam::123456789012:user/johndoe', +}); +const userImportedByName = User.fromUserName(stack, 'ImportedUserByName', 'janedoe'); + +new CfnOutput(stack, 'NameForUserImportedByArn', { value: userImportedByArn.userName }); +new CfnOutput(stack, 'NameForUserImportedByAttributes', { value: userImportedByAttributes.userName }); +new CfnOutput(stack, 'NameForUserImportedByName', { value: userImportedByName.userName }); + app.synth(); diff --git a/packages/@aws-cdk/aws-iam/test/user.test.ts b/packages/@aws-cdk/aws-iam/test/user.test.ts index 9908eeac2c6c7..4a59a86d4a45d 100644 --- a/packages/@aws-cdk/aws-iam/test/user.test.ts +++ b/packages/@aws-cdk/aws-iam/test/user.test.ts @@ -81,7 +81,7 @@ describe('IAM user', () => { }); }); - test('imported user has an ARN', () => { + test('user imported by user name has an ARN', () => { // GIVEN const stack = new Stack(); @@ -94,6 +94,32 @@ describe('IAM user', () => { }); }); + test('user imported by user ARN has a name', () => { + // GIVEN + const stack = new Stack(); + const userName = 'MyUserName'; + + // WHEN + const user = User.fromUserArn(stack, 'import', `arn:aws:iam::account-id:user/${userName}`); + + // THEN + expect(stack.resolve(user.userName)).toStrictEqual(userName); + }); + + test('user imported by user attributes has a name', () => { + // GIVEN + const stack = new Stack(); + const userName = 'MyUserName'; + + // WHEN + const user = User.fromUserAttributes(stack, 'import', { + userArn: `arn:aws:iam::account-id:user/${userName}`, + }); + + // THEN + expect(stack.resolve(user.userName)).toStrictEqual(userName); + }); + test('add to policy of imported user', () => { // GIVEN const stack = new Stack(); diff --git a/packages/@aws-cdk/aws-lambda-nodejs/README.md b/packages/@aws-cdk/aws-lambda-nodejs/README.md index 723ce157c6fbb..bfaa99eb0b243 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/README.md +++ b/packages/@aws-cdk/aws-lambda-nodejs/README.md @@ -143,6 +143,9 @@ new lambda.NodejsFunction(this, 'my-handler', { loader: { // Use the 'dataurl' loader for '.png' files '.png': 'dataurl', }, + define: { // Replace strings during build time + 'process.env.API_KEY': JSON.stringify('xxx-xxxx-xxx'), + }, logLevel: LogLevel.SILENT, // defaults to LogLevel.WARNING keepNames: true, // defaults to false tsconfig: 'custom-tsconfig.json' // use custom-tsconfig.json instead of default, diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts index 32c9336d96b5a..a6dcddde7709d 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts @@ -136,6 +136,7 @@ export class Bundling implements cdk.BundlingOptions { const npx = osPlatform === 'win32' ? 'npx.cmd' : 'npx'; const loaders = Object.entries(this.props.loader ?? {}); + const defines = Object.entries(this.props.define ?? {}); const esbuildCommand: string = [ npx, 'esbuild', @@ -147,6 +148,7 @@ export class Bundling implements cdk.BundlingOptions { ...this.props.sourceMap ? ['--sourcemap'] : [], ...this.externals.map(external => `--external:${external}`), ...loaders.map(([ext, name]) => `--loader:${ext}=${name}`), + ...defines.map(([key, value]) => `--define:${key}=${value}`), ...this.props.logLevel ? [`--log-level=${this.props.logLevel}`] : [], ...this.props.keepNames ? ['--keep-names'] : [], ...this.relativeTsconfigPath ? [`--tsconfig=${pathJoin(inputDir, this.relativeTsconfigPath)}`] : [], diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/types.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/types.ts index f1008dc7b0368..537523abb4f3b 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/types.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/types.ts @@ -138,6 +138,16 @@ export interface BundlingOptions { */ readonly environment?: { [key: string]: string; }; + /** + * Replace global identifiers with constant expressions. + * + * @example { 'process.env.DEBUG': 'true' } + * @example { 'process.env.API_KEY': JSON.stringify('xxx-xxxx-xxx') } + * + * @default - no replacements are made + */ + readonly define?: { [key: string]: string }; + /** * A list of modules that should be considered as externals (already available * in the runtime). diff --git a/packages/@aws-cdk/aws-lambda-nodejs/package.json b/packages/@aws-cdk/aws-lambda-nodejs/package.json index ba12c702466e4..75587e031f666 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/package.json +++ b/packages/@aws-cdk/aws-lambda-nodejs/package.json @@ -69,7 +69,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "delay": "4.4.0", - "esbuild": "^0.8.28", + "esbuild": "^0.8.31", "pkglint": "0.0.0" }, "dependencies": { 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 c665b862ad93f..bd69394ae757c 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts @@ -168,6 +168,10 @@ test('esbuild bundling with esbuild options', () => { banner: '/* comments */', footer: '/* comments */', forceDockerBundling: true, + define: { + 'DEBUG': 'true', + 'process.env.KEY': JSON.stringify('VALUE'), + }, }); // Correctly bundles with esbuild @@ -180,6 +184,7 @@ test('esbuild bundling with esbuild options', () => { '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', '--metafile=/asset-output/index.meta.json --banner=\'/* comments */\' --footer=\'/* comments */\'', ].join(' '), diff --git a/packages/@aws-cdk/aws-lambda/lib/code.ts b/packages/@aws-cdk/aws-lambda/lib/code.ts index fef200a9a6c9c..b2323008bb5fd 100644 --- a/packages/@aws-cdk/aws-lambda/lib/code.ts +++ b/packages/@aws-cdk/aws-lambda/lib/code.ts @@ -248,6 +248,9 @@ export class AssetCode extends Code { path: this.path, ...this.options, }); + } else if (cdk.Stack.of(this.asset) !== cdk.Stack.of(scope)) { + throw new Error(`Asset is already associated with another stack '${cdk.Stack.of(this.asset).stackName}'. ` + + 'Create a new Code instance for every stack.'); } if (!this.asset.isZipArchive) { diff --git a/packages/@aws-cdk/aws-lambda/lib/function.ts b/packages/@aws-cdk/aws-lambda/lib/function.ts index 71f91b9fd76d3..0e56ee695d4e5 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function.ts @@ -603,7 +603,13 @@ export class Function extends FunctionBase { this.deadLetterQueue = this.buildDeadLetterQueue(props); - const UNDEFINED_MARKER = '$$$undefined'; + let fileSystemConfigs: CfnFunction.FileSystemConfigProperty[] | undefined = undefined; + if (props.filesystem) { + fileSystemConfigs = [{ + arn: props.filesystem.config.arn, + localMountPath: props.filesystem.config.localMountPath, + }]; + } const resource: CfnFunction = new CfnFunction(this, 'Resource', { functionName: this.physicalName, @@ -616,10 +622,10 @@ export class Function extends FunctionBase { imageUri: code.image?.imageUri, }, layers: Lazy.list({ produce: () => this.layers.map(layer => layer.layerVersionArn) }, { omitEmpty: true }), - handler: props.handler === Handler.FROM_IMAGE ? UNDEFINED_MARKER : props.handler, + handler: props.handler === Handler.FROM_IMAGE ? undefined : props.handler, timeout: props.timeout && props.timeout.toSeconds(), packageType: props.runtime === Runtime.FROM_IMAGE ? 'Image' : undefined, - runtime: props.runtime === Runtime.FROM_IMAGE ? UNDEFINED_MARKER : props.runtime?.name, + runtime: props.runtime === Runtime.FROM_IMAGE ? undefined : props.runtime.name, role: this.role.roleArn, // Uncached because calling '_checkEdgeCompatibility', which gets called in the resolve of another // Token, actually *modifies* the 'environment' map. @@ -634,17 +640,9 @@ export class Function extends FunctionBase { entryPoint: code.image?.entrypoint, }), kmsKeyArn: props.environmentEncryption?.keyArn, + fileSystemConfigs, }); - // since patching the CFN spec to make Runtime and Handler optional causes a - // change in the order of the JSON keys, which results in a change of - // function hash (and invalidation of all lambda functions everywhere), we - // are using a marker to indicate this fields needs to be erased using an - // escape hatch. this should be fixed once the new spec is published and a - // patch is no longer needed. - if (resource.runtime === UNDEFINED_MARKER) { resource.addPropertyOverride('Runtime', undefined); } - if (resource.handler === UNDEFINED_MARKER) { resource.addPropertyOverride('Handler', undefined); } - resource.node.addDependency(this.role); this.functionName = this.getResourceNameAttribute(resource.ref); @@ -695,15 +693,6 @@ export class Function extends FunctionBase { if (config.dependency) { this.node.addDependency(...config.dependency); } - - resource.addPropertyOverride('FileSystemConfigs', - [ - { - LocalMountPath: config.localMountPath, - Arn: config.arn, - }, - ], - ); } } diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index 4414239146e74..79ab230a1b09d 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -79,7 +79,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/aws-lambda": "^8.10.64", - "@types/lodash": "^4.14.165", + "@types/lodash": "^4.14.167", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda/test/code.test.ts b/packages/@aws-cdk/aws-lambda/test/code.test.ts index a822ba698697e..9b99c095c2467 100644 --- a/packages/@aws-cdk/aws-lambda/test/code.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/code.test.ts @@ -77,6 +77,26 @@ describe('code', () => { }, }, ResourcePart.CompleteDefinition); }); + + test('fails if asset is bound with a second stack', () => { + // GIVEN + const asset = lambda.Code.fromAsset(path.join(__dirname, 'my-lambda-handler')); + + const app = new cdk.App(); + const stack1 = new cdk.Stack(app, 'Stack1'); + new lambda.Function(stack1, 'Func', { + code: asset, + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'foom', + }); + + const stack2 = new cdk.Stack(app, 'Stack2'); + expect(() => new lambda.Function(stack2, 'Func', { + code: asset, + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'foom', + })).toThrow(/already associated/); + }); }); describe('lambda.Code.fromCfnParameters', () => { diff --git a/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json index f123d24edf60a..6c8c451a2e736 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json +++ b/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json @@ -176,7 +176,7 @@ "Arn" ] }, - "Runtime": "nodejs10.x" + "Runtime": "nodejs12.x" }, "DependsOn": [ "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB", diff --git a/packages/@aws-cdk/aws-logs/lib/log-retention.ts b/packages/@aws-cdk/aws-logs/lib/log-retention.ts index 97ea68276b00c..44f36e5891cbe 100644 --- a/packages/@aws-cdk/aws-logs/lib/log-retention.ts +++ b/packages/@aws-cdk/aws-logs/lib/log-retention.ts @@ -154,7 +154,7 @@ class LogRetentionFunction extends cdk.Construct { type: 'AWS::Lambda::Function', properties: { Handler: 'index.handler', - Runtime: 'nodejs10.x', + Runtime: 'nodejs12.x', Code: { S3Bucket: asset.s3BucketName, S3Key: asset.s3ObjectKey, diff --git a/packages/@aws-cdk/aws-logs/package.json b/packages/@aws-cdk/aws-logs/package.json index 3a7d78df5486b..fd6306ce2c8d0 100644 --- a/packages/@aws-cdk/aws-logs/package.json +++ b/packages/@aws-cdk/aws-logs/package.json @@ -74,7 +74,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.822.0", + "aws-sdk": "^2.824.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-logs/test/test.log-retention.ts b/packages/@aws-cdk/aws-logs/test/test.log-retention.ts index 1f1faa24350bf..1de0c4d7b777c 100644 --- a/packages/@aws-cdk/aws-logs/test/test.log-retention.ts +++ b/packages/@aws-cdk/aws-logs/test/test.log-retention.ts @@ -40,6 +40,11 @@ export = { ], })); + expect(stack).to(haveResource('AWS::Lambda::Function', { + Handler: 'index.handler', + Runtime: 'nodejs12.x', + })); + expect(stack).to(haveResource('Custom::LogRetention', { 'ServiceToken': { 'Fn::GetAtt': [ 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 9952517f5e00e..ca91f92e62644 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 @@ -986,7 +986,7 @@ "Type": "AWS::Lambda::Function", "Properties": { "Handler": "index.handler", - "Runtime": "nodejs10.x", + "Runtime": "nodejs12.x", "Code": { "S3Bucket": { "Ref": "AssetParameters884431e2bc651d2b61bd699a29dc9684b0f66911f06bd3ed0635f854bf18e147S3BucketAE1150B3" diff --git a/packages/@aws-cdk/aws-route53/package.json b/packages/@aws-cdk/aws-route53/package.json index c3e12ca02f16b..cd66d64c90c43 100644 --- a/packages/@aws-cdk/aws-route53/package.json +++ b/packages/@aws-cdk/aws-route53/package.json @@ -74,7 +74,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.822.0", + "aws-sdk": "^2.824.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-s3-deployment/README.md b/packages/@aws-cdk/aws-s3-deployment/README.md index 9530ca3b61c34..2509372e55c11 100644 --- a/packages/@aws-cdk/aws-s3-deployment/README.md +++ b/packages/@aws-cdk/aws-s3-deployment/README.md @@ -220,8 +220,8 @@ size of the AWS Lambda resource handler. ## Development The custom resource is implemented in Python 3.6 in order to be able to leverage -the AWS CLI for "aws sync". The code is under [`lambda/src`](./lambda/src) and -unit tests are under [`lambda/test`](./lambda/test). +the AWS CLI for "aws sync". The code is under [`lib/lambda`](https://github.com/aws/aws-cdk/tree/master/packages/%40aws-cdk/aws-s3-deployment/lib/lambda) and +unit tests are under [`test/lambda`](https://github.com/aws/aws-cdk/tree/master/packages/%40aws-cdk/aws-s3-deployment/test/lambda). This package requires Python 3.6 during build time in order to create the custom resource Lambda bundle and test it. It also relies on a few bash scripts, so diff --git a/packages/@aws-cdk/aws-sqs/package.json b/packages/@aws-cdk/aws-sqs/package.json index efb0210a98205..bc9cadc2527fd 100644 --- a/packages/@aws-cdk/aws-sqs/package.json +++ b/packages/@aws-cdk/aws-sqs/package.json @@ -75,7 +75,7 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.822.0", + "aws-sdk": "^2.824.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", 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 6fe1a11e34723..45457a1c377c8 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/evaluate-expression.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/evaluate-expression.ts @@ -72,7 +72,7 @@ export class EvaluateExpression extends sfn.TaskStateBase { * @internal */ protected _renderTask(): any { - const matches = this.props.expression.match(/\$[.\[][.a-zA-Z[\]0-9]+/g); + const matches = this.props.expression.match(/\$[.\[][.a-zA-Z[\]0-9-_]+/g); let expressionAttributeValues = {}; if (matches) { 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-nodejs10.x-handler.test.ts index d110f97f47002..d42be11d7aaa4 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eval-nodejs10.x-handler.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eval-nodejs10.x-handler.test.ts @@ -58,3 +58,19 @@ test('with duplicated entries', async () => { const evaluated = await handler(event); expect(evaluated).toBe(2); }); + +test('with dash and underscore in path', async () => { + // GIVEN + const event: Event = { + expression: '$.a_b + $.c-d + $[_e]', + expressionAttributeValues: { + '$.a_b': 1, + '$.c-d': 2, + '$[_e]': 3, + }, + }; + + // THEN + const evaluated = await handler(event); + expect(evaluated).toBe(6); +}); 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 0bcc3cc3e98c3..679e817dfbead 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 @@ -62,3 +62,28 @@ test('expression does not contain paths', () => { }, }); }); + +test('with dash and underscore in path', () => { + // WHEN + const task = new tasks.EvaluateExpression(stack, 'Task', { + expression: '$.a_b + $.c-d + $[_e]', + }); + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + expect(stack).toHaveResource('AWS::StepFunctions::StateMachine', { + DefinitionString: { + 'Fn::Join': [ + '', + [ + '{"StartAt":"Task","States":{"Task":{"End":true,"Type":"Task","Resource":"', + { + 'Fn::GetAtt': ['Evala0d2ce44871b4e7487a1f5e63d7c3bdc4DAC06E1', 'Arn'], + }, + '","Parameters":{"expression":"$.a_b + $.c-d + $[_e]","expressionAttributeValues":{"$.a_b.$":"$.a_b","$.c-d.$":"$.c-d","$[_e].$":"$[_e]"}}}}}', + ], + ], + }, + }); +}); diff --git a/packages/@aws-cdk/aws-synthetics/README.md b/packages/@aws-cdk/aws-synthetics/README.md index 53ed300db49ab..fd2310ecaacf3 100644 --- a/packages/@aws-cdk/aws-synthetics/README.md +++ b/packages/@aws-cdk/aws-synthetics/README.md @@ -44,7 +44,7 @@ const canary = new synthetics.Canary(this, 'MyCanary', { code: synthetics.Code.fromAsset(path.join(__dirname, 'canary')), handler: 'index.handler', }), - runtime: synthetics.Runtime.SYNTHETICS_NODEJS_2_0, + runtime: synthetics.Runtime.SYNTHETICS_NODEJS_2_2, }); ``` @@ -84,7 +84,7 @@ The canary will automatically produce a CloudWatch Dashboard: ![UI Screenshot](images/ui-screenshot.png) -The Canary code will be executed in a lambda function created by Synthetics on your behalf. The Lambda function includes a custom [runtime](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_Library.html) provided by Synthetics. The provided runtime includes a variety of handy tools such as [Puppeteer](https://www.npmjs.com/package/puppeteer-core) and Chromium. The default runtime is `syn-nodejs-2.0`. +The Canary code will be executed in a lambda function created by Synthetics on your behalf. The Lambda function includes a custom [runtime](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_Library.html) provided by Synthetics. The provided runtime includes a variety of handy tools such as [Puppeteer](https://www.npmjs.com/package/puppeteer-core) (for nodejs based one) and Chromium. To learn more about Synthetics capabilities, check out the [docs](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries.html). @@ -107,7 +107,7 @@ const canary = new Canary(this, 'MyCanary', { code: synthetics.Code.fromInline('/* Synthetics handler code */'), handler: 'index.handler', // must be 'index.handler' }), - runtime: synthetics.Runtime.SYNTHETICS_NODEJS_2_0, + runtime: synthetics.Runtime.SYNTHETICS_NODEJS_2_2, }); // To supply the code from your local filesystem: @@ -116,7 +116,7 @@ const canary = new Canary(this, 'MyCanary', { code: synthetics.Code.fromAsset(path.join(__dirname, 'canary')), handler: 'index.handler', // must end with '.handler' }), - runtime: synthetics.Runtime.SYNTHETICS_NODEJS_2_0, + runtime: synthetics.Runtime.SYNTHETICS_NODEJS_2_2, }); // To supply the code from a S3 bucket: @@ -125,7 +125,7 @@ const canary = new Canary(this, 'MyCanary', { code: synthetics.Code.fromBucket(bucket, 'canary.zip'), handler: 'index.handler', // must end with '.handler' }), - runtime: synthetics.Runtime.SYNTHETICS_NODEJS_2_0, + runtime: synthetics.Runtime.SYNTHETICS_NODEJS_2_2, }); ``` diff --git a/packages/@aws-cdk/aws-synthetics/lib/canary.ts b/packages/@aws-cdk/aws-synthetics/lib/canary.ts index 7b1ccf79f5830..770a7be154f72 100644 --- a/packages/@aws-cdk/aws-synthetics/lib/canary.ts +++ b/packages/@aws-cdk/aws-synthetics/lib/canary.ts @@ -77,7 +77,7 @@ export class Runtime { * - Puppeteer-core version 1.14.0 * - The Chromium version that matches Puppeteer-core 1.14.0 * - * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_Library.html#CloudWatch_Synthetics_runtimeversion-1.0 + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-1.0 */ public static readonly SYNTHETICS_1_0 = new Runtime('syn-1.0'); @@ -85,12 +85,33 @@ export class Runtime { * `syn-nodejs-2.0` includes the following: * - Lambda runtime Node.js 10.x * - Puppeteer-core version 3.3.0 - * - Chromium version 81.0.4044.0 + * - Chromium version 83.0.4103.0 * - * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_Library.html#CloudWatch_Synthetics_runtimeversion-2.0 + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-2.0 */ public static readonly SYNTHETICS_NODEJS_2_0 = new Runtime('syn-nodejs-2.0'); + + /** + * `syn-nodejs-2.1` includes the following: + * - Lambda runtime Node.js 10.x + * - Puppeteer-core version 3.3.0 + * - Chromium version 83.0.4103.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-2.1 + */ + public static readonly SYNTHETICS_NODEJS_2_1 = new Runtime('syn-nodejs-2.1'); + + /** + * `syn-nodejs-2.2` includes the following: + * - Lambda runtime Node.js 10.x + * - Puppeteer-core version 3.3.0 + * - Chromium version 83.0.4103.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-2.2 + */ + public static readonly SYNTHETICS_NODEJS_2_2 = new Runtime('syn-nodejs-2.2'); + /** * @param name The name of the runtime version */ diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/assets/docker-image-asset.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/assets/docker-image-asset.ts index ebec6ab166fbb..654e1aa032926 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/assets/docker-image-asset.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/assets/docker-image-asset.ts @@ -23,12 +23,24 @@ export interface DockerImageSource { * The directory containing the Docker image build instructions. * * This path is relative to the asset manifest location. + * + * @default - Exactly one of `directory` and `executable` is required + */ + readonly directory?: string; + + /** + * A command-line executable that returns the name of a local + * Docker image on stdout after being run. + * + * @default - Exactly one of `directory` and `executable` is required */ - readonly directory: string; + readonly executable?: string[]; /** * The name of the file with build instructions * + * Only allowed when `directory` is set. + * * @default "Dockerfile" */ readonly dockerFile?: string; @@ -36,6 +48,8 @@ export interface DockerImageSource { /** * Target build stage in a Dockerfile with multiple build stages * + * Only allowed when `directory` is set. + * * @default - The last stage in the Dockerfile */ readonly dockerBuildTarget?: string; @@ -43,6 +57,8 @@ export interface DockerImageSource { /** * Additional build arguments * + * Only allowed when `directory` is set. + * * @default - No additional build arguments */ readonly dockerBuildArgs?: { [name: string]: string }; diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/assets/file-asset.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/assets/file-asset.ts index efa6cd4384bbe..58c7e0cc93ebc 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/assets/file-asset.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/assets/file-asset.ts @@ -34,16 +34,27 @@ export enum FileAssetPackaging { * Describe the source of a file asset */ export interface FileSource { + /** + * External command which will produce the file asset to upload. + * + * @default - Exactly one of `executable` and `path` is required. + */ + readonly executable?: string[]; + /** * The filesystem object to upload * * This path is relative to the asset manifest location. + * + * @default - Exactly one of `executable` and `path` is required. */ - readonly path: string; + readonly path?: string; /** * Packaging method * + * Only allowed when `path` is specified. + * * @default FILE */ readonly packaging?: FileAssetPackaging; @@ -62,4 +73,4 @@ export interface FileDestination extends AwsDestination { * The destination object key */ readonly objectKey: string; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/assets.schema.json b/packages/@aws-cdk/cloud-assembly-schema/schema/assets.schema.json index bbd61aae66813..995a895ad824d 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/assets.schema.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/assets.schema.json @@ -53,22 +53,26 @@ "description": "Describe the source of a file asset", "type": "object", "properties": { + "executable": { + "description": "External command which will produce the file asset to upload. (Default - Exactly one of `executable` and `path` is required.)", + "type": "array", + "items": { + "type": "string" + } + }, "path": { - "description": "The filesystem object to upload\n\nThis path is relative to the asset manifest location.", + "description": "The filesystem object to upload\n\nThis path is relative to the asset manifest location. (Default - Exactly one of `executable` and `path` is required.)", "type": "string" }, "packaging": { - "description": "Packaging method (Default FILE)", + "description": "Packaging method\n\nOnly allowed when `path` is specified. (Default FILE)", "enum": [ "file", "zip" ], "type": "string" } - }, - "required": [ - "path" - ] + } }, "FileDestination": { "description": "Where in S3 a file asset needs to be published", @@ -126,28 +130,32 @@ "type": "object", "properties": { "directory": { - "description": "The directory containing the Docker image build instructions.\n\nThis path is relative to the asset manifest location.", + "description": "The directory containing the Docker image build instructions.\n\nThis path is relative to the asset manifest location. (Default - Exactly one of `directory` and `executable` is required)", "type": "string" }, + "executable": { + "description": "A command-line executable that returns the name of a local\nDocker image on stdout after being run. (Default - Exactly one of `directory` and `executable` is required)", + "type": "array", + "items": { + "type": "string" + } + }, "dockerFile": { - "description": "The name of the file with build instructions (Default Dockerfile)", + "description": "The name of the file with build instructions\n\nOnly allowed when `directory` is set. (Default Dockerfile)", "type": "string" }, "dockerBuildTarget": { - "description": "Target build stage in a Dockerfile with multiple build stages (Default - The last stage in the Dockerfile)", + "description": "Target build stage in a Dockerfile with multiple build stages\n\nOnly allowed when `directory` is set. (Default - The last stage in the Dockerfile)", "type": "string" }, "dockerBuildArgs": { - "description": "Additional build arguments (Default - No additional build arguments)", + "description": "Additional build arguments\n\nOnly allowed when `directory` is set. (Default - No additional build arguments)", "type": "object", "additionalProperties": { "type": "string" } } - }, - "required": [ - "directory" - ] + } }, "DockerImageDestination": { "description": "Where to publish docker images", diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json index bdc5a9f306dec..e6bb766b23585 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json @@ -1 +1 @@ -{"version":"7.0.0"} \ No newline at end of file +{"version":"8.0.0"} diff --git a/packages/@aws-cdk/cloud-assembly-schema/test/assets.test.ts b/packages/@aws-cdk/cloud-assembly-schema/test/assets.test.ts index 62aebfa26e6ee..24ddd465484b7 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/test/assets.test.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/test/assets.test.ts @@ -21,6 +21,18 @@ describe('Docker image asset', () => { }, }, }, + externalAsset: { + source: { + executable: ['sometool'], + }, + destinations: { + dest: { + region: 'us-north-20', + repositoryName: 'REPO', + imageTag: 'TAG', + }, + }, + }, }, }); }).not.toThrow(); @@ -32,12 +44,18 @@ describe('Docker image asset', () => { version: Manifest.version(), dockerImages: { asset: { + source: { + directory: true, + }, + destinations: {}, + }, + externalAsset: { source: {}, destinations: {}, }, }, }); - }).toThrow(/instance\.dockerImages\.asset\.source requires property \"directory\"/); + }).toThrow(/instance\.dockerImages\.asset\.source\.directory is not of a type\(s\) string/); }); }); @@ -60,6 +78,18 @@ describe('File asset', () => { }, }, }, + externalAsset: { + source: { + executable: ['sometool'], + }, + destinations: { + dest: { + region: 'us-north-20', + bucketName: 'Bouquet', + objectKey: 'key', + }, + }, + }, }, }); }).not.toThrow(); @@ -109,6 +139,18 @@ describe('File asset', () => { }, }, }, + externalAsset: { + source: { + executable: ['sometool'], + }, + destinations: { + dest: { + region: 'us-north-20', + bucketName: 'Bouquet', + objectKey: 'key', + }, + }, + }, }, }); }).toThrow(/instance\.files\.asset\.source\.path is not of a type\(s\) string/); @@ -149,4 +191,4 @@ function validate(manifest: any) { fs.unlinkSync(filePath); fs.rmdirSync(dir); } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/cloudformation-diff/package.json b/packages/@aws-cdk/cloudformation-diff/package.json index c5244176f96bd..7179ae4210672 100644 --- a/packages/@aws-cdk/cloudformation-diff/package.json +++ b/packages/@aws-cdk/cloudformation-diff/package.json @@ -33,7 +33,7 @@ "@types/string-width": "^4.0.1", "@types/table": "^5.0.0", "cdk-build-tools": "0.0.0", - "fast-check": "^2.7.0", + "fast-check": "^2.11.0", "jest": "^26.6.3", "pkglint": "0.0.0", "ts-jest": "^26.4.4" diff --git a/packages/@aws-cdk/core/README.md b/packages/@aws-cdk/core/README.md index 3b013f3ff990e..8c5ce270d8dc1 100644 --- a/packages/@aws-cdk/core/README.md +++ b/packages/@aws-cdk/core/README.md @@ -843,3 +843,11 @@ IAM operator, we need it in the *key* of a `StringEquals` condition. JSON keys *must be* strings, so to circumvent this limitation, we use `CfnJson` to "delay" the rendition of this template section to deploy-time. This means that the value of `StringEquals` in the template will be `{ "Fn::GetAtt": [ "ConditionJson", "Value" ] }`, and will only "expand" to the operator we synthesized during deployment. + +### Stack Resource Limit + +When deploying to AWS CloudFormation, it needs to keep in check the amount of resources being added inside a Stack. Currently it's possible to check the limits in the [AWS CloudFormation quotas](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cloudformation-limits.html) page. + +It's possible to synthesize the project with more Resources than the allowed (or even reduce the number of Resources). + +Set the context key `@aws-cdk/core:stackResourceLimit` with the proper value, being 0 for disable the limit of resources. diff --git a/packages/@aws-cdk/core/lib/assets.ts b/packages/@aws-cdk/core/lib/assets.ts index 17d3b9d93e53f..d992546dbfdb3 100644 --- a/packages/@aws-cdk/core/lib/assets.ts +++ b/packages/@aws-cdk/core/lib/assets.ts @@ -106,17 +106,30 @@ export interface FileAssetSource { */ readonly sourceHash: string; + /** + * An external command that will produce the packaged asset. + * + * The command should produce the location of a ZIP file on `stdout`. + * + * @default - Exactly one of `directory` and `executable` is required + */ + readonly executable?: string[]; + /** * The path, relative to the root of the cloud assembly, in which this asset * source resides. This can be a path to a file or a directory, dependning on the * packaging type. + * + * @default - Exactly one of `directory` and `executable` is required */ - readonly fileName: string; + readonly fileName?: string; /** * Which type of packaging to perform. + * + * @default - Required if `fileName` is specified. */ - readonly packaging: FileAssetPackaging; + readonly packaging?: FileAssetPackaging; } export interface DockerImageAssetSource { @@ -130,11 +143,22 @@ export interface DockerImageAssetSource { */ readonly sourceHash: string; + /** + * An external command that will produce the packaged asset. + * + * The command should produce the name of a local Docker image on `stdout`. + * + * @default - Exactly one of `directoryName` and `executable` is required + */ + readonly executable?: string[]; + /** * The directory where the Dockerfile is stored, must be relative * to the cloud assembly root. + * + * @default - Exactly one of `directoryName` and `executable` is required */ - readonly directoryName: string; + readonly directoryName?: string; /** * Build args to pass to the `docker build` command. @@ -143,6 +167,8 @@ export interface DockerImageAssetSource { * values cannot refer to unresolved tokens (such as `lambda.functionArn` or * `queue.queueUrl`). * + * Only allowed when `directoryName` is specified. + * * @default - no build args are passed */ readonly dockerBuildArgs?: { [key: string]: string }; @@ -150,6 +176,8 @@ export interface DockerImageAssetSource { /** * Docker target to build to * + * Only allowed when `directoryName` is specified. + * * @default - no target */ readonly dockerBuildTarget?: string; @@ -157,6 +185,8 @@ export interface DockerImageAssetSource { /** * Path to the Dockerfile (relative to the directory). * + * Only allowed when `directoryName` is specified. + * * @default - no file */ readonly dockerFile?: string; diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts index 3f627f2057e6b..f90ae86dbf584 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts @@ -1,7 +1,7 @@ -import * as cxschema from '@aws-cdk/cloud-assembly-schema'; -import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs'; import * as path from 'path'; +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import * as cxapi from '@aws-cdk/cx-api'; import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, FileAssetPackaging, FileAssetSource } from '../assets'; import { Fn } from '../cfn-fn'; import { CfnParameter } from '../cfn-parameter'; @@ -9,8 +9,8 @@ import { CfnRule } from '../cfn-rule'; import { ISynthesisSession } from '../construct-compat'; import { Stack } from '../stack'; import { Token } from '../token'; -import { StackSynthesizer } from './stack-synthesizer'; import { assertBound, contentHash } from './_shared'; +import { StackSynthesizer } from './stack-synthesizer'; export const BOOTSTRAP_QUALIFIER_CONTEXT = '@aws-cdk/core:bootstrapQualifier'; @@ -289,12 +289,15 @@ export class DefaultStackSynthesizer extends StackSynthesizer { public addFileAsset(asset: FileAssetSource): FileAssetLocation { assertBound(this.stack); assertBound(this.bucketName); + validateFileAssetSource(asset); + const objectKey = this.bucketPrefix + asset.sourceHash + (asset.packaging === FileAssetPackaging.ZIP_DIRECTORY ? '.zip' : ''); // Add to manifest this.files[asset.sourceHash] = { source: { path: asset.fileName, + executable: asset.executable, packaging: asset.packaging, }, destinations: { @@ -325,12 +328,14 @@ export class DefaultStackSynthesizer extends StackSynthesizer { public addDockerImageAsset(asset: DockerImageAssetSource): DockerImageAssetLocation { assertBound(this.stack); assertBound(this.repositoryName); + validateDockerImageAssetSource(asset); const imageTag = asset.sourceHash; // Add to manifest this.dockerImages[asset.sourceHash] = { source: { + executable: asset.executable, directory: asset.directoryName, dockerBuildArgs: asset.dockerBuildArgs, dockerBuildTarget: asset.dockerBuildTarget, @@ -442,7 +447,7 @@ export class DefaultStackSynthesizer extends StackSynthesizer { // // Instead, we'll have a protocol with the CLI that we put an 's3://.../...' URL here, and the CLI // is going to resolve it to the correct 'https://.../' URL before it gives it to CloudFormation. - return `s3://${this.bucketName}/${sourceHash}`; + return `s3://${this.bucketName}/${this.bucketPrefix}${sourceHash}`; } /** @@ -565,4 +570,31 @@ function range(startIncl: number, endExcl: number) { ret.push(i); } return ret; +} + + +function validateFileAssetSource(asset: FileAssetSource) { + if (!!asset.executable === !!asset.fileName) { + throw new Error(`Exactly one of 'fileName' or 'executable' is required, got: ${JSON.stringify(asset)}`); + } + + if (!!asset.packaging !== !!asset.fileName) { + throw new Error(`'packaging' is expected in combination with 'fileName', got: ${JSON.stringify(asset)}`); + } +} + +function validateDockerImageAssetSource(asset: DockerImageAssetSource) { + if (!!asset.executable === !!asset.directoryName) { + throw new Error(`Exactly one of 'directoryName' or 'executable' is required, got: ${JSON.stringify(asset)}`); + } + + check('dockerBuildArgs'); + check('dockerBuildTarget'); + check('dockerFile'); + + function check(key: K) { + if (asset[key] && !asset.directoryName) { + throw new Error(`'${key}' is only allowed in combination with 'directoryName', got: ${JSON.stringify(asset)}`); + } + } } \ No newline at end of file diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/legacy.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/legacy.ts index e6dfd63235b8c..bf699b271878d 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/legacy.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/legacy.ts @@ -120,6 +120,10 @@ export class LegacyStackSynthesizer extends StackSynthesizer { // only add every image (identified by source hash) once for each stack that uses it. if (!this.addedImageAssets.has(assetId)) { + if (!asset.directoryName) { + throw new Error(`LegacyStackSynthesizer does not support this type of file asset: ${JSON.stringify(asset)}`); + } + const metadata: cxschema.ContainerImageAssetMetadataEntry = { repositoryName, imageTag, @@ -149,6 +153,10 @@ export class LegacyStackSynthesizer extends StackSynthesizer { if (!params) { params = new FileAssetParameters(this.assetParameters, asset.sourceHash); + if (!asset.fileName || !asset.packaging) { + throw new Error(`LegacyStackSynthesizer does not support this type of file asset: ${JSON.stringify(asset)}`); + } + const metadata: cxschema.FileAssetMetadataEntry = { path: asset.fileName, id: asset.sourceHash, diff --git a/packages/@aws-cdk/core/lib/stack.ts b/packages/@aws-cdk/core/lib/stack.ts index e1664e2996a90..c6a8a56916f2f 100644 --- a/packages/@aws-cdk/core/lib/stack.ts +++ b/packages/@aws-cdk/core/lib/stack.ts @@ -27,8 +27,12 @@ import { Construct as CoreConstruct } from './construct-compat'; const STACK_SYMBOL = Symbol.for('@aws-cdk/core.Stack'); const MY_STACK_CACHE = Symbol.for('@aws-cdk/core.Stack.myStack'); +export const STACK_RESOURCE_LIMIT_CONTEXT = '@aws-cdk/core:stackResourceLimit'; + const VALID_STACK_NAME_REGEX = /^[A-Za-z][A-Za-z0-9-]*$/; +const MAX_RESOURCES = 500; + export interface StackProps { /** * A description of the stack. @@ -753,6 +757,17 @@ export class Stack extends CoreConstruct implements ITaggable { // write the CloudFormation template as a JSON file const outPath = path.join(builder.outdir, this.templateFile); + + if (this.maxResources > 0) { + const resources = template.Resources || {}; + const numberOfResources = Object.keys(resources).length; + + if (numberOfResources > this.maxResources) { + throw new Error(`Number of resources: ${numberOfResources} is greater than allowed maximum of ${this.maxResources}`); + } else if (numberOfResources >= (this.maxResources * 0.8)) { + Annotations.of(this).addInfo(`Number of resources: ${numberOfResources} is approaching allowed maximum of ${this.maxResources}`); + } + } fs.writeFileSync(outPath, JSON.stringify(template, undefined, 2)); for (const ctx of this._missingContext) { @@ -907,6 +922,16 @@ export class Stack extends CoreConstruct implements ITaggable { }; } + /** + * Maximum number of resources in the stack + * + * Set to 0 to mean "unlimited". + */ + private get maxResources(): number { + const contextLimit = this.node.tryGetContext(STACK_RESOURCE_LIMIT_CONTEXT); + return contextLimit !== undefined ? parseInt(contextLimit, 10) : MAX_RESOURCES; + } + /** * Check whether this stack has a (transitive) dependency on another stack * diff --git a/packages/@aws-cdk/core/package.json b/packages/@aws-cdk/core/package.json index 6fab10e06197a..c380e97f5d6c8 100644 --- a/packages/@aws-cdk/core/package.json +++ b/packages/@aws-cdk/core/package.json @@ -176,13 +176,13 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/lodash": "^4.14.165", + "@types/lodash": "^4.14.167", "@types/minimatch": "^3.0.3", "@types/node": "^10.17.48", "@types/sinon": "^9.0.9", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", - "fast-check": "^2.7.0", + "fast-check": "^2.11.0", "lodash": "^4.17.20", "nodeunit-shim": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/core/test/stack-synthesis/new-style-synthesis.test.ts b/packages/@aws-cdk/core/test/stack-synthesis/new-style-synthesis.test.ts index 52d5e0ad20b16..d5e04c65018a0 100644 --- a/packages/@aws-cdk/core/test/stack-synthesis/new-style-synthesis.test.ts +++ b/packages/@aws-cdk/core/test/stack-synthesis/new-style-synthesis.test.ts @@ -243,6 +243,9 @@ nodeunitShim({ // WHEN const asm = myapp.synth(); + // THEN -- the S3 url is advertised on the stack artifact + const stackArtifact = asm.getStackArtifact('mystack-bucketPrefix'); + // THEN - we have an asset manifest with both assets and the stack template in there const manifest = readAssetManifest(asm); @@ -254,6 +257,10 @@ nodeunitShim({ assumeRoleExternalId: 'file-external-id', }); + const templateHash = last(stackArtifact.stackTemplateAssetObjectUrl?.split('/')); + + test.equals(stackArtifact.stackTemplateAssetObjectUrl, `s3://file-asset-bucket/000000000000/${templateHash}`); + test.done(); }, diff --git a/packages/@aws-cdk/core/test/stack.test.ts b/packages/@aws-cdk/core/test/stack.test.ts index 63c04be2e81de..581f4ed003895 100644 --- a/packages/@aws-cdk/core/test/stack.test.ts +++ b/packages/@aws-cdk/core/test/stack.test.ts @@ -45,6 +45,68 @@ nodeunitShim({ test.done(); }, + 'when stackResourceLimit is default, should give error'(test: Test) { + // GIVEN + const app = new App({}); + + const stack = new Stack(app, 'MyStack'); + + // WHEN + for (let index = 0; index < 1000; index++) { + new CfnResource(stack, `MyResource-${index}`, { type: 'MyResourceType' }); + } + + test.throws(() => { + app.synth(); + }, 'Number of resources: 1000 is greater than allowed maximum of 500'); + + test.done(); + }, + + 'when stackResourceLimit is defined, should give the proper error'(test: Test) { + // GIVEN + const app = new App({ + context: { + '@aws-cdk/core:stackResourceLimit': 100, + }, + }); + + const stack = new Stack(app, 'MyStack'); + + // WHEN + for (let index = 0; index < 200; index++) { + new CfnResource(stack, `MyResource-${index}`, { type: 'MyResourceType' }); + } + + test.throws(() => { + app.synth(); + }, 'Number of resources: 200 is greater than allowed maximum of 100'); + + test.done(); + }, + + 'when stackResourceLimit is 0, should not give error'(test: Test) { + // GIVEN + const app = new App({ + context: { + '@aws-cdk/core:stackResourceLimit': 0, + }, + }); + + const stack = new Stack(app, 'MyStack'); + + // WHEN + for (let index = 0; index < 1000; index++) { + new CfnResource(stack, `MyResource-${index}`, { type: 'MyResourceType' }); + } + + test.doesNotThrow(() => { + app.synth(); + }); + + test.done(); + }, + 'stack.templateOptions can be used to set template-level options'(test: Test) { const stack = new Stack(); diff --git a/packages/@aws-cdk/custom-resources/package.json b/packages/@aws-cdk/custom-resources/package.json index 56d59a94f2a1c..630de06bbd877 100644 --- a/packages/@aws-cdk/custom-resources/package.json +++ b/packages/@aws-cdk/custom-resources/package.json @@ -80,7 +80,7 @@ "@types/aws-lambda": "^8.10.64", "@types/fs-extra": "^8.1.1", "@types/sinon": "^9.0.9", - "aws-sdk": "^2.822.0", + "aws-sdk": "^2.824.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index 64b542ed4b2fd..d17a38e62f923 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -115,6 +115,9 @@ $ cdk synth $ # Synthesize cloud assembly for StackName, but don't include dependencies $ cdk synth MyStackName --exclusively + +$ # Synthesize cloud assembly for StackName, but don't cloudFormation template output to STDOUT +$ cdk synth MyStackName --quiet ``` See the [AWS Documentation](https://docs.aws.amazon.com/cdk/latest/guide/apps.html#apps_cloud_assembly) to learn more about cloud assemblies. diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index e305da55164e5..d14e53892354a 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -68,7 +68,8 @@ async function parseCommandLineArguments() { .option('long', { type: 'boolean', default: false, alias: 'l', desc: 'Display environment information for each stack' }), ) .command(['synthesize [STACKS..]', 'synth [STACKS..]'], 'Synthesizes and prints the CloudFormation template for this stack', yargs => yargs - .option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only synthesize requested stacks, don\'t include dependencies' })) + .option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only synthesize requested stacks, don\'t include dependencies' }) + .option('quiet', { type: 'boolean', alias: 'q', desc: 'Do not output CloudFormation Template to stdout', default: false })) .command('bootstrap [ENVIRONMENTS..]', 'Deploys the CDK toolkit stack into an AWS environment', yargs => yargs .option('bootstrap-bucket-name', { type: 'string', alias: ['b', 'toolkit-bucket-name'], desc: 'The name of the CDK toolkit bucket; bucket will be created and must not exist', default: undefined }) .option('bootstrap-kms-key-id', { type: 'string', desc: 'AWS KMS master key ID used for the SSE-KMS encryption', default: undefined, conflicts: 'bootstrap-customer-key' }) @@ -328,7 +329,7 @@ async function initCommandLine() { case 'synthesize': case 'synth': - return cli.synth(args.STACKS, args.exclusively); + return cli.synth(args.STACKS, args.exclusively, args.quiet); case 'metadata': return cli.metadata(args.STACK); diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index c0ae230879a80..c3e8c649eaa7b 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -295,12 +295,15 @@ export class CdkToolkit { * OUTPUT: If more than one stack ends up being selected, an output directory * should be supplied, where the templates will be written. */ - public async synth(stackNames: string[], exclusively: boolean): Promise { + public async synth(stackNames: string[], exclusively: boolean, quiet: boolean): Promise { const stacks = await this.selectStacksForDiff(stackNames, exclusively); // if we have a single stack, print it to STDOUT if (stacks.stackCount === 1) { - return stacks.firstStack.template; + if (!quiet) { + return stacks.firstStack.template; + } + return undefined; } // This is a slight hack; in integ mode we allow multiple stacks to be synthesized to stdout sequentially. diff --git a/packages/aws-cdk/lib/settings.ts b/packages/aws-cdk/lib/settings.ts index 0663d021f63d3..f61d2ebd270da 100644 --- a/packages/aws-cdk/lib/settings.ts +++ b/packages/aws-cdk/lib/settings.ts @@ -98,7 +98,8 @@ export class Configuration { this.context = new Context( this.commandLineContext, this.projectConfig.subSettings([CONTEXT_KEY]).makeReadOnly(), - this.projectContext); + this.projectContext, + userConfig.subSettings([CONTEXT_KEY]).makeReadOnly()); // Build settings from what's left this.settings = this.defaultConfig diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 6b851cf0656b8..4bb111e863ef6 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -72,8 +72,8 @@ "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/region-info": "0.0.0", "@aws-cdk/yaml-cfn": "0.0.0", - "archiver": "^5.1.0", - "aws-sdk": "^2.822.0", + "archiver": "^5.2.0", + "aws-sdk": "^2.824.0", "camelcase": "^6.2.0", "cdk-assets": "0.0.0", "colors": "^1.4.0", diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index caf3e6cc83257..9266d9bc10646 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -138,6 +138,16 @@ describe('deploy', () => { }); }); +describe('synth', () => { + test('with no stdout option', async () => { + // GIVE + const toolkit = defaultToolkitSetup(); + + // THEN + await expect(toolkit.synth(['Test-Stack-A'], false, true)).resolves.toBeUndefined(); + }); +}); + class MockStack { public static readonly MOCK_STACK_A: TestStackArtifact = { stackName: 'Test-Stack-A', diff --git a/packages/aws-cdk/test/usersettings.test.ts b/packages/aws-cdk/test/usersettings.test.ts new file mode 100644 index 0000000000000..948b3b3f907bc --- /dev/null +++ b/packages/aws-cdk/test/usersettings.test.ts @@ -0,0 +1,72 @@ +import * as os from 'os'; +import * as fs_path from 'path'; +import * as fs from 'fs-extra'; +import { mocked } from 'ts-jest/utils'; +import { Configuration, PROJECT_CONFIG, PROJECT_CONTEXT } from '../lib/settings'; + +// mock fs deeply +jest.mock('fs-extra'); +const mockedFs = mocked(fs, true); + +const USER_CONFIG = fs_path.join(os.homedir(), '.cdk.json'); + +test('load settings from both files if available', async () => { + // GIVEN + const GIVEN_CONFIG: Map = new Map([ + [PROJECT_CONFIG, { + project: 'foobar', + }], + [USER_CONFIG, { + project: 'foo', + test: 'bar', + }], + ]); + + // WHEN + mockedFs.pathExists.mockImplementation(path => { + return GIVEN_CONFIG.has(path); + }); + mockedFs.readJSON.mockImplementation(path => { + return GIVEN_CONFIG.get(path); + }); + + const config = await new Configuration().load(); + + // THEN + expect(config.settings.get(['project'])).toBe('foobar'); + expect(config.settings.get(['test'])).toBe('bar'); +}); + +test('load context from all 3 files if available', async () => { + // GIVEN + const GIVEN_CONFIG: Map = new Map([ + [PROJECT_CONFIG, { + context: { + project: 'foobar', + }, + }], + [PROJECT_CONTEXT, { + foo: 'bar', + }], + [USER_CONFIG, { + context: { + test: 'bar', + }, + }], + ]); + + // WHEN + mockedFs.pathExists.mockImplementation(path => { + return GIVEN_CONFIG.has(path); + }); + mockedFs.readJSON.mockImplementation(path => { + return GIVEN_CONFIG.get(path); + }); + + const config = await new Configuration().load(); + + // THEN + expect(config.context.get('project')).toBe('foobar'); + expect(config.context.get('foo')).toBe('bar'); + expect(config.context.get('test')).toBe('bar'); +}); \ No newline at end of file diff --git a/packages/cdk-assets/README.md b/packages/cdk-assets/README.md index 2eb10ae621947..c40afcd00c42d 100644 --- a/packages/cdk-assets/README.md +++ b/packages/cdk-assets/README.md @@ -28,6 +28,7 @@ Currently the following asset types are supported: * Files and archives, uploaded to S3 * Docker Images, uploaded to ECR +* Files, archives, and Docker images built by external utilities S3 buckets and ECR repositories to upload to are expected to exist already. @@ -41,6 +42,13 @@ itself in the following behaviors: image in the local Docker cache) already exists named after the asset's ID, it will not be packaged, but will be uploaded directly to the destination location. + +For assets build by external utilities, the contract is such that cdk-assets +expects the utility to manage dedupe detection as well as path/image tag generation. +This means that cdk-assets will call the external utility every time generation +is warranted, and it is up to the utility to a) determine whether to do a +full rebuild; and b) to return only one thing on stdout: the path to the file/archive +asset, or the name of the local Docker image. ## Usage @@ -82,6 +90,19 @@ An asset manifest looks like this: } } }, + "3dfe2b80b050e7e4e168f84feff678d4": { + "source": { + "executable": ["myzip"] + }, + "destinations": { + "us-east-1": { + "region": "us-east-1", + "assumeRoleArn": "arn:aws:iam::12345789012:role/my-account", + "bucketName": "MySpecialBucket", + "objectKey": "3dfe2b80b050e7e4e168f84feff678d4.zip" + } + } + }, }, "dockerImages": { "b48783c58a86f7b8c68a4591c4f9be31": { @@ -97,6 +118,20 @@ An asset manifest looks like this: "imageUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/MyRepository:1234567891b48783c58a86f7b8c68a4591c4f9be31", } } + }, + "d92753c58a86f7b8c68a4591c4f9cf28": { + "source": { + "executable": ["mytool", "package", "dockerdir"], + }, + "destinations": { + "us-east-1": { + "region": "us-east-1", + "assumeRoleArn": "arn:aws:iam::12345789012:role/my-account", + "repositoryName": "MyRepository2", + "imageTag": "d92753c58a86f7b8c68a4591c4f9cf28", + "imageUri": "123456789987.dkr.ecr.us-east-1.amazonaws.com/MyRepository2:1234567891b48783c58a86f7b8c68a4591c4f9be31", + } + } } } } diff --git a/packages/cdk-assets/lib/private/handlers/container-images.ts b/packages/cdk-assets/lib/private/handlers/container-images.ts index bd755a52f139b..a3b6756ecb18d 100644 --- a/packages/cdk-assets/lib/private/handlers/container-images.ts +++ b/packages/cdk-assets/lib/private/handlers/container-images.ts @@ -1,73 +1,128 @@ import * as path from 'path'; +import { DockerImageDestination } from '@aws-cdk/cloud-assembly-schema'; import { DockerImageManifestEntry } from '../../asset-manifest'; import { EventType } from '../../progress'; import { IAssetHandler, IHandlerHost } from '../asset-handler'; import { Docker } from '../docker'; import { replaceAwsPlaceholders } from '../placeholders'; +import { shell } from '../shell'; export class ContainerImageAssetHandler implements IAssetHandler { - private readonly localTagName: string; private readonly docker = new Docker(m => this.host.emitMessage(EventType.DEBUG, m)); constructor( private readonly workDir: string, private readonly asset: DockerImageManifestEntry, private readonly host: IHandlerHost) { - - this.localTagName = `cdkasset-${this.asset.id.assetId.toLowerCase()}`; } public async publish(): Promise { const destination = await replaceAwsPlaceholders(this.asset.destination, this.host.aws); - const ecr = await this.host.aws.ecrClient(destination); - const account = (await this.host.aws.discoverCurrentAccount()).accountId; - const repoUri = await repositoryUri(ecr, destination.repositoryName); + if (!repoUri) { throw new Error(`No ECR repository named '${destination.repositoryName}' in account ${account}. Is this account bootstrapped?`); } const imageUri = `${repoUri}:${destination.imageTag}`; - this.host.emitMessage(EventType.CHECK, `Check ${imageUri}`); - if (await imageExists(ecr, destination.repositoryName, destination.imageTag)) { - this.host.emitMessage(EventType.FOUND, `Found ${imageUri}`); - return; - } - + if (await this.destinationAlreadyExists(ecr, destination, imageUri)) { return; } if (this.host.aborted) { return; } // Login before build so that the Dockerfile can reference images in the ECR repo await this.docker.login(ecr); - await this.buildImage(); + + const localTagName = this.asset.source.executable + ? await this.buildExternalAsset(this.asset.source.executable) + : await this.buildDirectoryAsset(); + + if (localTagName === undefined || this.host.aborted) { + return; + } this.host.emitMessage(EventType.UPLOAD, `Push ${imageUri}`); if (this.host.aborted) { return; } - await this.docker.tag(this.localTagName, imageUri); + await this.docker.tag(localTagName, imageUri); await this.docker.push(imageUri); } - private async buildImage(): Promise { - if (await this.docker.exists(this.localTagName)) { - this.host.emitMessage(EventType.CACHED, `Cached ${this.localTagName}`); - return; + /** + * Build a (local) Docker asset from a directory with a Dockerfile + * + * Tags under a deterministic, unique, local identifier wich will skip + * the build if it already exists. + */ + private async buildDirectoryAsset(): Promise { + const localTagName = `cdkasset-${this.asset.id.assetId.toLowerCase()}`; + + if (!(await this.isImageCached(localTagName))) { + if (this.host.aborted) { return undefined; } + + await this.buildImage(localTagName); + } + + return localTagName; + } + + /** + * Build a (local) Docker asset by running an external command + * + * External command is responsible for deduplicating the build if possible, + * and is expected to return the generated image identifier on stdout. + */ + private async buildExternalAsset(executable: string[]): Promise { + this.host.emitMessage(EventType.BUILD, `Building Docker image using command '${executable}'`); + if (this.host.aborted) { return undefined; } + + return (await shell(executable, { quiet: true })).trim(); + } + + + /** + * Check whether the image already exists in the ECR repo + * + * Use the fields from the destination to do the actual check. The imageUri + * should correspond to that, but is only used to print Docker image location + * for user benefit (the format is slightly different). + */ + private async destinationAlreadyExists(ecr: AWS.ECR, destination: DockerImageDestination, imageUri: string): Promise { + this.host.emitMessage(EventType.CHECK, `Check ${imageUri}`); + if (await imageExists(ecr, destination.repositoryName, destination.imageTag)) { + this.host.emitMessage(EventType.FOUND, `Found ${imageUri}`); + return true; } + return false; + } + + private async buildImage(localTagName: string): Promise { const source = this.asset.source; + if (!source.directory) { + throw new Error(`'directory' is expected in the DockerImage asset source, got: ${JSON.stringify(source)}`); + } const fullPath = path.resolve(this.workDir, source.directory); this.host.emitMessage(EventType.BUILD, `Building Docker image at ${fullPath}`); await this.docker.build({ directory: fullPath, - tag: this.localTagName, + tag: localTagName, buildArgs: source.dockerBuildArgs, target: source.dockerBuildTarget, file: source.dockerFile, }); } + + private async isImageCached(localTagName: string): Promise { + if (await this.docker.exists(localTagName)) { + this.host.emitMessage(EventType.CACHED, `Cached ${localTagName}`); + return true; + } + + return false; + } } async function imageExists(ecr: AWS.ECR, repositoryName: string, imageTag: string) { @@ -93,4 +148,4 @@ async function repositoryUri(ecr: AWS.ECR, repositoryName: string): Promise { - const source = this.asset.source; - const fullPath = path.resolve(this.workDir, this.asset.source.path); + private async packageFile(source: FileSource): Promise { + if (!source.path) { + throw new Error(`'path' is expected in the File asset source, got: ${JSON.stringify(source)}`); + } + + const fullPath = path.resolve(this.workDir, source.path); if (source.packaging === FileAssetPackaging.ZIP_DIRECTORY) { + const contentType = 'application/zip'; + await fs.mkdir(this.fileCacheRoot, { recursive: true }); - const ret = path.join(this.fileCacheRoot, `${this.asset.id.assetId}.zip`); + const packagedPath = path.join(this.fileCacheRoot, `${this.asset.id.assetId}.zip`); - if (await pathExists(ret)) { - this.host.emitMessage(EventType.CACHED, `From cache ${ret}`); - return ret; + if (await pathExists(packagedPath)) { + this.host.emitMessage(EventType.CACHED, `From cache ${path}`); + return { packagedPath, contentType }; } - this.host.emitMessage(EventType.BUILD, `Zip ${fullPath} -> ${ret}`); - await zipDirectory(fullPath, ret); - return ret; + this.host.emitMessage(EventType.BUILD, `Zip ${fullPath} -> ${path}`); + await zipDirectory(fullPath, packagedPath); + return { packagedPath, contentType }; } else { - return fullPath; + return { packagedPath: fullPath }; } } + + private async externalPackageFile(executable: string[]): Promise { + this.host.emitMessage(EventType.BUILD, `Building asset source using command: '${executable}'`); + + return { + packagedPath: (await shell(executable, { quiet: true })).trim(), + contentType: 'application/zip', + }; + } } enum BucketOwnership { @@ -109,3 +124,21 @@ async function objectExists(s3: AWS.S3, bucket: string, key: string) { const response = await s3.listObjectsV2({ Bucket: bucket, Prefix: key, MaxKeys: 1 }).promise(); return response.Contents != null && response.Contents.some(object => object.Key === key); } + + +/** + * A packaged asset which can be uploaded (either a single file or directory) + */ +interface PackagedFileAsset { + /** + * Path of the file or directory + */ + readonly packagedPath: string; + + /** + * Content type to be added in the S3 upload action + * + * @default - No content type + */ + readonly contentType?: string; +} diff --git a/packages/cdk-assets/lib/private/handlers/index.ts b/packages/cdk-assets/lib/private/handlers/index.ts index 2e4d406ce5b0b..97ec7354279df 100644 --- a/packages/cdk-assets/lib/private/handlers/index.ts +++ b/packages/cdk-assets/lib/private/handlers/index.ts @@ -12,4 +12,4 @@ export function makeAssetHandler(manifest: AssetManifest, asset: IManifestEntry, } throw new Error(`Unrecognized asset type: '${asset}'`); -} \ No newline at end of file +} diff --git a/packages/cdk-assets/package.json b/packages/cdk-assets/package.json index cde03cd4142c0..f5f6f12a4bf09 100644 --- a/packages/cdk-assets/package.json +++ b/packages/cdk-assets/package.json @@ -46,8 +46,8 @@ "dependencies": { "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/cx-api": "0.0.0", - "archiver": "^5.1.0", - "aws-sdk": "^2.822.0", + "archiver": "^5.2.0", + "aws-sdk": "^2.824.0", "glob": "^7.1.6", "yargs": "^16.2.0" }, diff --git a/packages/cdk-assets/test/docker-images.test.ts b/packages/cdk-assets/test/docker-images.test.ts index 3f0aeaabf474c..3b608a1e63ffe 100644 --- a/packages/cdk-assets/test/docker-images.test.ts +++ b/packages/cdk-assets/test/docker-images.test.ts @@ -9,6 +9,8 @@ import { mockSpawn } from './mock-child_process'; let aws: ReturnType; const absoluteDockerPath = '/simple/cdk.out/dockerdir'; beforeEach(() => { + jest.resetAllMocks(); + mockfs({ '/simple/cdk.out/assets.json': JSON.stringify({ version: Manifest.version(), @@ -28,6 +30,24 @@ beforeEach(() => { }, }, }), + '/external/cdk.out/assets.json': JSON.stringify({ + version: Manifest.version(), + dockerImages: { + theExternalAsset: { + source: { + executable: ['sometool'], + }, + destinations: { + theDestination: { + region: 'us-north-50', + assumeRoleArn: 'arn:aws:role', + repositoryName: 'repo', + imageTag: 'ghijkl', + }, + }, + }, + }, + }), '/simple/cdk.out/dockerdir/Dockerfile': 'FROM scratch', '/abs/cdk.out/assets.json': JSON.stringify({ version: Manifest.version(), @@ -92,7 +112,7 @@ describe('with a complete manifest', () => { ], }); - mockSpawn( + const expectAllSpawns = mockSpawn( { commandLine: ['docker', 'login', '--username', 'user', '--password-stdin', 'https://proxy.com/'] }, { commandLine: ['docker', 'inspect', 'cdkasset-theasset'] }, { commandLine: ['docker', 'tag', 'cdkasset-theasset', '12345.amazonaws.com/repo:abcdef'] }, @@ -100,6 +120,9 @@ describe('with a complete manifest', () => { ); await pub.publish(); + + expectAllSpawns(); + expect(true).toBeTruthy(); // Expect no exception, satisfy linter }); test('build and upload docker image if not exists anywhere', async () => { @@ -110,7 +133,7 @@ describe('with a complete manifest', () => { ], }); - mockSpawn( + const expectAllSpawns = mockSpawn( { commandLine: ['docker', 'login', '--username', 'user', '--password-stdin', 'https://proxy.com/'] }, { commandLine: ['docker', 'inspect', 'cdkasset-theasset'], exitCode: 1 }, { commandLine: ['docker', 'build', '--tag', 'cdkasset-theasset', '.'], cwd: absoluteDockerPath }, @@ -119,6 +142,41 @@ describe('with a complete manifest', () => { ); await pub.publish(); + + expectAllSpawns(); + expect(true).toBeTruthy(); // Expect no exception, satisfy linter + }); +}); + +describe('external assets', () => { + let pub: AssetPublishing; + const externalTag = 'external:tag'; + beforeEach(() => { + pub = new AssetPublishing(AssetManifest.fromPath('/external/cdk.out'), { aws }); + }); + + test('upload externally generated Docker image', async () => { + aws.mockEcr.describeImages = mockedApiFailure('ImageNotFoundException', 'File does not exist'); + aws.mockEcr.getAuthorizationToken = mockedApiResult({ + authorizationData: [ + { authorizationToken: 'dXNlcjpwYXNz', proxyEndpoint: 'https://proxy.com/' }, + ], + }); + + const expectAllSpawns = mockSpawn( + { commandLine: ['docker', 'login', '--username', 'user', '--password-stdin', 'https://proxy.com/'] }, + { commandLine: ['sometool'], stdout: externalTag }, + { commandLine: ['docker', 'tag', externalTag, '12345.amazonaws.com/repo:ghijkl'] }, + { commandLine: ['docker', 'push', '12345.amazonaws.com/repo:ghijkl'] }, + ); + + await pub.publish(); + + expect(aws.ecrClient).toHaveBeenCalledWith(expect.objectContaining({ + region: 'us-north-50', + assumeRoleArn: 'arn:aws:role', + })); + expectAllSpawns(); }); }); @@ -132,7 +190,7 @@ test('correctly identify Docker directory if path is absolute', async () => { ], }); - mockSpawn( + const expectAllSpawns = mockSpawn( // Only care about the 'build' command line { commandLine: ['docker', 'login'], prefix: true }, { commandLine: ['docker', 'inspect'], exitCode: 1, prefix: true }, @@ -142,4 +200,7 @@ test('correctly identify Docker directory if path is absolute', async () => { ); await pub.publish(); + + expect(true).toBeTruthy(); // Expect no exception, satisfy linter + expectAllSpawns(); }); diff --git a/packages/cdk-assets/test/files.test.ts b/packages/cdk-assets/test/files.test.ts index e8c7247ef7f42..42cb8a71c05ad 100644 --- a/packages/cdk-assets/test/files.test.ts +++ b/packages/cdk-assets/test/files.test.ts @@ -1,10 +1,17 @@ +jest.mock('child_process'); + import { Manifest } from '@aws-cdk/cloud-assembly-schema'; import * as mockfs from 'mock-fs'; import { AssetManifest, AssetPublishing } from '../lib'; import { mockAws, mockedApiResult, mockUpload } from './mock-aws'; +import { mockSpawn } from './mock-child_process'; + +const ABS_PATH = '/simple/cdk.out/some_external_file'; let aws: ReturnType; beforeEach(() => { + jest.resetAllMocks(); + mockfs({ '/simple/cdk.out/assets.json': JSON.stringify({ version: Manifest.version(), @@ -25,6 +32,7 @@ beforeEach(() => { }, }), '/simple/cdk.out/some_file': 'FILE_CONTENTS', + [ABS_PATH]: 'FILE_CONTENTS', '/abs/cdk.out/assets.json': JSON.stringify({ version: Manifest.version(), files: { @@ -36,7 +44,25 @@ beforeEach(() => { theDestination: { region: 'us-north-50', assumeRoleArn: 'arn:aws:role', - bucketName: 'some_bucket', + bucketName: 'some_other_bucket', + objectKey: 'some_key', + }, + }, + }, + }, + }), + '/external/cdk.out/assets.json': JSON.stringify({ + version: Manifest.version(), + files: { + externalAsset: { + source: { + executable: ['sometool'], + }, + destinations: { + theDestination: { + region: 'us-north-50', + assumeRoleArn: 'arn:aws:role', + bucketName: 'some_external_bucket', objectKey: 'some_key', }, }, @@ -127,4 +153,40 @@ test('correctly identify asset path if path is absolute', async () => { aws.mockS3.upload = mockUpload('FILE_CONTENTS'); await pub.publish(); + + expect(true).toBeTruthy(); // No exception, satisfy linter +}); + +describe('external assets', () => { + let pub: AssetPublishing; + beforeEach(() => { + pub = new AssetPublishing(AssetManifest.fromPath('/external/cdk.out'), { aws }); + }); + + test('do nothing if file exists already', async () => { + aws.mockS3.listObjectsV2 = mockedApiResult({ Contents: [{ Key: 'some_key' }] }); + + await pub.publish(); + + expect(aws.mockS3.listObjectsV2).toHaveBeenCalledWith(expect.objectContaining({ + Bucket: 'some_external_bucket', + Prefix: 'some_key', + MaxKeys: 1, + })); + }); + + test('upload external asset correctly', async () => { + aws.mockS3.listObjectsV2 = mockedApiResult({ Contents: undefined }); + aws.mockS3.upload = mockUpload('FILE_CONTENTS'); + const expectAllSpawns = mockSpawn({ commandLine: ['sometool'], stdout: ABS_PATH }); + + await pub.publish(); + + expect(aws.s3Client).toHaveBeenCalledWith(expect.objectContaining({ + region: 'us-north-50', + assumeRoleArn: 'arn:aws:role', + })); + + expectAllSpawns(); + }); }); diff --git a/packages/cdk-assets/test/mock-child_process.ts b/packages/cdk-assets/test/mock-child_process.ts index da0fd27d08fe6..2cb513e24fff7 100644 --- a/packages/cdk-assets/test/mock-child_process.ts +++ b/packages/cdk-assets/test/mock-child_process.ts @@ -17,7 +17,7 @@ export interface Invocation { prefix?: boolean; } -export function mockSpawn(...invocations: Invocation[]) { +export function mockSpawn(...invocations: Invocation[]): () => void { let mock = (child_process.spawn as any); for (const _invocation of invocations) { const invocation = _invocation; // Mirror into variable for closure @@ -42,7 +42,7 @@ export function mockSpawn(...invocations: Invocation[]) { child.stderr = new events.EventEmitter(); if (invocation.stdout) { - mockEmit(child.stdout, 'data', invocation.stdout); + mockEmit(child.stdout, 'data', Buffer.from(invocation.stdout)); } mockEmit(child, 'close', invocation.exitCode ?? 0); @@ -53,6 +53,10 @@ export function mockSpawn(...invocations: Invocation[]) { mock.mockImplementation((binary: string, args: string[], _options: any) => { throw new Error(`Did not expect call of ${JSON.stringify([binary, ...args])}`); }); + + return () => { + expect(mock).toHaveBeenCalledTimes(invocations.length); + }; } /** diff --git a/tools/cdk-build-tools/config/eslintrc.js b/tools/cdk-build-tools/config/eslintrc.js index a047a30a99800..63608e69161a3 100644 --- a/tools/cdk-build-tools/config/eslintrc.js +++ b/tools/cdk-build-tools/config/eslintrc.js @@ -197,6 +197,7 @@ module.exports = { }], // Overrides for plugin:jest/recommended + "jest/expect-expect": "off", "jest/no-conditional-expect": "off", "jest/no-done-callback": "off", // Far too many of these in the codebase. "jest/no-standalone-expect": "off", // nodeunitShim confuses this check. diff --git a/tools/cdk-build-tools/package.json b/tools/cdk-build-tools/package.json index 965e0e91780a7..226ba63a75b81 100644 --- a/tools/cdk-build-tools/package.json +++ b/tools/cdk-build-tools/package.json @@ -39,7 +39,7 @@ "pkglint": "0.0.0" }, "dependencies": { - "@typescript-eslint/eslint-plugin": "^4.11.1", + "@typescript-eslint/eslint-plugin": "^4.12.0", "@typescript-eslint/parser": "^4.7.0", "awslint": "0.0.0", "colors": "^1.4.0", diff --git a/version.v1.json b/version.v1.json index 5d4e6a68aecb5..5ccb440326d66 100644 --- a/version.v1.json +++ b/version.v1.json @@ -1,3 +1,3 @@ { - "version": "1.83.0" + "version": "1.84.0" } diff --git a/yarn.lock b/yarn.lock index 059f2c3306ca0..29e50d06679d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1616,10 +1616,10 @@ dependencies: jszip "*" -"@types/lodash@^4.14.165": - version "4.14.165" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.165.tgz#74d55d947452e2de0742bad65270433b63a8c30f" - integrity sha512-tjSSOTHhI5mCHTy/OOXYIhi2Wt1qcbHmuXD1Ha7q70CgI/I71afO4XtLb/cVexki1oVYchpul/TOuu3Arcdxrg== +"@types/lodash@^4.14.167": + version "4.14.167" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.167.tgz#ce7d78553e3c886d4ea643c37ec7edc20f16765e" + integrity sha512-w7tQPjARrvdeBkX/Rwg95S592JwxqOjmms3zWQ0XZgSyxSLdzWaYH3vErBhdVS/lRBX7F8aBYcYJYTr5TMGOzw== "@types/md5@^2.2.1": version "2.2.1" @@ -1774,28 +1774,28 @@ resolved "https://registry.yarnpkg.com/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.4.tgz#445251eb00bd9c1e751f82c7c6bf4f714edfd464" integrity sha512-/emrKCfQMQmFCqRqqBJ0JueHBT06jBRM3e8OgnvDUcvuExONujIk2hFA5dNsN9Nt41ljGVDdChvCydATZ+KOZw== -"@typescript-eslint/eslint-plugin@^4.11.1": - version "4.11.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.11.1.tgz#7579c6d17ad862154c10bc14b40e5427b729e209" - integrity sha512-fABclAX2QIEDmTMk6Yd7Muv1CzFLwWM4505nETzRHpP3br6jfahD9UUJkhnJ/g2m7lwfz8IlswcwGGPGiq9exw== +"@typescript-eslint/eslint-plugin@^4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.12.0.tgz#00d1b23b40b58031e6d7c04a5bc6c1a30a2e834a" + integrity sha512-wHKj6q8s70sO5i39H2g1gtpCXCvjVszzj6FFygneNFyIAxRvNSVz9GML7XpqrB9t7hNutXw+MHnLN/Ih6uyB8Q== dependencies: - "@typescript-eslint/experimental-utils" "4.11.1" - "@typescript-eslint/scope-manager" "4.11.1" + "@typescript-eslint/experimental-utils" "4.12.0" + "@typescript-eslint/scope-manager" "4.12.0" debug "^4.1.1" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@4.11.1", "@typescript-eslint/experimental-utils@^4.0.1": - version "4.11.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.11.1.tgz#2dad3535b878c25c7424e40bfa79d899f3f485bc" - integrity sha512-mAlWowT4A6h0TC9F+J5pdbEhjNiEMO+kqPKQ4sc3fVieKL71dEqfkKgtcFVSX3cjSBwYwhImaQ/mXQF0oaI38g== +"@typescript-eslint/experimental-utils@4.12.0", "@typescript-eslint/experimental-utils@^4.0.1": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.12.0.tgz#372838e76db76c9a56959217b768a19f7129546b" + integrity sha512-MpXZXUAvHt99c9ScXijx7i061o5HEjXltO+sbYfZAAHxv3XankQkPaNi5myy0Yh0Tyea3Hdq1pi7Vsh0GJb0fA== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/scope-manager" "4.11.1" - "@typescript-eslint/types" "4.11.1" - "@typescript-eslint/typescript-estree" "4.11.1" + "@typescript-eslint/scope-manager" "4.12.0" + "@typescript-eslint/types" "4.12.0" + "@typescript-eslint/typescript-estree" "4.12.0" eslint-scope "^5.0.0" eslint-utils "^2.0.0" @@ -1809,13 +1809,13 @@ "@typescript-eslint/typescript-estree" "4.7.0" debug "^4.1.1" -"@typescript-eslint/scope-manager@4.11.1": - version "4.11.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.11.1.tgz#72dc2b60b0029ab0888479b12bf83034920b4b69" - integrity sha512-Al2P394dx+kXCl61fhrrZ1FTI7qsRDIUiVSuN6rTwss6lUn8uVO2+nnF4AvO0ug8vMsy3ShkbxLu/uWZdTtJMQ== +"@typescript-eslint/scope-manager@4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.12.0.tgz#beeb8beca895a07b10c593185a5612f1085ef279" + integrity sha512-QVf9oCSVLte/8jvOsxmgBdOaoe2J0wtEmBr13Yz0rkBNkl5D8bfnf6G4Vhox9qqMIoG7QQoVwd2eG9DM/ge4Qg== dependencies: - "@typescript-eslint/types" "4.11.1" - "@typescript-eslint/visitor-keys" "4.11.1" + "@typescript-eslint/types" "4.12.0" + "@typescript-eslint/visitor-keys" "4.12.0" "@typescript-eslint/scope-manager@4.7.0": version "4.7.0" @@ -1825,23 +1825,23 @@ "@typescript-eslint/types" "4.7.0" "@typescript-eslint/visitor-keys" "4.7.0" -"@typescript-eslint/types@4.11.1": - version "4.11.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.11.1.tgz#3ba30c965963ef9f8ced5a29938dd0c465bd3e05" - integrity sha512-5kvd38wZpqGY4yP/6W3qhYX6Hz0NwUbijVsX2rxczpY6OXaMxh0+5E5uLJKVFwaBM7PJe1wnMym85NfKYIh6CA== +"@typescript-eslint/types@4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.12.0.tgz#fb891fe7ccc9ea8b2bbd2780e36da45d0dc055e5" + integrity sha512-N2RhGeheVLGtyy+CxRmxdsniB7sMSCfsnbh8K/+RUIXYYq3Ub5+sukRCjVE80QerrUBvuEvs4fDhz5AW/pcL6g== "@typescript-eslint/types@4.7.0": version "4.7.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.7.0.tgz#5e95ef5c740f43d942542b35811f87b62fccca69" integrity sha512-uLszFe0wExJc+I7q0Z/+BnP7wao/kzX0hB5vJn4LIgrfrMLgnB2UXoReV19lkJQS1a1mHWGGODSxnBx6JQC3Sg== -"@typescript-eslint/typescript-estree@4.11.1": - version "4.11.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.11.1.tgz#a4416b4a65872a48773b9e47afabdf7519eb10bc" - integrity sha512-tC7MKZIMRTYxQhrVAFoJq/DlRwv1bnqA4/S2r3+HuHibqvbrPcyf858lNzU7bFmy4mLeIHFYr34ar/1KumwyRw== +"@typescript-eslint/typescript-estree@4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.12.0.tgz#3963418c850f564bdab3882ae23795d115d6d32e" + integrity sha512-gZkFcmmp/CnzqD2RKMich2/FjBTsYopjiwJCroxqHZIY11IIoN0l5lKqcgoAPKHt33H2mAkSfvzj8i44Jm7F4w== dependencies: - "@typescript-eslint/types" "4.11.1" - "@typescript-eslint/visitor-keys" "4.11.1" + "@typescript-eslint/types" "4.12.0" + "@typescript-eslint/visitor-keys" "4.12.0" debug "^4.1.1" globby "^11.0.1" is-glob "^4.0.1" @@ -1863,12 +1863,12 @@ semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/visitor-keys@4.11.1": - version "4.11.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.11.1.tgz#4c050a4c1f7239786e2dd4e69691436143024e05" - integrity sha512-IrlBhD9bm4bdYcS8xpWarazkKXlE7iYb1HzRuyBP114mIaj5DJPo11Us1HgH60dTt41TCZXMaTCAW+OILIYPOg== +"@typescript-eslint/visitor-keys@4.12.0": + version "4.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.12.0.tgz#a470a79be6958075fa91c725371a83baf428a67a" + integrity sha512-hVpsLARbDh4B9TKYz5cLbcdMIOAoBYgFPCSP9FFS/liSF+b33gVNq8JHY3QGhHNVz85hObvL7BEYLlgx553WCw== dependencies: - "@typescript-eslint/types" "4.11.1" + "@typescript-eslint/types" "4.12.0" eslint-visitor-keys "^2.0.0" "@typescript-eslint/visitor-keys@4.7.0": @@ -2107,10 +2107,10 @@ archiver-utils@^2.1.0: normalize-path "^3.0.0" readable-stream "^2.0.0" -archiver@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.1.0.tgz#05b0f6f7836f3e6356a0532763d2bb91017a7e37" - integrity sha512-iKuQUP1nuKzBC2PFlGet5twENzCfyODmvkxwDV0cEFXavwcLrIW5ssTuHi9dyTPvpWr6Faweo2eQaQiLIwyXTA== +archiver@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.2.0.tgz#25aa1b3d9febf7aec5b0f296e77e69960c26db94" + integrity sha512-QEAKlgQuAtUxKeZB9w5/ggKXh21bZS+dzzuQ0RPBC20qtDCbTyzqmisoeJP46MP39fg4B4IcyvR+yeyEBdblsQ== dependencies: archiver-utils "^2.1.0" async "^3.2.0" @@ -2309,10 +2309,10 @@ aws-sdk-mock@^5.1.0: sinon "^9.0.1" traverse "^0.6.6" -aws-sdk@^2.637.0, aws-sdk@^2.822.0: - version "2.822.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.822.0.tgz#63a13b0124efe2a9a9d2c87eaa39ca1ab63d3309" - integrity sha512-1a2L4e9ICIZLsY9mh0EFuAQYiDRFiLHLjicIa6Q4aDmN31cLqdQKCnYoWwm9yi0RHI4fnYQiQqSBF/f1Apfe9A== +aws-sdk@^2.637.0, aws-sdk@^2.824.0: + version "2.824.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.824.0.tgz#a67747d4d0b53d09c6c121e93f44d8f6e76fc44b" + integrity sha512-9KNRQBkIMPn+6DWb4gR+RzqTMNyGLEwOgXbE4dDehOIAflfLnv3IFwLnzrhxJnleB4guYrILIsBroJFBzjiekg== dependencies: buffer "4.9.2" events "1.1.1" @@ -3938,10 +3938,10 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" -esbuild@^0.8.28: - version "0.8.28" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.8.28.tgz#54f1b2dda1c1a8613308a41d31f9a5ad9fbbcbed" - integrity sha512-8u4PliPrsELMwl0X4ChPJvlNfoSh5OSpLHcAFFiipi2m/k+9i4cEjQB8rztLiiqO7kXnL+IaNU36StgnpHhOwA== +esbuild@^0.8.31: + version "0.8.31" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.8.31.tgz#c21e7adb3ad283c951a53de7ad64a5ae2df2ed34" + integrity sha512-7EIU0VdUxltwivjVezX3HgeNzeIVR1snkrAo57WdUnuBMykdzin5rTrxwCDM6xQqj0RL/HjOEm3wFr2ijHKeaA== escalade@^3.1.1: version "3.1.1" @@ -4285,12 +4285,12 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= -fast-check@^2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/fast-check/-/fast-check-2.7.0.tgz#d935243a43bc5e8ac4724ee2cb6c109533e8fd85" - integrity sha512-+frnWpxp43Egnx2wuqRVrbHj1YXpHRwLle6lhKJODnu7uH0krGjNRlUo+1oioKULA5jgQ6I6ctTrqFuaw4gZFA== +fast-check@^2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/fast-check/-/fast-check-2.11.0.tgz#a21bdbdcab27812fbf93612f3c84483883115ca2" + integrity sha512-galBVrbyjdHOW+WOCp/NFP3J6t6Pc0uajz0oJaUAFRXLHXt6lcUeD1bcBFqUWV1aeK9QJgeRpIYf4e+PHeASUQ== dependencies: - pure-rand "^4.0.0" + pure-rand "^4.1.1" fast-deep-equal@^2.0.1: version "2.0.1" @@ -8205,10 +8205,10 @@ punycode@^2.0.0, punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -pure-rand@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-4.0.0.tgz#df8f44bc1b82c4f3d0e245e8f7ced6f09c1e9dc4" - integrity sha512-5+HGyGi+6VygEcP1O4jMj0c5HyFgsP9lEy2uA4c+KBq84y21hpmv85wAzPZ/H+q1TUbP3mIMZhqFg08/HAOUqw== +pure-rand@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-4.1.1.tgz#9fca2d4af5c4e870bac337ed860977426ed17bf6" + integrity sha512-cZw4AL/KI6aDTdqHEbJPe2ZoHM3kSdpJRLJetv8c3tfq9o+PvQDXrHNEpB0AWukAGFx4fmeOerAGwkA4rtUgdA== q@^1.5.1: version "1.5.1"