diff --git a/.gitallowed b/.gitallowed index 20a94b7dae003..6ac984df8735b 100644 --- a/.gitallowed +++ b/.gitallowed @@ -14,7 +14,11 @@ account: '012345678913' # Account patterns used in the CHANGELOG account: '123456789012' + +111111111111 +222222222222 123456789012 +333333333333 # The account ID's of public facing ECR images for App Mesh Envoy # https://docs.aws.amazon.com/app-mesh/latest/userguide/envoy.html diff --git a/.github/workflows/close-stale-prs.yml b/.github/workflows/close-stale-prs.yml index 5fef3433ff06e..d8543bc1725df 100644 --- a/.github/workflows/close-stale-prs.yml +++ b/.github/workflows/close-stale-prs.yml @@ -2,7 +2,7 @@ on: schedule: # Cron format: min hr day month dow - cron: "0 0 * * *" - workflow_dispatch: + workflow_dispatch: jobs: close-stale-prs: permissions: @@ -23,5 +23,5 @@ jobs: important-checks-regex: AutoBuildv2Project1C6BFA3F warn-message: This PR has been in the STATE state for 3 weeks, and looks abandoned. To keep this PR from being closed, please continue work on it. If not, it will automatically be closed in a week. close-message: This PR has been deemed to be abandoned, and will be automatically closed. Please create a new PR for these changes if you think this decision has been made in error. - skip-labels: contribution/core + skip-labels: contribution/core, pr-linter/do-not-close close-label: closed-for-staleness diff --git a/CHANGELOG.v2.alpha.md b/CHANGELOG.v2.alpha.md index 17ed885b301e6..bb662176a330f 100644 --- a/CHANGELOG.v2.alpha.md +++ b/CHANGELOG.v2.alpha.md @@ -2,6 +2,23 @@ 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. +## [2.64.0-alpha.0](https://github.com/aws/aws-cdk/compare/v2.63.2-alpha.0...v2.64.0-alpha.0) (2023-02-09) + + +### Features + +* **cloud9:** support setting environment owner ([#23878](https://github.com/aws/aws-cdk/issues/23878)) ([08a2f36](https://github.com/aws/aws-cdk/commit/08a2f363093f39d04026778bb8d5d7f673698b57)), closes [#22474](https://github.com/aws/aws-cdk/issues/22474) +* **redshift:** Tables can include comments ([#23847](https://github.com/aws/aws-cdk/issues/23847)) ([46cadd4](https://github.com/aws/aws-cdk/commit/46cadd4b2dd417e1484ba63389b33e1504cfd842)), closes [#22682](https://github.com/aws/aws-cdk/issues/22682) + + +### Bug Fixes + +* **servicecatalogappregistry:** default stack name is not meaningful and causes conflict when multiple stacks deployed to the same account-region ([#23823](https://github.com/aws/aws-cdk/issues/23823)) ([420b5ff](https://github.com/aws/aws-cdk/commit/420b5ff2bd08311f2c8cabbe0787c0e0bf4f8ae3)) + +## [2.63.2-alpha.0](https://github.com/aws/aws-cdk/compare/v2.63.1-alpha.0...v2.63.2-alpha.0) (2023-02-04) + +## [2.63.1-alpha.0](https://github.com/aws/aws-cdk/compare/v2.63.0-alpha.0...v2.63.1-alpha.0) (2023-02-03) + ## [2.63.0-alpha.0](https://github.com/aws/aws-cdk/compare/v2.62.2-alpha.0...v2.63.0-alpha.0) (2023-01-31) diff --git a/CHANGELOG.v2.md b/CHANGELOG.v2.md index 44c0548929370..d78c292fd363f 100644 --- a/CHANGELOG.v2.md +++ b/CHANGELOG.v2.md @@ -2,6 +2,41 @@ 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. +## [2.64.0](https://github.com/aws/aws-cdk/compare/v2.63.2...v2.64.0) (2023-02-09) + + +### Features + +* **cfnspec:** cloudformation spec v109.0.0 ([#23968](https://github.com/aws/aws-cdk/issues/23968)) ([5d59134](https://github.com/aws/aws-cdk/commit/5d5913455da2cdb834feef708fb01f9e77df656f)) +* **cfnspec:** cloudformation spec v109.0.0 ([#23984](https://github.com/aws/aws-cdk/issues/23984)) ([affe040](https://github.com/aws/aws-cdk/commit/affe040c8443be074822254d1e75a28b264cd801)) +* **cli:** --hotswap will not use CFN anymore, --hotswap-fallback to fall back if necessary ([#23653](https://github.com/aws/aws-cdk/issues/23653)) ([a5317ca](https://github.com/aws/aws-cdk/commit/a5317ca52f05ebc34d9f22196ab0ef36d5cac967)), closes [#22784](https://github.com/aws/aws-cdk/issues/22784) [#21773](https://github.com/aws/aws-cdk/issues/21773) [#21556](https://github.com/aws/aws-cdk/issues/21556) [#23640](https://github.com/aws/aws-cdk/issues/23640) +* **elbv2:** add metrics to INetworkLoadBalancer and IApplicationLoadBalancer ([#23853](https://github.com/aws/aws-cdk/issues/23853)) ([cb889bc](https://github.com/aws/aws-cdk/commit/cb889bc2c267654ca97e3d85a16a99a667d3584c)), closes [#10850](https://github.com/aws/aws-cdk/issues/10850) +* **iam:** implement IGrantable to Policy and ManagedPolicy ([#22712](https://github.com/aws/aws-cdk/issues/22712)) ([d3df40f](https://github.com/aws/aws-cdk/commit/d3df40ff89c70b9243ec175747eb398368067095)), closes [#10308](https://github.com/aws/aws-cdk/issues/10308) +* **lambda:** enable RuntimeManagementConfig ([#23891](https://github.com/aws/aws-cdk/issues/23891)) ([be4f971](https://github.com/aws/aws-cdk/commit/be4f97129f4237b39d0b99977eb597e2af49ed2a)), closes [#23890](https://github.com/aws/aws-cdk/issues/23890) +* **s3:** allow configuring S3 Object Lock ([#23744](https://github.com/aws/aws-cdk/issues/23744)) ([bdcd6c8](https://github.com/aws/aws-cdk/commit/bdcd6c890878fb71c480bf40964f1b6ea0a5f270)), closes [#5247](https://github.com/aws/aws-cdk/issues/5247) [#21738](https://github.com/aws/aws-cdk/issues/21738) + + +### Bug Fixes + +* Use the correct LB full name when creating metrics for imported LBs ([#23972](https://github.com/aws/aws-cdk/issues/23972)) ([16c23b7](https://github.com/aws/aws-cdk/commit/16c23b7554923bf6c2703ba5f229e6c34b459a2f)), closes [#23853](https://github.com/aws/aws-cdk/issues/23853) +* **cdk-assets:** asset concurrency leaves a corrupted archive ([#24026](https://github.com/aws/aws-cdk/issues/24026)) ([989454f](https://github.com/aws/aws-cdk/commit/989454f7e27f3cbf33180d8aab29d56472378126)) +* **cdk-assets:** packaging assets is broken on Node older than 14.17 ([#23994](https://github.com/aws/aws-cdk/issues/23994)) ([5bde92c](https://github.com/aws/aws-cdk/commit/5bde92c2ae29781aafd8c3817d08e93748c39885)), closes [#23859](https://github.com/aws/aws-cdk/issues/23859) +* **codedeploy:** cross-region referenced groups use wrong config ([#23986](https://github.com/aws/aws-cdk/issues/23986)) ([390ec78](https://github.com/aws/aws-cdk/commit/390ec78437a55ad68757f8ce812535e9bc149a2a)) +* **core:** cross-stack reference error doesn't include violation ([#23987](https://github.com/aws/aws-cdk/issues/23987)) ([c7ad66f](https://github.com/aws/aws-cdk/commit/c7ad66fad6ca5aff5f2ae9754d263dea9d1de368)) +* **ec2:** Cannot deploy VPC flow log with other resources that requires bucket policies ([#23889](https://github.com/aws/aws-cdk/issues/23889)) ([e646ad5](https://github.com/aws/aws-cdk/commit/e646ad5b5496b176549f8c039a5ffabbf07403ff)), closes [#18985](https://github.com/aws/aws-cdk/issues/18985) +* **pipelines:** cannot configure actionName for all sources ([#24027](https://github.com/aws/aws-cdk/issues/24027)) ([9cd639b](https://github.com/aws/aws-cdk/commit/9cd639b0f83e65fbe531d56210f68e99874f506e)) +* **s3:** infer bucketWebsiteUrl and bucketDomainName suffixes from bucket region ([#23919](https://github.com/aws/aws-cdk/issues/23919)) ([252f052](https://github.com/aws/aws-cdk/commit/252f052d4239b320ac542c7db256683425ad7eba)) +* **s3-deployment:** wrong URL in BucketDeployment.deployedBucket.bucketWebsiteUrl ([#24055](https://github.com/aws/aws-cdk/issues/24055)) ([ece46db](https://github.com/aws/aws-cdk/commit/ece46dbd939383f240023172a491767b51eaa722)), closes [#23354](https://github.com/aws/aws-cdk/issues/23354) + +## [2.63.2](https://github.com/aws/aws-cdk/compare/v2.63.1...v2.63.2) (2023-02-04) + +## [2.63.1](https://github.com/aws/aws-cdk/compare/v2.63.0...v2.63.1) (2023-02-03) + + +### Reverts + +* **cdk-assets:** packaging assets is broken on Node older than 14.17 ([#23994](https://github.com/aws/aws-cdk/issues/23994)) ([1976f1a](https://github.com/aws/aws-cdk/commit/1976f1a7f585b1adb582c5cb557b96ed38418fca)), closes [#23859](https://github.com/aws/aws-cdk/issues/23859) + ## [2.63.0](https://github.com/aws/aws-cdk/compare/v2.62.2...v2.63.0) (2023-01-31) diff --git a/package.json b/package.json index 5ba300b3ae8cb..41b1578b5f8f6 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "devDependencies": { "@types/prettier": "2.6.0", "@yarnpkg/lockfile": "^1.1.0", - "cdk-generate-synthetic-examples": "^0.1.138", + "cdk-generate-synthetic-examples": "^0.1.140", "conventional-changelog-cli": "^2.2.2", "fs-extra": "^9.1.0", "graceful-fs": "^4.2.10", diff --git a/packages/@aws-cdk/aws-cloud9/README.md b/packages/@aws-cdk/aws-cloud9/README.md index 84b00eb03218e..f87860ae71b3d 100644 --- a/packages/@aws-cdk/aws-cloud9/README.md +++ b/packages/@aws-cdk/aws-cloud9/README.md @@ -23,19 +23,19 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. -AWS Cloud9 is a cloud-based integrated development environment (IDE) that lets you write, run, and debug your code with just a -browser. It includes a code editor, debugger, and terminal. Cloud9 comes prepackaged with essential tools for popular -programming languages, including JavaScript, Python, PHP, and more, so you don’t need to install files or configure your -development machine to start new projects. Since your Cloud9 IDE is cloud-based, you can work on your projects from your -office, home, or anywhere using an internet-connected machine. Cloud9 also provides a seamless experience for developing -serverless applications enabling you to easily define resources, debug, and switch between local and remote execution of -serverless applications. With Cloud9, you can quickly share your development environment with your team, enabling you to pair +AWS Cloud9 is a cloud-based integrated development environment (IDE) that lets you write, run, and debug your code with just a +browser. It includes a code editor, debugger, and terminal. Cloud9 comes prepackaged with essential tools for popular +programming languages, including JavaScript, Python, PHP, and more, so you don’t need to install files or configure your +development machine to start new projects. Since your Cloud9 IDE is cloud-based, you can work on your projects from your +office, home, or anywhere using an internet-connected machine. Cloud9 also provides a seamless experience for developing +serverless applications enabling you to easily define resources, debug, and switch between local and remote execution of +serverless applications. With Cloud9, you can quickly share your development environment with your team, enabling you to pair program and track each other's inputs in real time. ## Creating EC2 Environment -EC2 Environments are defined with `Ec2Environment`. To create an EC2 environment in the private subnet, specify +EC2 Environments are defined with `Ec2Environment`. To create an EC2 environment in the private subnet, specify `subnetSelection` with private `subnetType`. @@ -52,7 +52,7 @@ new cloud9.Ec2Environment(this, 'Cloud9Env2', { imageId: cloud9.ImageId.AMAZON_LINUX_2, }); -// or specify in a different subnetSelection +// or specify in a different subnetSelection const c9env = new cloud9.Ec2Environment(this, 'Cloud9Env3', { vpc, subnetSelection: { @@ -104,3 +104,39 @@ new cloud9.Ec2Environment(this, 'C9Env', { imageId: cloud9.ImageId.AMAZON_LINUX_2, }); ``` + +## Specifying Owners + +Every Cloud9 Environment has an **owner**. An owner has full control over the environment, and can invite additional members to the environment for collaboration purposes. For more information, see [Working with shared environments in AWS Cloud9](https://docs.aws.amazon.com/cloud9/latest/user-guide/share-environment.html)). + +By default, the owner will be the identity that creates the Environment, which is most likely your CloudFormation Execution Role when the Environment is created using CloudFormation. Provider a value for the `owner` property to assign a different owner, either a specific IAM User or the AWS Account Root User. + +`Owner` is a user that owns a Cloud9 environment . `Owner` has their own access permissions, resources. And we can specify an `Owner`in an Ec2 environment which could be of two types, 1. AccountRoot and 2. Iam User. It allows AWS to determine who has permissions to manage the environment, either an IAM user or the account root user (but using the account root user is not recommended, see [environment sharing best practices](https://docs.aws.amazon.com/cloud9/latest/user-guide/share-environment.html#share-environment-best-practices)). + +To specify the AWS Account Root User as the environment owner, use `Owner.accountRoot()` + +```ts +declare const vpc: ec2.Vpc; +new cloud9.Ec2Environment(this, 'C9Env', { + vpc, + imageId: cloud9.ImageId.AMAZON_LINUX_2, + + owner: cloud9.Owner.accountRoot('111111111') +}) +``` + +To specify a specific IAM User as the environment owner, use `Owner.user()`. The user should have the `AWSCloud9Administrator` managed policy + +```ts +import * as iam from '@aws-cdk/aws-iam'; + +const user = new iam.User(this, 'user'); +user.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AWSCloud9Administrator')); +declare const vpc: ec2.Vpc; +new cloud9.Ec2Environment(this, 'C9Env', { + vpc, + imageId: cloud9.ImageId.AMAZON_LINUX_2, + + owner: cloud9.Owner.user(user) +}) +``` diff --git a/packages/@aws-cdk/aws-cloud9/lib/environment.ts b/packages/@aws-cdk/aws-cloud9/lib/environment.ts index 15d7390dd6ee5..d1e4565f786ff 100644 --- a/packages/@aws-cdk/aws-cloud9/lib/environment.ts +++ b/packages/@aws-cdk/aws-cloud9/lib/environment.ts @@ -1,5 +1,6 @@ import * as codecommit from '@aws-cdk/aws-codecommit'; import * as ec2 from '@aws-cdk/aws-ec2'; +import { IUser } from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnEnvironmentEC2 } from '../lib/cloud9.generated'; @@ -53,11 +54,19 @@ export enum ImageId { */ UBUNTU_18_04 = 'ubuntu-18.04-x86_64' } - /** * Properties for Ec2Environment */ export interface Ec2EnvironmentProps { + /** + * Owner of the environment. + * + * The owner has full control of the environment and can invite additional members. + * + * @default - The identity that CloudFormation executes under will be the owner + */ + readonly owner?: Owner; + /** * The type of instance to connect to the environment. * @@ -182,6 +191,7 @@ export class Ec2Environment extends cdk.Resource implements IEc2Environment { const c9env = new CfnEnvironmentEC2(this, 'Resource', { name: props.ec2EnvironmentName, description: props.description, + ownerArn: props.owner?.ownerArn, instanceType: props.instanceType?.toString() ?? ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.MICRO).toString(), subnetId: this.vpc.selectSubnets(vpcSubnets).subnetIds[0], repositories: props.clonedRepositories ? props.clonedRepositories.map(r => ({ @@ -217,3 +227,38 @@ export class CloneRepository { private constructor(public readonly repositoryUrl: string, public readonly pathComponent: string) {} } + +/** + * An environment owner + * + * + */ +export class Owner { + /** + * Make an IAM user the environment owner + * + * User need to have AWSCloud9Administrator permissions + * @see https://docs.aws.amazon.com/cloud9/latest/user-guide/share-environment.html#share-environment-about + * + * @param user the User object to use as the environment owner + */ + public static user(user: IUser): Owner { + return { ownerArn: user.userArn }; + } + + + /** + * Make the Account Root User the environment owner (not recommended) + * + * @param accountId the AccountId to use as the environment owner. + */ + public static accountRoot(accountId: string): Owner { + return { ownerArn: `arn:aws:iam::${accountId}:root` }; + } + + /** + * + * @param ownerArn of environment owner. + */ + private constructor(public readonly ownerArn: string) {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloud9/package.json b/packages/@aws-cdk/aws-cloud9/package.json index c52e71087c1e2..b76154609d311 100644 --- a/packages/@aws-cdk/aws-cloud9/package.json +++ b/packages/@aws-cdk/aws-cloud9/package.json @@ -92,6 +92,7 @@ "dependencies": { "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^10.0.0" }, @@ -99,6 +100,7 @@ "peerDependencies": { "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^10.0.0" }, diff --git a/packages/@aws-cdk/aws-cloud9/test/cloud9.environment.test.ts b/packages/@aws-cdk/aws-cloud9/test/cloud9.environment.test.ts index 948cfa5bee9ec..69210bf01a135 100644 --- a/packages/@aws-cdk/aws-cloud9/test/cloud9.environment.test.ts +++ b/packages/@aws-cdk/aws-cloud9/test/cloud9.environment.test.ts @@ -1,9 +1,10 @@ import { Match, Template } from '@aws-cdk/assertions'; import * as codecommit from '@aws-cdk/aws-codecommit'; import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as cloud9 from '../lib'; -import { ConnectionType, ImageId } from '../lib'; +import { ConnectionType, ImageId, Owner } from '../lib'; let stack: cdk.Stack; let vpc: ec2.IVpc; @@ -79,7 +80,6 @@ test('throw error when subnetSelection not specified and the provided VPC has no test('can use CodeCommit repositories', () => { // WHEN const repo = codecommit.Repository.fromRepositoryName(stack, 'Repo', 'foo'); - new cloud9.Ec2Environment(stack, 'C9Env', { vpc, clonedRepositories: [ @@ -114,6 +114,37 @@ test('can use CodeCommit repositories', () => { }); }); +test('environment owner can be an IAM user', () => { + // WHEN + const user = new iam.User(stack, 'User', { + userName: 'testUser', + }); + new cloud9.Ec2Environment(stack, 'C9Env', { + vpc, + imageId: cloud9.ImageId.AMAZON_LINUX_2, + owner: Owner.user(user), + }); + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cloud9::EnvironmentEC2', { + OwnerArn: { + 'Fn::GetAtt': ['User00B015A1', 'Arn'], + }, + }); +}); + +test('environment owner can be account root', () => { + // WHEN + new cloud9.Ec2Environment(stack, 'C9Env', { + vpc, + imageId: cloud9.ImageId.AMAZON_LINUX_2, + owner: Owner.accountRoot('12345678'), + }); + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cloud9::EnvironmentEC2', { + OwnerArn: 'arn:aws:iam::12345678:root', + }); +}); + test.each([ [ConnectionType.CONNECT_SSH, 'CONNECT_SSH'], [ConnectionType.CONNECT_SSM, 'CONNECT_SSM'], diff --git a/packages/@aws-cdk/aws-codedeploy/lib/ecs/deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/lib/ecs/deployment-group.ts index 40ddcfb31069e..7b549e79d173c 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/ecs/deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/ecs/deployment-group.ts @@ -222,7 +222,7 @@ export class EcsDeploymentGroup extends DeploymentGroupBase implements IEcsDeplo this.alarms = props.alarms || []; this.role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AWSCodeDeployRoleForECS')); - this.deploymentConfig = props.deploymentConfig || EcsDeploymentConfig.ALL_AT_ONCE; + this.deploymentConfig = this._bindDeploymentConfig(props.deploymentConfig || EcsDeploymentConfig.ALL_AT_ONCE); if (cdk.Resource.isOwnedResource(props.service)) { const cfnSvc = (props.service as ecs.BaseService).node.defaultChild as ecs.CfnService; @@ -358,6 +358,6 @@ class ImportedEcsDeploymentGroup extends ImportedDeploymentGroupBase implements }); this.application = props.application; - this.deploymentConfig = props.deploymentConfig || EcsDeploymentConfig.ALL_AT_ONCE; + this.deploymentConfig = this._bindDeploymentConfig(props.deploymentConfig || EcsDeploymentConfig.ALL_AT_ONCE); } } diff --git a/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-group.ts index 85110a037e6ea..ea61370f9a140 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/lambda/deployment-group.ts @@ -162,7 +162,7 @@ export class LambdaDeploymentGroup extends DeploymentGroupBase implements ILambd this.alarms = props.alarms || []; this.role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSCodeDeployRoleForLambdaLimited')); - this.deploymentConfig = props.deploymentConfig || LambdaDeploymentConfig.CANARY_10PERCENT_5MINUTES; + this.deploymentConfig = this._bindDeploymentConfig(props.deploymentConfig || LambdaDeploymentConfig.CANARY_10PERCENT_5MINUTES); const resource = new CfnDeploymentGroup(this, 'Resource', { applicationName: this.application.applicationName, @@ -290,6 +290,6 @@ class ImportedLambdaDeploymentGroup extends ImportedDeploymentGroupBase implemen }); this.application = props.application; - this.deploymentConfig = props.deploymentConfig || LambdaDeploymentConfig.CANARY_10PERCENT_5MINUTES; + this.deploymentConfig = this._bindDeploymentConfig(props.deploymentConfig || LambdaDeploymentConfig.CANARY_10PERCENT_5MINUTES); } } diff --git a/packages/@aws-cdk/aws-codedeploy/lib/private/base-deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/lib/private/base-deployment-group.ts index 3ea12aa50d702..e3d09743bff58 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/private/base-deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/private/base-deployment-group.ts @@ -1,7 +1,9 @@ import * as iam from '@aws-cdk/aws-iam'; import { Resource, IResource, ArnFormat, Arn, Aws } from '@aws-cdk/core'; import { Construct } from 'constructs'; +import { IBaseDeploymentConfig } from '../base-deployment-config'; import { CfnDeploymentGroup } from '../codedeploy.generated'; +import { isPredefinedDeploymentConfig } from './predefined-deployment-config'; import { validateName } from './utils'; /** @@ -52,6 +54,15 @@ export class ImportedDeploymentGroupBase extends Resource { this.deploymentGroupName = deploymentGroupName; this.deploymentGroupArn = deploymentGroupArn; } + + /** + * Bind DeploymentGroupConfig to the current group, if supported + * + * @internal + */ + protected _bindDeploymentConfig(config: IBaseDeploymentConfig) { + return isPredefinedDeploymentConfig(config) ? config.bindEnvironment(this) : config; + } } export interface DeploymentGroupBaseProps { @@ -114,6 +125,15 @@ export class DeploymentGroupBase extends Resource { this.node.addValidation({ validate: () => validateName('Deployment group', this.physicalName) }); } + /** + * Bind DeploymentGroupConfig to the current group, if supported + * + * @internal + */ + protected _bindDeploymentConfig(config: IBaseDeploymentConfig) { + return isPredefinedDeploymentConfig(config) ? config.bindEnvironment(this) : config; + } + /** * Set name and ARN properties. * @@ -135,4 +155,4 @@ export class DeploymentGroupBase extends Resource { arnFormat: ArnFormat.COLON_RESOURCE_NAME, }); } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-codedeploy/lib/private/predefined-deployment-config.ts b/packages/@aws-cdk/aws-codedeploy/lib/private/predefined-deployment-config.ts new file mode 100644 index 0000000000000..3341a4df0af95 --- /dev/null +++ b/packages/@aws-cdk/aws-codedeploy/lib/private/predefined-deployment-config.ts @@ -0,0 +1,31 @@ +import { IResource } from '@aws-cdk/core'; +import { IBaseDeploymentConfig } from '../base-deployment-config'; + +/** + * A reference to a DeploymentConfig that is managed by AWS + * + * Since these DeploymentConfigs are present in every region, and we might use + * them in conjunction with cross-region DeploymentGroups, we need to specialize + * the account and region to the DeploymentGroup before using. + * + * A DeploymentGroup must call `bindEnvironment()` first if it detects this type, + * before reading the DeploymentConfig ARN. + * + * This type is fully hidden, which means that the constant objects provided by + * CDK will have magical behavior that customers can't reimplement themselves. + * Not ideal, but our DeploymentConfig type inheritance is already overly + * complicated and to do it properly with the nominal typing we are emplying + * will require adding 4 more empty or nearly empty interfaces, which seems a + * bit silly for a need that's not necessarily clearly needed by customers. + * We can always move to exposing later. + */ +export interface IPredefinedDeploymentConfig { + /** + * Bind the predefined deployment config to the environment of the given resource + */ + bindEnvironment(deploymentGroup: IResource): IBaseDeploymentConfig; +} + +export function isPredefinedDeploymentConfig(x: unknown): x is IPredefinedDeploymentConfig { + return typeof x === 'object' && !!x && !!(x as any).bindEnvironment; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codedeploy/lib/private/utils.ts b/packages/@aws-cdk/aws-codedeploy/lib/private/utils.ts index 9dccb367d8578..2cd2857e3079f 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/private/utils.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/private/utils.ts @@ -1,8 +1,9 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; -import { Token, Stack, ArnFormat, Arn, Fn, Aws } from '@aws-cdk/core'; +import { Token, Stack, ArnFormat, Arn, Fn, Aws, IResource } from '@aws-cdk/core'; import { IBaseDeploymentConfig } from '../base-deployment-config'; import { CfnDeploymentGroup } from '../codedeploy.generated'; import { AutoRollbackConfig } from '../rollback-config'; +import { IPredefinedDeploymentConfig } from './predefined-deployment-config'; export function arnForApplication(stack: Stack, applicationName: string): string { return stack.formatArn({ @@ -18,11 +19,11 @@ export function nameFromDeploymentGroupArn(deploymentGroupArn: string): string { return Fn.select(1, Fn.split('/', components.resourceName ?? '')); } -export function arnForDeploymentConfig(name: string): string { +export function arnForDeploymentConfig(name: string, resource?: IResource): string { return Arn.format({ partition: Aws.PARTITION, - account: Aws.ACCOUNT_ID, - region: Aws.REGION, + account: resource?.env.account ?? Aws.ACCOUNT_ID, + region: resource?.env.region ?? Aws.REGION, service: 'codedeploy', resource: 'deploymentconfig', resourceName: name, @@ -41,10 +42,14 @@ CfnDeploymentGroup.AlarmConfigurationProperty | undefined { }; } -export function deploymentConfig(name: string): IBaseDeploymentConfig { +export function deploymentConfig(name: string): IBaseDeploymentConfig & IPredefinedDeploymentConfig { return { deploymentConfigName: name, deploymentConfigArn: arnForDeploymentConfig(name), + bindEnvironment: (resource) => ({ + deploymentConfigName: name, + deploymentConfigArn: arnForDeploymentConfig(name, resource), + }), }; } diff --git a/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-group.ts index 828529358a6cd..b78bc0179af67 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-group.ts @@ -68,7 +68,7 @@ class ImportedServerDeploymentGroup extends ImportedDeploymentGroupBase implemen }); this.application = props.application; - this.deploymentConfig = props.deploymentConfig || ServerDeploymentConfig.ONE_AT_A_TIME; + this.deploymentConfig = this._bindDeploymentConfig(props.deploymentConfig || ServerDeploymentConfig.ONE_AT_A_TIME); } } @@ -255,7 +255,7 @@ export class ServerDeploymentGroup extends DeploymentGroupBase implements IServe this.application = props.application || new ServerApplication(this, 'Application', { applicationName: props.deploymentGroupName === cdk.PhysicalName.GENERATE_IF_NEEDED ? cdk.PhysicalName.GENERATE_IF_NEEDED : undefined, }); - this.deploymentConfig = props.deploymentConfig || ServerDeploymentConfig.ONE_AT_A_TIME; + this.deploymentConfig = this._bindDeploymentConfig(props.deploymentConfig || ServerDeploymentConfig.ONE_AT_A_TIME); this.role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSCodeDeployRole')); this._autoScalingGroups = props.autoScalingGroups || []; diff --git a/packages/@aws-cdk/aws-codedeploy/test/ecs/deployment-group.test.ts b/packages/@aws-cdk/aws-codedeploy/test/ecs/deployment-group.test.ts index aed7b8655516e..0b6a6ee653baa 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/ecs/deployment-group.test.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/ecs/deployment-group.test.ts @@ -5,7 +5,7 @@ import * as ecs from '@aws-cdk/aws-ecs'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; -import { Duration } from '@aws-cdk/core'; +import { Duration, Stack } from '@aws-cdk/core'; import * as codedeploy from '../../lib'; const mockCluster = 'my-cluster'; @@ -45,7 +45,7 @@ describe('CodeDeploy ECS DeploymentGroup', () => { deploymentGroupName: 'EcsDeploymentGroup', }); - expect(importedGroup.deploymentConfig).toEqual(codedeploy.EcsDeploymentConfig.ALL_AT_ONCE); + expect(importedGroup.deploymentConfig.deploymentConfigName).toEqual('CodeDeployDefault.ECSAllAtOnce'); }); }); @@ -849,25 +849,34 @@ describe('CodeDeploy ECS DeploymentGroup', () => { }); }); - test('deploymentGroup from Arn knows its account and region', () => { - // GIVEN - const stack = new cdk.Stack(undefined, 'Stack', { env: { account: '111111111111', region: 'blabla-1' } }); + describe('deploymentGroup from ARN in different account and region', () => { + let stack: Stack; + let application: codedeploy.IEcsApplication; + let group: codedeploy.IEcsDeploymentGroup; - // WHEN - const application = codedeploy.EcsApplication.fromEcsApplicationArn(stack, 'Application', 'arn:aws:codedeploy:theregion-1:222222222222:application:MyApplication'); - const group = codedeploy.EcsDeploymentGroup.fromEcsDeploymentGroupAttributes(stack, 'Group', { - application, - deploymentGroupName: 'DeploymentGroup', + const account = '222222222222'; + const region = 'theregion-1'; + + beforeEach(() => { + stack = new cdk.Stack(undefined, 'Stack', { env: { account: '111111111111', region: 'blabla-1' } }); + + application = codedeploy.EcsApplication.fromEcsApplicationArn(stack, 'Application', `arn:aws:codedeploy:${region}:${account}:application:MyApplication`); + group = codedeploy.EcsDeploymentGroup.fromEcsDeploymentGroupAttributes(stack, 'Group', { + application, + deploymentGroupName: 'DeploymentGroup', + }); }); - // THEN - expect(application.env).toEqual(expect.objectContaining({ - account: '222222222222', - region: 'theregion-1', - })); - expect(group.env).toEqual(expect.objectContaining({ - account: '222222222222', - region: 'theregion-1', - })); + test('knows its account and region', () => { + // THEN + expect(application.env).toEqual(expect.objectContaining({ account, region })); + expect(group.env).toEqual(expect.objectContaining({ account, region })); + }); + + test('references the predefined DeploymentGroupConfig in the right region', () => { + expect(group.deploymentConfig.deploymentConfigArn).toEqual(expect.stringContaining( + `:codedeploy:${region}:${account}:deploymentconfig:CodeDeployDefault.ECSAllAtOnce`, + )); + }); }); }); diff --git a/packages/@aws-cdk/aws-codedeploy/test/lambda/deployment-group.test.ts b/packages/@aws-cdk/aws-codedeploy/test/lambda/deployment-group.test.ts index 9eae61c7a4e4f..202f4127432ed 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/lambda/deployment-group.test.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/lambda/deployment-group.test.ts @@ -3,6 +3,7 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; +import { Stack } from '@aws-cdk/core'; import * as codedeploy from '../../lib'; import { TrafficRouting } from '../../lib'; @@ -616,26 +617,35 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { }); }); - test('deploymentGroup from Arn knows its account and region', () => { - // GIVEN - const stack = new cdk.Stack(undefined, 'Stack', { env: { account: '111111111111', region: 'blabla-1' } }); + describe('deploymentGroup from ARN in different account and region', () => { + let stack: Stack; + let application: codedeploy.ILambdaApplication; + let group: codedeploy.ILambdaDeploymentGroup; - // WHEN - const application = codedeploy.LambdaApplication.fromLambdaApplicationArn(stack, 'Application', 'arn:aws:codedeploy:theregion-1:222222222222:application:MyApplication'); - const group = codedeploy.LambdaDeploymentGroup.fromLambdaDeploymentGroupAttributes(stack, 'Group', { - application, - deploymentGroupName: 'DeploymentGroup', - }); - - // THEN - expect(application.env).toEqual(expect.objectContaining({ - account: '222222222222', - region: 'theregion-1', - })); - expect(group.env).toEqual(expect.objectContaining({ - account: '222222222222', - region: 'theregion-1', - })); + const account = '222222222222'; + const region = 'theregion-1'; + + beforeEach(() => { + stack = new cdk.Stack(undefined, 'Stack', { env: { account: '111111111111', region: 'blabla-1' } }); + + application = codedeploy.LambdaApplication.fromLambdaApplicationArn(stack, 'Application', `arn:aws:codedeploy:${region}:${account}:application:MyApplication`); + group = codedeploy.LambdaDeploymentGroup.fromLambdaDeploymentGroupAttributes(stack, 'Group', { + application, + deploymentGroupName: 'DeploymentGroup', + }); + }); + + test('knows its account and region', () => { + // THEN + expect(application.env).toEqual(expect.objectContaining({ account, region })); + expect(group.env).toEqual(expect.objectContaining({ account, region })); + }); + + test('references the predefined DeploymentGroupConfig in the right region', () => { + expect(group.deploymentConfig.deploymentConfigArn).toEqual(expect.stringContaining( + `:codedeploy:${region}:${account}:deploymentconfig:CodeDeployDefault.LambdaCanary10Percent5Minutes`, + )); + }); }); }); @@ -649,7 +659,7 @@ describe('imported with fromLambdaDeploymentGroupAttributes', () => { deploymentGroupName: 'LambdaDeploymentGroup', }); - expect(importedGroup.deploymentConfig).toEqual(codedeploy.LambdaDeploymentConfig.CANARY_10PERCENT_5MINUTES); + expect(importedGroup.deploymentConfig.deploymentConfigName).toEqual('CodeDeployDefault.LambdaCanary10Percent5Minutes'); }); }); diff --git a/packages/@aws-cdk/aws-codedeploy/test/server/deployment-group.test.ts b/packages/@aws-cdk/aws-codedeploy/test/server/deployment-group.test.ts index 4f360aa9cb9c2..02d76400d3120 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/server/deployment-group.test.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/server/deployment-group.test.ts @@ -494,25 +494,34 @@ describe('CodeDeploy Server Deployment Group', () => { expect(() => app.synth()).toThrow('Deployment group name: "my name" can only contain letters (a-z, A-Z), numbers (0-9), periods (.), underscores (_), + (plus signs), = (equals signs), , (commas), @ (at signs), - (minus signs).'); }); - test('deploymentGroup from Arn knows its account and region', () => { - // GIVEN - const stack = new cdk.Stack(undefined, 'Stack', { env: { account: '111111111111', region: 'blabla-1' } }); + describe('deploymentGroup from ARN in different account and region', () => { + let stack: cdk.Stack; + let application: codedeploy.IServerApplication; + let group: codedeploy.IServerDeploymentGroup; - // WHEN - const application = codedeploy.ServerApplication.fromServerApplicationArn(stack, 'Application', 'arn:aws:codedeploy:theregion-1:222222222222:application:MyApplication'); - const group = codedeploy.ServerDeploymentGroup.fromServerDeploymentGroupAttributes(stack, 'Group', { - application, - deploymentGroupName: 'DeploymentGroup', - }); - - // THEN - expect(application.env).toEqual(expect.objectContaining({ - account: '222222222222', - region: 'theregion-1', - })); - expect(group.env).toEqual(expect.objectContaining({ - account: '222222222222', - region: 'theregion-1', - })); + const account = '222222222222'; + const region = 'theregion-1'; + + beforeEach(() => { + stack = new cdk.Stack(undefined, 'Stack', { env: { account: '111111111111', region: 'blabla-1' } }); + + application = codedeploy.ServerApplication.fromServerApplicationArn(stack, 'Application', `arn:aws:codedeploy:${region}:${account}:application:MyApplication`); + group = codedeploy.ServerDeploymentGroup.fromServerDeploymentGroupAttributes(stack, 'Group', { + application, + deploymentGroupName: 'DeploymentGroup', + }); + }); + + test('knows its account and region', () => { + // THEN + expect(application.env).toEqual(expect.objectContaining({ account, region })); + expect(group.env).toEqual(expect.objectContaining({ account, region })); + }); + + test('references the predefined DeploymentGroupConfig in the right region', () => { + expect(group.deploymentConfig.deploymentConfigArn).toEqual(expect.stringContaining( + `:codedeploy:${region}:${account}:deploymentconfig:CodeDeployDefault.OneAtATime`, + )); + }); }); }); diff --git a/packages/@aws-cdk/aws-ecr-assets/README.md b/packages/@aws-cdk/aws-ecr-assets/README.md index e6f8fa7c5f43a..32059529f1ee1 100644 --- a/packages/@aws-cdk/aws-ecr-assets/README.md +++ b/packages/@aws-cdk/aws-ecr-assets/README.md @@ -56,6 +56,9 @@ the `buildArgs` property. It is recommended to skip hashing of `buildArgs` for values that can change between different machines to maintain a consistent asset hash. +Additionally, you can supply `buildSecrets`. Your system must have Buildkit +enabled, see https://docs.docker.com/build/buildkit/. + ```ts import { DockerImageAsset } from '@aws-cdk/aws-ecr-assets'; diff --git a/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts b/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts index a0269f1f495f3..da03e18f80046 100644 --- a/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts +++ b/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts @@ -98,6 +98,13 @@ export interface DockerImageAssetInvalidationOptions { */ readonly buildArgs?: boolean; + /** + * Use `buildSecrets` while calculating the asset hash + * + * @default true + */ + readonly buildSecrets?: boolean; + /** * Use `target` while calculating the asset hash * @@ -170,6 +177,23 @@ export interface DockerImageAssetOptions extends FingerprintOptions, FileFingerp */ readonly buildArgs?: { [key: string]: string }; + /** + * Build secrets. + * + * Docker BuildKit must be enabled to use build secrets. + * + * @see https://docs.docker.com/build/buildkit/ + * + * @default - no build secrets + * + * @example + * + * { + * 'MY_SECRET': DockerBuildSecret.fromSrc('file.txt') + * } + */ + readonly buildSecrets?: { [key: string]: string } + /** * Docker target to build to * @@ -282,6 +306,11 @@ export class DockerImageAsset extends Construct implements IAsset { */ private readonly dockerBuildArgs?: { [key: string]: string }; + /** + * Build secrets to pass to the `docker build` command. + */ + private readonly dockerBuildSecrets?: { [key: string]: string }; + /** * Outputs to pass to the `docker build` command. */ @@ -345,6 +374,7 @@ export class DockerImageAsset extends Construct implements IAsset { const extraHash: { [field: string]: any } = {}; if (props.invalidation?.extraHash !== false && props.extraHash) { extraHash.user = props.extraHash; } if (props.invalidation?.buildArgs !== false && props.buildArgs) { extraHash.buildArgs = props.buildArgs; } + if (props.invalidation?.buildSecrets !== false && props.buildSecrets) { extraHash.buildSecrets = props.buildSecrets; } if (props.invalidation?.target !== false && props.target) { extraHash.target = props.target; } if (props.invalidation?.file !== false && props.file) { extraHash.file = props.file; } if (props.invalidation?.repositoryName !== false && props.repositoryName) { extraHash.repositoryName = props.repositoryName; } @@ -374,12 +404,14 @@ export class DockerImageAsset extends Construct implements IAsset { const stack = Stack.of(this); this.assetPath = staging.relativeStagedPath(stack); this.dockerBuildArgs = props.buildArgs; + this.dockerBuildSecrets = props.buildSecrets; this.dockerBuildTarget = props.target; this.dockerOutputs = props.outputs; const location = stack.synthesizer.addDockerImageAsset({ directoryName: this.assetPath, dockerBuildArgs: this.dockerBuildArgs, + dockerBuildSecrets: this.dockerBuildSecrets, dockerBuildTarget: this.dockerBuildTarget, dockerFile: props.file, sourceHash: staging.assetHash, @@ -420,6 +452,7 @@ export class DockerImageAsset extends Construct implements IAsset { resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_PATH_KEY] = this.assetPath; resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_DOCKERFILE_PATH_KEY] = this.dockerfilePath; resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_DOCKER_BUILD_ARGS_KEY] = this.dockerBuildArgs; + resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_DOCKER_BUILD_SECRETS_KEY] = this.dockerBuildSecrets; resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_DOCKER_BUILD_TARGET_KEY] = this.dockerBuildTarget; resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY] = resourceProperty; resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_DOCKER_OUTPUTS_KEY] = this.dockerOutputs; @@ -435,16 +468,25 @@ function validateProps(props: DockerImageAssetProps) { } validateBuildArgs(props.buildArgs); + validateBuildSecrets(props.buildSecrets); } -function validateBuildArgs(buildArgs?: { [key: string]: string }) { - for (const [key, value] of Object.entries(buildArgs || {})) { +function validateBuildProps(buildPropName: string, buildProps?: { [key: string]: string }) { + for (const [key, value] of Object.entries(buildProps || {})) { if (Token.isUnresolved(key) || Token.isUnresolved(value)) { - throw new Error('Cannot use tokens in keys or values of "buildArgs" since they are needed before deployment'); + throw new Error(`Cannot use tokens in keys or values of "${buildPropName}" since they are needed before deployment`); } } } +function validateBuildArgs(buildArgs?: { [key: string]: string }) { + validateBuildProps('buildArgs', buildArgs); +} + +function validateBuildSecrets(buildSecrets?: { [key: string]: string }) { + validateBuildProps('buildSecrets', buildSecrets); +} + function toSymlinkFollow(follow?: FollowMode): SymlinkFollowMode | undefined { switch (follow) { case undefined: return undefined; diff --git a/packages/@aws-cdk/aws-ecr-assets/test/demo-image-secret/Dockerfile b/packages/@aws-cdk/aws-ecr-assets/test/demo-image-secret/Dockerfile new file mode 100644 index 0000000000000..72a0396611404 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr-assets/test/demo-image-secret/Dockerfile @@ -0,0 +1,6 @@ +FROM public.ecr.aws/lambda/python:3.6 +RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret +EXPOSE 8000 +WORKDIR /src +ADD . /src +CMD python3 index.py diff --git a/packages/@aws-cdk/aws-ecr-assets/test/demo-image-secret/index.py b/packages/@aws-cdk/aws-ecr-assets/test/demo-image-secret/index.py new file mode 100644 index 0000000000000..2ccedfce3ab76 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr-assets/test/demo-image-secret/index.py @@ -0,0 +1,33 @@ +#!/usr/bin/python +import sys +import textwrap +import http.server +import socketserver + +PORT = 8000 + + +class Handler(http.server.SimpleHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header('Content-Type', 'text/html') + self.end_headers() + self.wfile.write(textwrap.dedent('''\ + + It works + +

Hello from the integ test container

+

This container got built and started as part of the integ test.

+ + + ''').encode('utf-8')) + + +def main(): + httpd = http.server.HTTPServer(("", PORT), Handler) + print("serving at port", PORT) + httpd.serve_forever() + + +if __name__ == '__main__': + main() diff --git a/packages/@aws-cdk/aws-ecr-assets/test/image-asset.test.ts b/packages/@aws-cdk/aws-ecr-assets/test/image-asset.test.ts index db926dedb562f..2b5c98a100f4d 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/image-asset.test.ts +++ b/packages/@aws-cdk/aws-ecr-assets/test/image-asset.test.ts @@ -2,7 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { describeDeprecated, testDeprecated } from '@aws-cdk/cdk-build-tools'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; -import { App, DefaultStackSynthesizer, IgnoreMode, Lazy, LegacyStackSynthesizer, Stack, Stage } from '@aws-cdk/core'; +import { App, DefaultStackSynthesizer, DockerBuildSecret, IgnoreMode, Lazy, LegacyStackSynthesizer, Stack, Stage } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { DockerImageAsset } from '../lib'; @@ -150,6 +150,7 @@ describe('image asset', () => { const asset5 = new DockerImageAsset(stack, 'Asset5', { directory, file: 'Dockerfile.Custom', target: 'NonDefaultTarget' }); const asset6 = new DockerImageAsset(stack, 'Asset6', { directory, extraHash: 'random-extra' }); const asset7 = new DockerImageAsset(stack, 'Asset7', { directory, outputs: ['123'] }); + const asset8 = new DockerImageAsset(stack, 'Asset8', { directory, buildSecrets: { mySecret: DockerBuildSecret.fromSrc('abc.txt') } }); expect(asset1.assetHash).toEqual('13248c55633f3b198a628bb2ea4663cb5226f8b2801051bd0c725950266fd590'); expect(asset2.assetHash).toEqual('36bf205fb9adc5e45ba1c8d534158a0aed96d190eff433af1d90f3b94f96e751'); @@ -158,6 +159,7 @@ describe('image asset', () => { expect(asset5.assetHash).toEqual('c02bfba13b2e7e1ff5c778a76e10296b9e8d17f7f8252d097f4170ae04ce0eb4'); expect(asset6.assetHash).toEqual('3528d6838647a5e9011b0f35aec514d03ad11af05a94653cdcf4dacdbb070a06'); expect(asset7.assetHash).toEqual('ced0a3076efe217f9cbdff0943e543f36ecf77f70b9a6fe28b8633deb728a462'); + expect(asset8.assetHash).toEqual('ffc2718e616141d18c8f4623d13cdfd68cb8f010ca5db31c916c8b5f10c162be'); }); diff --git a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/asset.60dea2e16e94d1977b92fe03fa7085fea446233f1fe499702b69593438baa59f/Dockerfile b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/asset.60dea2e16e94d1977b92fe03fa7085fea446233f1fe499702b69593438baa59f/Dockerfile new file mode 100644 index 0000000000000..72a0396611404 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/asset.60dea2e16e94d1977b92fe03fa7085fea446233f1fe499702b69593438baa59f/Dockerfile @@ -0,0 +1,6 @@ +FROM public.ecr.aws/lambda/python:3.6 +RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret +EXPOSE 8000 +WORKDIR /src +ADD . /src +CMD python3 index.py diff --git a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/asset.60dea2e16e94d1977b92fe03fa7085fea446233f1fe499702b69593438baa59f/index.py b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/asset.60dea2e16e94d1977b92fe03fa7085fea446233f1fe499702b69593438baa59f/index.py new file mode 100644 index 0000000000000..2ccedfce3ab76 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/asset.60dea2e16e94d1977b92fe03fa7085fea446233f1fe499702b69593438baa59f/index.py @@ -0,0 +1,33 @@ +#!/usr/bin/python +import sys +import textwrap +import http.server +import socketserver + +PORT = 8000 + + +class Handler(http.server.SimpleHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header('Content-Type', 'text/html') + self.end_headers() + self.wfile.write(textwrap.dedent('''\ + + It works + +

Hello from the integ test container

+

This container got built and started as part of the integ test.

+ + + ''').encode('utf-8')) + + +def main(): + httpd = http.server.HTTPServer(("", PORT), Handler) + print("serving at port", PORT) + httpd.serve_forever() + + +if __name__ == '__main__': + main() diff --git a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/cdk.out b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/cdk.out index e425c25d285ad..d8b441d447f8a 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/cdk.out +++ b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"24.0.0"} \ No newline at end of file +{"version":"29.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/integ-assets-docker.assets.json b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/integ-assets-docker.assets.json index 4630499e9d3a3..9e78ad767b944 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/integ-assets-docker.assets.json +++ b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/integ-assets-docker.assets.json @@ -1,7 +1,7 @@ { - "version": "24.0.0", + "version": "29.0.0", "files": { - "3ef2c8ebbbb128e6fbd2f26a8c80b8154d5fe5157a29846585cb36feac29318e": { + "b1025f887a56783d23c02c714067f4e119f3a3393c9db47c7ce05076e52e58bd": { "source": { "path": "integ-assets-docker.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "3ef2c8ebbbb128e6fbd2f26a8c80b8154d5fe5157a29846585cb36feac29318e.json", + "objectKey": "b1025f887a56783d23c02c714067f4e119f3a3393c9db47c7ce05076e52e58bd.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } @@ -55,6 +55,21 @@ "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-image-publishing-role-${AWS::AccountId}-${AWS::Region}" } } + }, + "60dea2e16e94d1977b92fe03fa7085fea446233f1fe499702b69593438baa59f": { + "source": { + "directory": "asset.60dea2e16e94d1977b92fe03fa7085fea446233f1fe499702b69593438baa59f", + "dockerBuildSecrets": { + "mysecret": "src=index.py" + } + }, + "destinations": { + "current_account-current_region": { + "repositoryName": "cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}", + "imageTag": "60dea2e16e94d1977b92fe03fa7085fea446233f1fe499702b69593438baa59f", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-image-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/integ-assets-docker.template.json b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/integ-assets-docker.template.json index 56e3637207a43..8c7c033450117 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/integ-assets-docker.template.json +++ b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/integ-assets-docker.template.json @@ -76,6 +76,11 @@ "Value": { "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:fa08370824fa0a7eab2c59a4f371fe7631019044d6c906b4268193120dc213b4" } + }, + "ImageUri5": { + "Value": { + "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:60dea2e16e94d1977b92fe03fa7085fea446233f1fe499702b69593438baa59f" + } } }, "Parameters": { diff --git a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/integ.json b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/integ.json index 4848cd7f244e2..f4aed5a9c37d0 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/integ.json +++ b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "24.0.0", + "version": "29.0.0", "testCases": { "integ.assets-docker": { "stacks": [ diff --git a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/manifest.json b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/manifest.json index 48be2750bd076..cd31c93490241 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "24.0.0", + "version": "29.0.0", "artifacts": { "integ-assets-docker.assets": { "type": "cdk:asset-manifest", @@ -17,7 +17,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/3ef2c8ebbbb128e6fbd2f26a8c80b8154d5fe5157a29846585cb36feac29318e.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/b1025f887a56783d23c02c714067f4e119f3a3393c9db47c7ce05076e52e58bd.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -69,6 +69,12 @@ "data": "ImageUri4" } ], + "/integ-assets-docker/ImageUri5": [ + { + "type": "aws:cdk:logicalId", + "data": "ImageUri5" + } + ], "/integ-assets-docker/BootstrapVersion": [ { "type": "aws:cdk:logicalId", diff --git a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/tree.json b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/tree.json index c013950bfa133..32988bdc52723 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/tree.json +++ b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.js.snapshot/tree.json @@ -112,6 +112,32 @@ "version": "0.0.0" } }, + "DockerImage5": { + "id": "DockerImage5", + "path": "integ-assets-docker/DockerImage5", + "children": { + "Staging": { + "id": "Staging", + "path": "integ-assets-docker/DockerImage5/Staging", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "Repository": { + "id": "Repository", + "path": "integ-assets-docker/DockerImage5/Repository", + "constructInfo": { + "fqn": "@aws-cdk/aws-ecr.RepositoryBase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ecr-assets.DockerImageAsset", + "version": "0.0.0" + } + }, "MyUser": { "id": "MyUser", "path": "integ-assets-docker/MyUser", @@ -236,6 +262,14 @@ "version": "0.0.0" } }, + "ImageUri5": { + "id": "ImageUri5", + "path": "integ-assets-docker/ImageUri5", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + }, "BootstrapVersion": { "id": "BootstrapVersion", "path": "integ-assets-docker/BootstrapVersion", @@ -263,7 +297,7 @@ "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.182" + "version": "10.1.216" } } }, diff --git a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.ts b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.ts index 1aa9fa392af0a..702b83fe011a5 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.ts +++ b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.ts @@ -24,15 +24,24 @@ const asset4 = new assets.DockerImageAsset(stack, 'DockerImage4', { outputs: ['type=docker'], }); +const asset5 = new assets.DockerImageAsset(stack, 'DockerImage5', { + directory: path.join(__dirname, 'demo-image-secret'), + buildSecrets: { + mysecret: cdk.DockerBuildSecret.fromSrc('index.py'), + }, +}); + const user = new iam.User(stack, 'MyUser'); asset.repository.grantPull(user); asset2.repository.grantPull(user); asset3.repository.grantPull(user); asset4.repository.grantPull(user); +asset5.repository.grantPull(user); new cdk.CfnOutput(stack, 'ImageUri', { value: asset.imageUri }); new cdk.CfnOutput(stack, 'ImageUri2', { value: asset2.imageUri }); new cdk.CfnOutput(stack, 'ImageUri3', { value: asset3.imageUri }); new cdk.CfnOutput(stack, 'ImageUri4', { value: asset4.imageUri }); +new cdk.CfnOutput(stack, 'ImageUri5', { value: asset5.imageUri }); app.synth(); diff --git a/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.js.snapshot/cdk.out b/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.js.snapshot/cdk.out index 588d7b269d34f..d8b441d447f8a 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.js.snapshot/cdk.out +++ b/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"20.0.0"} \ No newline at end of file +{"version":"29.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.js.snapshot/integ.json b/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.js.snapshot/integ.json index c1dff06736e53..7c8541b18231b 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.js.snapshot/integ.json +++ b/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.js.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "20.0.0", + "version": "29.0.0", "testCases": { "integ.nested-stacks-docker": { "stacks": [ diff --git a/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.js.snapshot/manifest.json b/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.js.snapshot/manifest.json index f318c19a2b7cf..a8cd150aeb96f 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.js.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.js.snapshot/manifest.json @@ -1,12 +1,6 @@ { - "version": "20.0.0", + "version": "29.0.0", "artifacts": { - "Tree": { - "type": "cdk:tree", - "properties": { - "file": "tree.json" - } - }, "nested-stacks-docker.assets": { "type": "cdk:asset-manifest", "properties": { @@ -77,6 +71,12 @@ ] }, "displayName": "nested-stacks-docker" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.js.snapshot/nested-stacks-docker.assets.json b/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.js.snapshot/nested-stacks-docker.assets.json index 8f3fdeddfade8..aa40b93d5962f 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.js.snapshot/nested-stacks-docker.assets.json +++ b/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.js.snapshot/nested-stacks-docker.assets.json @@ -1,5 +1,5 @@ { - "version": "20.0.0", + "version": "29.0.0", "files": { "eaf17d410c9c3958b50b406011121bab5f3147b4a61a0f93a9ab1db097033867": { "source": { diff --git a/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.js.snapshot/tree.json b/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.js.snapshot/tree.json index 9bbc4b8027746..0360789f62220 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.js.snapshot/tree.json +++ b/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.js.snapshot/tree.json @@ -4,14 +4,6 @@ "id": "App", "path": "", "children": { - "Tree": { - "id": "Tree", - "path": "Tree", - "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.1.85" - } - }, "nested-stacks-docker": { "id": "nested-stacks-docker", "path": "nested-stacks-docker", @@ -28,8 +20,8 @@ "id": "Staging", "path": "nested-stacks-docker/nested-stack-with-image/my-image/Staging", "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.1.85" + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" } }, "Repository": { @@ -142,14 +134,14 @@ "id": "output", "path": "nested-stacks-docker/nested-stack-with-image/output", "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.1.85" + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.1.85" + "fqn": "@aws-cdk/core.NestedStack", + "version": "0.0.0" } }, "nested-stack-with-image.NestedStack": { @@ -185,26 +177,50 @@ } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.1.85" + "fqn": "@aws-cdk/core.CfnStack", + "version": "0.0.0" } } }, "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.85" + "version": "10.1.216" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "nested-stacks-docker/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "nested-stacks-docker/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" } } }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.85" + "version": "10.1.216" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.1.85" + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index 8c9645856d00f..92fd09a911fcd 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -80,7 +80,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/lambda-layer-kubectl-v24": "^2.0.83", + "@aws-cdk/lambda-layer-kubectl-v24": "^2.0.85", "aws-cdk-lib": "2.47.0", "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", @@ -93,8 +93,8 @@ "@types/sinon": "^9.0.11", "@types/yaml": "1.9.6", "aws-sdk": "^2.1211.0", - "cdk8s": "^2.6.34", - "cdk8s-plus-24": "2.4.5", + "cdk8s": "^2.6.36", + "cdk8s-plus-24": "2.4.8", "jest": "^27.5.1", "sinon": "^9.2.4" }, diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts index 3666d078f4a7f..518e0865161c9 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts @@ -1,6 +1,6 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import { Arn, ArnFormat, Fn, Token } from '@aws-cdk/core'; import { ApplicationProtocol, Protocol } from './enums'; -import { Arn, ArnFormat } from '@aws-cdk/core'; export type Attributes = { [key: string]: string | undefined }; @@ -92,10 +92,21 @@ export function mapTagMapToCxschema(tagMap: Record): cxschema.Ta .map(([key, value]) => ({ key, value })); } -export function parseLoadBalancerFullName(loadBalancerArn: string): string { - const arnComponents = Arn.split(loadBalancerArn, ArnFormat.SLASH_RESOURCE_NAME); - if (!arnComponents.resourceName) { - throw new Error(`Provided ARN does not belong to a load balancer: ${loadBalancerArn}`); +export function parseLoadBalancerFullName(arn: string): string { + if (Token.isUnresolved(arn)) { + // Unfortunately it is not possible to use Arn.split() because the ARNs have this shape: + // + // arn:...:loadbalancer/net/my-load-balancer/123456 + // + // And the way that Arn.split() handles this situation is not enough to obtain the full name + const arnParts = Fn.split('/', arn); + return `${Fn.select(1, arnParts)}/${Fn.select(2, arnParts)}/${Fn.select(3, arnParts)}`; + } else { + const arnComponents = Arn.split(arn, ArnFormat.SLASH_RESOURCE_NAME); + const resourceName = arnComponents.resourceName; + if (!resourceName) { + throw new Error(`Provided ARN does not belong to a load balancer: ${arn}`); + } + return resourceName; } - return arnComponents.resourceName; } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-StackWithLb.assets.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-StackWithLb.assets.json index de8afd591772f..e830638335cf4 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-StackWithLb.assets.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-StackWithLb.assets.json @@ -1,17 +1,17 @@ { "version": "29.0.0", "files": { - "b2b2e615554259736dccb6ecc100edae2dc9d18e7d4b2103b6b7ebacebba8485": { + "e24b7b4b9bebbe70e470dbe2c83f8f69c8338d37b258b8ebec384b51fd61536d": { "source": { "path": "aws-cdk-elbv2-StackWithLb.template.json", "packaging": "file" }, "destinations": { - "123456-eu-west-1": { - "bucketName": "cdk-hnb659fds-assets-123456-eu-west-1", - "objectKey": "b2b2e615554259736dccb6ecc100edae2dc9d18e7d4b2103b6b7ebacebba8485.json", - "region": "eu-west-1", - "assumeRoleArn": "arn:${AWS::Partition}:iam::123456:role/cdk-hnb659fds-file-publishing-role-123456-eu-west-1" + "12345678-test-region": { + "bucketName": "cdk-hnb659fds-assets-12345678-test-region", + "objectKey": "e24b7b4b9bebbe70e470dbe2c83f8f69c8338d37b258b8ebec384b51fd61536d.json", + "region": "test-region", + "assumeRoleArn": "arn:${AWS::Partition}:iam::12345678:role/cdk-hnb659fds-file-publishing-role-12345678-test-region" } } } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-StackWithLb.template.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-StackWithLb.template.json index c2966a3dabc2c..4170f7e23915c 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-StackWithLb.template.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-StackWithLb.template.json @@ -21,7 +21,7 @@ "VpcId": { "Ref": "VPCB9E5F0B4" }, - "AvailabilityZone": "dummy1a", + "AvailabilityZone": "test-region-1a", "CidrBlock": "10.0.0.0/18", "MapPublicIpOnLaunch": true, "Tags": [ @@ -122,7 +122,7 @@ "VpcId": { "Ref": "VPCB9E5F0B4" }, - "AvailabilityZone": "dummy1b", + "AvailabilityZone": "test-region-1b", "CidrBlock": "10.0.64.0/18", "MapPublicIpOnLaunch": true, "Tags": [ @@ -223,7 +223,7 @@ "VpcId": { "Ref": "VPCB9E5F0B4" }, - "AvailabilityZone": "dummy1a", + "AvailabilityZone": "test-region-1a", "CidrBlock": "10.0.128.0/18", "MapPublicIpOnLaunch": false, "Tags": [ @@ -285,7 +285,7 @@ "VpcId": { "Ref": "VPCB9E5F0B4" }, - "AvailabilityZone": "dummy1b", + "AvailabilityZone": "test-region-1b", "CidrBlock": "10.0.192.0/18", "MapPublicIpOnLaunch": false, "Tags": [ @@ -382,12 +382,6 @@ "Ref": "VPCPublicSubnet2Subnet74179F39" } ], - "Tags": [ - { - "Key": "some", - "Value": "tag" - } - ], "Type": "network" }, "DependsOn": [ @@ -398,6 +392,24 @@ ] } }, + "Outputs": { + "NlbArn": { + "Value": { + "Ref": "LB8A12904C" + }, + "Export": { + "Name": "NlbArn" + } + }, + "ExportsOutputRefLB8A12904C1150D6A6": { + "Value": { + "Ref": "LB8A12904C" + }, + "Export": { + "Name": "aws-cdk-elbv2-StackWithLb:ExportsOutputRefLB8A12904C1150D6A6" + } + } + }, "Parameters": { "BootstrapVersion": { "Type": "AWS::SSM::Parameter::Value", diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-integ-StackUnderTest.assets.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-integ-StackUnderTest.assets.json index 7cbc4f901fc43..37afa2c575793 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-integ-StackUnderTest.assets.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-integ-StackUnderTest.assets.json @@ -1,17 +1,17 @@ { "version": "29.0.0", "files": { - "c90244dbab9ab3bd198b9233fcf42c068fad41afc3efb1b1a1b12d352b81970d": { + "e42d9c4a114328c16cb773781b2fee3cceeb499294ef3cb4f7a0aeafce947f13": { "source": { "path": "aws-cdk-elbv2-integ-StackUnderTest.template.json", "packaging": "file" }, "destinations": { - "123456-eu-west-1": { - "bucketName": "cdk-hnb659fds-assets-123456-eu-west-1", - "objectKey": "c90244dbab9ab3bd198b9233fcf42c068fad41afc3efb1b1a1b12d352b81970d.json", - "region": "eu-west-1", - "assumeRoleArn": "arn:${AWS::Partition}:iam::123456:role/cdk-hnb659fds-file-publishing-role-123456-eu-west-1" + "12345678-test-region": { + "bucketName": "cdk-hnb659fds-assets-12345678-test-region", + "objectKey": "e42d9c4a114328c16cb773781b2fee3cceeb499294ef3cb4f7a0aeafce947f13.json", + "region": "test-region", + "assumeRoleArn": "arn:${AWS::Partition}:iam::12345678:role/cdk-hnb659fds-file-publishing-role-12345678-test-region" } } } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-integ-StackUnderTest.template.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-integ-StackUnderTest.template.json index 9090201be1346..0356b00b65d81 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-integ-StackUnderTest.template.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/aws-cdk-elbv2-integ-StackUnderTest.template.json @@ -1,6 +1,6 @@ { "Resources": { - "NlbByAttributesAlarmFlowCountB9EE6965": { + "NlbByHardcodedArnAlarmFlowCount60A46641": { "Type": "AWS::CloudWatch::Alarm", "Properties": { "ComparisonOperator": "GreaterThanOrEqualToThreshold", @@ -17,6 +17,136 @@ "Statistic": "Average", "Threshold": 0 } + }, + "NlbByCfnOutputsFromAnotherStackOutsideCdkAlarmFlowCountD9A1D5AC": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "Dimensions": [ + { + "Name": "LoadBalancer", + "Value": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::ImportValue": "NlbArn" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 2, + { + "Fn::Split": [ + "/", + { + "Fn::ImportValue": "NlbArn" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + "/", + { + "Fn::ImportValue": "NlbArn" + } + ] + } + ] + } + ] + ] + } + } + ], + "MetricName": "ActiveFlowCount", + "Namespace": "AWS/NetworkELB", + "Period": 300, + "Statistic": "Average", + "Threshold": 0 + } + }, + "NlbByCfnOutputsFromAnotherStackWithinCdkAlarmFlowCountD865DB84": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "Dimensions": [ + { + "Name": "LoadBalancer", + "Value": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::ImportValue": "aws-cdk-elbv2-StackWithLb:ExportsOutputRefLB8A12904C1150D6A6" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 2, + { + "Fn::Split": [ + "/", + { + "Fn::ImportValue": "aws-cdk-elbv2-StackWithLb:ExportsOutputRefLB8A12904C1150D6A6" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + "/", + { + "Fn::ImportValue": "aws-cdk-elbv2-StackWithLb:ExportsOutputRefLB8A12904C1150D6A6" + } + ] + } + ] + } + ] + ] + } + } + ], + "MetricName": "ActiveFlowCount", + "Namespace": "AWS/NetworkELB", + "Period": 300, + "Statistic": "Average", + "Threshold": 0 + } } }, "Parameters": { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/integ.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/integ.json index 97d4c77fd964a..c87d22abd7b94 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/integ.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/integ.json @@ -1,4 +1,5 @@ { + "enableLookups": true, "version": "29.0.0", "testCases": { "elbv2-integ/DefaultTest": { @@ -8,8 +9,8 @@ }, "aws-cdk-elbv2-integ-StackUnderTest/aws-cdk-elbv2-integ-StackUnderTestTestCase": { "env": { - "account": "123456", - "region": "eu-west-1" + "account": "12345678", + "region": "test-region" }, "stacks": [ "aws-cdk-elbv2-integ-StackUnderTest" diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/manifest.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/manifest.json index e7748e9449349..ecb92960fabc0 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/manifest.json @@ -1,6 +1,209 @@ { "version": "29.0.0", "artifacts": { + "aws-cdk-elbv2-StackWithLb.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-cdk-elbv2-StackWithLb.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-cdk-elbv2-StackWithLb": { + "type": "aws:cloudformation:stack", + "environment": "aws://12345678/test-region", + "properties": { + "templateFile": "aws-cdk-elbv2-StackWithLb.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::12345678:role/cdk-hnb659fds-cfn-exec-role-12345678-test-region", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-12345678-test-region/e24b7b4b9bebbe70e470dbe2c83f8f69c8338d37b258b8ebec384b51fd61536d.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-cdk-elbv2-StackWithLb.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::12345678:role/cdk-hnb659fds-lookup-role-12345678-test-region", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "aws-cdk-elbv2-StackWithLb.assets" + ], + "metadata": { + "/aws-cdk-elbv2-StackWithLb/VPC/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCB9E5F0B4" + } + ], + "/aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet1/Subnet": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPublicSubnet1SubnetB4246D30" + } + ], + "/aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet1/RouteTable": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPublicSubnet1RouteTableFEE4B781" + } + ], + "/aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet1/RouteTableAssociation": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPublicSubnet1RouteTableAssociation0B0896DC" + } + ], + "/aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet1/DefaultRoute": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPublicSubnet1DefaultRoute91CEF279" + } + ], + "/aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet1/EIP": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPublicSubnet1EIP6AD938E8" + } + ], + "/aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet1/NATGateway": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPublicSubnet1NATGatewayE0556630" + } + ], + "/aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet2/Subnet": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPublicSubnet2Subnet74179F39" + } + ], + "/aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet2/RouteTable": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPublicSubnet2RouteTable6F1A15F1" + } + ], + "/aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet2/RouteTableAssociation": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPublicSubnet2RouteTableAssociation5A808732" + } + ], + "/aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet2/DefaultRoute": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPublicSubnet2DefaultRouteB7481BBA" + } + ], + "/aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet2/EIP": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPublicSubnet2EIP4947BC00" + } + ], + "/aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet2/NATGateway": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPublicSubnet2NATGateway3C070193" + } + ], + "/aws-cdk-elbv2-StackWithLb/VPC/PrivateSubnet1/Subnet": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPrivateSubnet1Subnet8BCA10E0" + } + ], + "/aws-cdk-elbv2-StackWithLb/VPC/PrivateSubnet1/RouteTable": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPrivateSubnet1RouteTableBE8A6027" + } + ], + "/aws-cdk-elbv2-StackWithLb/VPC/PrivateSubnet1/RouteTableAssociation": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPrivateSubnet1RouteTableAssociation347902D1" + } + ], + "/aws-cdk-elbv2-StackWithLb/VPC/PrivateSubnet1/DefaultRoute": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPrivateSubnet1DefaultRouteAE1D6490" + } + ], + "/aws-cdk-elbv2-StackWithLb/VPC/PrivateSubnet2/Subnet": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + ], + "/aws-cdk-elbv2-StackWithLb/VPC/PrivateSubnet2/RouteTable": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPrivateSubnet2RouteTable0A19E10E" + } + ], + "/aws-cdk-elbv2-StackWithLb/VPC/PrivateSubnet2/RouteTableAssociation": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPrivateSubnet2RouteTableAssociation0C73D413" + } + ], + "/aws-cdk-elbv2-StackWithLb/VPC/PrivateSubnet2/DefaultRoute": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPrivateSubnet2DefaultRouteF4F5CFD2" + } + ], + "/aws-cdk-elbv2-StackWithLb/VPC/IGW": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCIGWB7E252D3" + } + ], + "/aws-cdk-elbv2-StackWithLb/VPC/VPCGW": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCVPCGW99B986DC" + } + ], + "/aws-cdk-elbv2-StackWithLb/LB/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "LB8A12904C" + } + ], + "/aws-cdk-elbv2-StackWithLb/NlbArn": [ + { + "type": "aws:cdk:logicalId", + "data": "NlbArn" + } + ], + "/aws-cdk-elbv2-StackWithLb/Exports/Output{\"Ref\":\"LB8A12904C\"}": [ + { + "type": "aws:cdk:logicalId", + "data": "ExportsOutputRefLB8A12904C1150D6A6" + } + ], + "/aws-cdk-elbv2-StackWithLb/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/aws-cdk-elbv2-StackWithLb/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "aws-cdk-elbv2-StackWithLb" + }, "awscdkelbv2integStackUnderTestDeployAssert483BFB1A.assets": { "type": "cdk:asset-manifest", "properties": { @@ -105,32 +308,45 @@ }, "aws-cdk-elbv2-integ-StackUnderTest": { "type": "aws:cloudformation:stack", - "environment": "aws://123456/eu-west-1", + "environment": "aws://12345678/test-region", "properties": { "templateFile": "aws-cdk-elbv2-integ-StackUnderTest.template.json", "validateOnSynth": false, - "assumeRoleArn": "arn:${AWS::Partition}:iam::123456:role/cdk-hnb659fds-deploy-role-123456-eu-west-1", - "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::123456:role/cdk-hnb659fds-cfn-exec-role-123456-eu-west-1", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-123456-eu-west-1/c90244dbab9ab3bd198b9233fcf42c068fad41afc3efb1b1a1b12d352b81970d.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::12345678:role/cdk-hnb659fds-cfn-exec-role-12345678-test-region", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-12345678-test-region/e42d9c4a114328c16cb773781b2fee3cceeb499294ef3cb4f7a0aeafce947f13.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ "aws-cdk-elbv2-integ-StackUnderTest.assets" ], "lookupRole": { - "arn": "arn:${AWS::Partition}:iam::123456:role/cdk-hnb659fds-lookup-role-123456-eu-west-1", + "arn": "arn:${AWS::Partition}:iam::12345678:role/cdk-hnb659fds-lookup-role-12345678-test-region", "requiresBootstrapStackVersion": 8, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" } }, "dependencies": [ + "aws-cdk-elbv2-StackWithLb", "aws-cdk-elbv2-integ-StackUnderTest.assets" ], "metadata": { - "/aws-cdk-elbv2-integ-StackUnderTest/NlbByAttributes_AlarmFlowCount/Resource": [ + "/aws-cdk-elbv2-integ-StackUnderTest/NlbByHardcodedArn_AlarmFlowCount/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "NlbByHardcodedArnAlarmFlowCount60A46641" + } + ], + "/aws-cdk-elbv2-integ-StackUnderTest/NlbByCfnOutputsFromAnotherStackOutsideCdk_AlarmFlowCount/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "NlbByCfnOutputsFromAnotherStackOutsideCdkAlarmFlowCountD9A1D5AC" + } + ], + "/aws-cdk-elbv2-integ-StackUnderTest/NlbByCfnOutputsFromAnotherStackWithinCdk_AlarmFlowCount/Resource": [ { "type": "aws:cdk:logicalId", - "data": "NlbByAttributesAlarmFlowCountB9EE6965" + "data": "NlbByCfnOutputsFromAnotherStackWithinCdkAlarmFlowCountD865DB84" } ], "/aws-cdk-elbv2-integ-StackUnderTest/BootstrapVersion": [ diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/tree.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/tree.json index 040b056ab3ada..16876b01d13bb 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/tree.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.js.snapshot/tree.json @@ -4,6 +4,713 @@ "id": "App", "path": "", "children": { + "aws-cdk-elbv2-StackWithLb": { + "id": "aws-cdk-elbv2-StackWithLb", + "path": "aws-cdk-elbv2-StackWithLb", + "children": { + "VPC": { + "id": "VPC", + "path": "aws-cdk-elbv2-StackWithLb/VPC", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-elbv2-StackWithLb/VPC/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::VPC", + "aws:cdk:cloudformation:props": { + "cidrBlock": "10.0.0.0/16", + "enableDnsHostnames": true, + "enableDnsSupport": true, + "instanceTenancy": "default", + "tags": [ + { + "key": "Name", + "value": "my-vpc-name" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnVPC", + "version": "0.0.0" + } + }, + "PublicSubnet1": { + "id": "PublicSubnet1", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet1", + "children": { + "Subnet": { + "id": "Subnet", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet1/Subnet", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::Subnet", + "aws:cdk:cloudformation:props": { + "vpcId": { + "Ref": "VPCB9E5F0B4" + }, + "availabilityZone": "test-region-1a", + "cidrBlock": "10.0.0.0/18", + "mapPublicIpOnLaunch": true, + "tags": [ + { + "key": "aws-cdk:subnet-name", + "value": "Public" + }, + { + "key": "aws-cdk:subnet-type", + "value": "Public" + }, + { + "key": "Name", + "value": "aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet1" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnSubnet", + "version": "0.0.0" + } + }, + "Acl": { + "id": "Acl", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet1/Acl", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "RouteTable": { + "id": "RouteTable", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet1/RouteTable", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::RouteTable", + "aws:cdk:cloudformation:props": { + "vpcId": { + "Ref": "VPCB9E5F0B4" + }, + "tags": [ + { + "key": "Name", + "value": "aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet1" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnRouteTable", + "version": "0.0.0" + } + }, + "RouteTableAssociation": { + "id": "RouteTableAssociation", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet1/RouteTableAssociation", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::SubnetRouteTableAssociation", + "aws:cdk:cloudformation:props": { + "routeTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "subnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnSubnetRouteTableAssociation", + "version": "0.0.0" + } + }, + "DefaultRoute": { + "id": "DefaultRoute", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet1/DefaultRoute", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::Route", + "aws:cdk:cloudformation:props": { + "routeTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "destinationCidrBlock": "0.0.0.0/0", + "gatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnRoute", + "version": "0.0.0" + } + }, + "EIP": { + "id": "EIP", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet1/EIP", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::EIP", + "aws:cdk:cloudformation:props": { + "domain": "vpc", + "tags": [ + { + "key": "Name", + "value": "aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet1" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnEIP", + "version": "0.0.0" + } + }, + "NATGateway": { + "id": "NATGateway", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet1/NATGateway", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::NatGateway", + "aws:cdk:cloudformation:props": { + "subnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "allocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "tags": [ + { + "key": "Name", + "value": "aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet1" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnNatGateway", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.PublicSubnet", + "version": "0.0.0" + } + }, + "PublicSubnet2": { + "id": "PublicSubnet2", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet2", + "children": { + "Subnet": { + "id": "Subnet", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet2/Subnet", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::Subnet", + "aws:cdk:cloudformation:props": { + "vpcId": { + "Ref": "VPCB9E5F0B4" + }, + "availabilityZone": "test-region-1b", + "cidrBlock": "10.0.64.0/18", + "mapPublicIpOnLaunch": true, + "tags": [ + { + "key": "aws-cdk:subnet-name", + "value": "Public" + }, + { + "key": "aws-cdk:subnet-type", + "value": "Public" + }, + { + "key": "Name", + "value": "aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet2" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnSubnet", + "version": "0.0.0" + } + }, + "Acl": { + "id": "Acl", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet2/Acl", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "RouteTable": { + "id": "RouteTable", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet2/RouteTable", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::RouteTable", + "aws:cdk:cloudformation:props": { + "vpcId": { + "Ref": "VPCB9E5F0B4" + }, + "tags": [ + { + "key": "Name", + "value": "aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet2" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnRouteTable", + "version": "0.0.0" + } + }, + "RouteTableAssociation": { + "id": "RouteTableAssociation", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet2/RouteTableAssociation", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::SubnetRouteTableAssociation", + "aws:cdk:cloudformation:props": { + "routeTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "subnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnSubnetRouteTableAssociation", + "version": "0.0.0" + } + }, + "DefaultRoute": { + "id": "DefaultRoute", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet2/DefaultRoute", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::Route", + "aws:cdk:cloudformation:props": { + "routeTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "destinationCidrBlock": "0.0.0.0/0", + "gatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnRoute", + "version": "0.0.0" + } + }, + "EIP": { + "id": "EIP", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet2/EIP", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::EIP", + "aws:cdk:cloudformation:props": { + "domain": "vpc", + "tags": [ + { + "key": "Name", + "value": "aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet2" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnEIP", + "version": "0.0.0" + } + }, + "NATGateway": { + "id": "NATGateway", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet2/NATGateway", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::NatGateway", + "aws:cdk:cloudformation:props": { + "subnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + "allocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "tags": [ + { + "key": "Name", + "value": "aws-cdk-elbv2-StackWithLb/VPC/PublicSubnet2" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnNatGateway", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.PublicSubnet", + "version": "0.0.0" + } + }, + "PrivateSubnet1": { + "id": "PrivateSubnet1", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PrivateSubnet1", + "children": { + "Subnet": { + "id": "Subnet", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PrivateSubnet1/Subnet", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::Subnet", + "aws:cdk:cloudformation:props": { + "vpcId": { + "Ref": "VPCB9E5F0B4" + }, + "availabilityZone": "test-region-1a", + "cidrBlock": "10.0.128.0/18", + "mapPublicIpOnLaunch": false, + "tags": [ + { + "key": "aws-cdk:subnet-name", + "value": "Private" + }, + { + "key": "aws-cdk:subnet-type", + "value": "Private" + }, + { + "key": "Name", + "value": "aws-cdk-elbv2-StackWithLb/VPC/PrivateSubnet1" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnSubnet", + "version": "0.0.0" + } + }, + "Acl": { + "id": "Acl", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PrivateSubnet1/Acl", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "RouteTable": { + "id": "RouteTable", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PrivateSubnet1/RouteTable", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::RouteTable", + "aws:cdk:cloudformation:props": { + "vpcId": { + "Ref": "VPCB9E5F0B4" + }, + "tags": [ + { + "key": "Name", + "value": "aws-cdk-elbv2-StackWithLb/VPC/PrivateSubnet1" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnRouteTable", + "version": "0.0.0" + } + }, + "RouteTableAssociation": { + "id": "RouteTableAssociation", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PrivateSubnet1/RouteTableAssociation", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::SubnetRouteTableAssociation", + "aws:cdk:cloudformation:props": { + "routeTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "subnetId": { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnSubnetRouteTableAssociation", + "version": "0.0.0" + } + }, + "DefaultRoute": { + "id": "DefaultRoute", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PrivateSubnet1/DefaultRoute", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::Route", + "aws:cdk:cloudformation:props": { + "routeTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "destinationCidrBlock": "0.0.0.0/0", + "natGatewayId": { + "Ref": "VPCPublicSubnet1NATGatewayE0556630" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnRoute", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.PrivateSubnet", + "version": "0.0.0" + } + }, + "PrivateSubnet2": { + "id": "PrivateSubnet2", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PrivateSubnet2", + "children": { + "Subnet": { + "id": "Subnet", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PrivateSubnet2/Subnet", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::Subnet", + "aws:cdk:cloudformation:props": { + "vpcId": { + "Ref": "VPCB9E5F0B4" + }, + "availabilityZone": "test-region-1b", + "cidrBlock": "10.0.192.0/18", + "mapPublicIpOnLaunch": false, + "tags": [ + { + "key": "aws-cdk:subnet-name", + "value": "Private" + }, + { + "key": "aws-cdk:subnet-type", + "value": "Private" + }, + { + "key": "Name", + "value": "aws-cdk-elbv2-StackWithLb/VPC/PrivateSubnet2" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnSubnet", + "version": "0.0.0" + } + }, + "Acl": { + "id": "Acl", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PrivateSubnet2/Acl", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "RouteTable": { + "id": "RouteTable", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PrivateSubnet2/RouteTable", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::RouteTable", + "aws:cdk:cloudformation:props": { + "vpcId": { + "Ref": "VPCB9E5F0B4" + }, + "tags": [ + { + "key": "Name", + "value": "aws-cdk-elbv2-StackWithLb/VPC/PrivateSubnet2" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnRouteTable", + "version": "0.0.0" + } + }, + "RouteTableAssociation": { + "id": "RouteTableAssociation", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PrivateSubnet2/RouteTableAssociation", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::SubnetRouteTableAssociation", + "aws:cdk:cloudformation:props": { + "routeTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "subnetId": { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnSubnetRouteTableAssociation", + "version": "0.0.0" + } + }, + "DefaultRoute": { + "id": "DefaultRoute", + "path": "aws-cdk-elbv2-StackWithLb/VPC/PrivateSubnet2/DefaultRoute", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::Route", + "aws:cdk:cloudformation:props": { + "routeTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "destinationCidrBlock": "0.0.0.0/0", + "natGatewayId": { + "Ref": "VPCPublicSubnet2NATGateway3C070193" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnRoute", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.PrivateSubnet", + "version": "0.0.0" + } + }, + "IGW": { + "id": "IGW", + "path": "aws-cdk-elbv2-StackWithLb/VPC/IGW", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::InternetGateway", + "aws:cdk:cloudformation:props": { + "tags": [ + { + "key": "Name", + "value": "my-vpc-name" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnInternetGateway", + "version": "0.0.0" + } + }, + "VPCGW": { + "id": "VPCGW", + "path": "aws-cdk-elbv2-StackWithLb/VPC/VPCGW", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::VPCGatewayAttachment", + "aws:cdk:cloudformation:props": { + "vpcId": { + "Ref": "VPCB9E5F0B4" + }, + "internetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnVPCGatewayAttachment", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.Vpc", + "version": "0.0.0" + } + }, + "LB": { + "id": "LB", + "path": "aws-cdk-elbv2-StackWithLb/LB", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-elbv2-StackWithLb/LB/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + "aws:cdk:cloudformation:props": { + "loadBalancerAttributes": [ + { + "key": "deletion_protection.enabled", + "value": "false" + } + ], + "name": "my-load-balancer", + "scheme": "internet-facing", + "subnets": [ + { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + ], + "type": "network" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-elasticloadbalancingv2.CfnLoadBalancer", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-elasticloadbalancingv2.NetworkLoadBalancer", + "version": "0.0.0" + } + }, + "NlbArn": { + "id": "NlbArn", + "path": "aws-cdk-elbv2-StackWithLb/NlbArn", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + }, + "Exports": { + "id": "Exports", + "path": "aws-cdk-elbv2-StackWithLb/Exports", + "children": { + "Output{\"Ref\":\"LB8A12904C\"}": { + "id": "Output{\"Ref\":\"LB8A12904C\"}", + "path": "aws-cdk-elbv2-StackWithLb/Exports/Output{\"Ref\":\"LB8A12904C\"}", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.237" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "aws-cdk-elbv2-StackWithLb/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "aws-cdk-elbv2-StackWithLb/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, "aws-cdk-elbv2-integ-StackUnderTest": { "id": "aws-cdk-elbv2-integ-StackUnderTest", "path": "aws-cdk-elbv2-integ-StackUnderTest", @@ -13,7 +720,7 @@ "path": "aws-cdk-elbv2-integ-StackUnderTest/Default", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.216" + "version": "10.1.237" } }, "DeployAssert": { @@ -51,7 +758,7 @@ "path": "aws-cdk-elbv2-integ-StackUnderTest/aws-cdk-elbv2-integ-StackUnderTestTestCase/Default", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.216" + "version": "10.1.237" } }, "DeployAssert": { @@ -86,21 +793,21 @@ "version": "0.0.0" } }, - "NlbByAttributes": { - "id": "NlbByAttributes", - "path": "aws-cdk-elbv2-integ-StackUnderTest/NlbByAttributes", + "NlbByHardcodedArn": { + "id": "NlbByHardcodedArn", + "path": "aws-cdk-elbv2-integ-StackUnderTest/NlbByHardcodedArn", "constructInfo": { "fqn": "@aws-cdk/core.Resource", "version": "0.0.0" } }, - "NlbByAttributes_AlarmFlowCount": { - "id": "NlbByAttributes_AlarmFlowCount", - "path": "aws-cdk-elbv2-integ-StackUnderTest/NlbByAttributes_AlarmFlowCount", + "NlbByHardcodedArn_AlarmFlowCount": { + "id": "NlbByHardcodedArn_AlarmFlowCount", + "path": "aws-cdk-elbv2-integ-StackUnderTest/NlbByHardcodedArn_AlarmFlowCount", "children": { "Resource": { "id": "Resource", - "path": "aws-cdk-elbv2-integ-StackUnderTest/NlbByAttributes_AlarmFlowCount/Resource", + "path": "aws-cdk-elbv2-integ-StackUnderTest/NlbByHardcodedArn_AlarmFlowCount/Resource", "attributes": { "aws:cdk:cloudformation:type": "AWS::CloudWatch::Alarm", "aws:cdk:cloudformation:props": { @@ -130,6 +837,188 @@ "version": "0.0.0" } }, + "NlbByCfnOutputsFromAnotherStackOutsideCdk": { + "id": "NlbByCfnOutputsFromAnotherStackOutsideCdk", + "path": "aws-cdk-elbv2-integ-StackUnderTest/NlbByCfnOutputsFromAnotherStackOutsideCdk", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "NlbByCfnOutputsFromAnotherStackOutsideCdk_AlarmFlowCount": { + "id": "NlbByCfnOutputsFromAnotherStackOutsideCdk_AlarmFlowCount", + "path": "aws-cdk-elbv2-integ-StackUnderTest/NlbByCfnOutputsFromAnotherStackOutsideCdk_AlarmFlowCount", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-elbv2-integ-StackUnderTest/NlbByCfnOutputsFromAnotherStackOutsideCdk_AlarmFlowCount/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudWatch::Alarm", + "aws:cdk:cloudformation:props": { + "comparisonOperator": "GreaterThanOrEqualToThreshold", + "evaluationPeriods": 1, + "dimensions": [ + { + "name": "LoadBalancer", + "value": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::ImportValue": "NlbArn" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 2, + { + "Fn::Split": [ + "/", + { + "Fn::ImportValue": "NlbArn" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + "/", + { + "Fn::ImportValue": "NlbArn" + } + ] + } + ] + } + ] + ] + } + } + ], + "metricName": "ActiveFlowCount", + "namespace": "AWS/NetworkELB", + "period": 300, + "statistic": "Average", + "threshold": 0 + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.CfnAlarm", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.Alarm", + "version": "0.0.0" + } + }, + "NlbByCfnOutputsFromAnotherStackWithinCdk": { + "id": "NlbByCfnOutputsFromAnotherStackWithinCdk", + "path": "aws-cdk-elbv2-integ-StackUnderTest/NlbByCfnOutputsFromAnotherStackWithinCdk", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "NlbByCfnOutputsFromAnotherStackWithinCdk_AlarmFlowCount": { + "id": "NlbByCfnOutputsFromAnotherStackWithinCdk_AlarmFlowCount", + "path": "aws-cdk-elbv2-integ-StackUnderTest/NlbByCfnOutputsFromAnotherStackWithinCdk_AlarmFlowCount", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-elbv2-integ-StackUnderTest/NlbByCfnOutputsFromAnotherStackWithinCdk_AlarmFlowCount/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudWatch::Alarm", + "aws:cdk:cloudformation:props": { + "comparisonOperator": "GreaterThanOrEqualToThreshold", + "evaluationPeriods": 1, + "dimensions": [ + { + "name": "LoadBalancer", + "value": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::ImportValue": "aws-cdk-elbv2-StackWithLb:ExportsOutputRefLB8A12904C1150D6A6" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 2, + { + "Fn::Split": [ + "/", + { + "Fn::ImportValue": "aws-cdk-elbv2-StackWithLb:ExportsOutputRefLB8A12904C1150D6A6" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + "/", + { + "Fn::ImportValue": "aws-cdk-elbv2-StackWithLb:ExportsOutputRefLB8A12904C1150D6A6" + } + ] + } + ] + } + ] + ] + } + } + ], + "metricName": "ActiveFlowCount", + "namespace": "AWS/NetworkELB", + "period": 300, + "statistic": "Average", + "threshold": 0 + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.CfnAlarm", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.Alarm", + "version": "0.0.0" + } + }, "BootstrapVersion": { "id": "BootstrapVersion", "path": "aws-cdk-elbv2-integ-StackUnderTest/BootstrapVersion", @@ -165,7 +1054,7 @@ "path": "elbv2-integ/DefaultTest/Default", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.216" + "version": "10.1.237" } }, "DeployAssert": { @@ -211,7 +1100,7 @@ "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.216" + "version": "10.1.237" } } }, diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.ts index 820416949836c..ef5a0f7125ada 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb-lookup.ts @@ -1,11 +1,11 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import * as integ from '@aws-cdk/integ-tests'; -import * as elbv2 from '../lib'; import { IntegTestCaseStack } from '@aws-cdk/integ-tests'; +import * as elbv2 from '../lib'; -const appWithLb = new cdk.App(); -const stackWithLb = new cdk.Stack(appWithLb, 'aws-cdk-elbv2-StackWithLb', { +const app = new cdk.App(); +const stackWithLb = new cdk.Stack(app, 'aws-cdk-elbv2-StackWithLb', { env: { account: process.env.CDK_INTEG_ACCOUNT ?? process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_INTEG_REGION ?? process.env.CDK_DEFAULT_REGION, @@ -22,29 +22,45 @@ const lb = new elbv2.NetworkLoadBalancer(stackWithLb, 'LB', { internetFacing: true, loadBalancerName: 'my-load-balancer', }); -cdk.Tags.of(lb).add('some', 'tag'); -const lbArn = 'arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/network/my-load-balancer/50dc6c495c0c9188'; +new cdk.CfnOutput(stackWithLb, 'NlbArn', { + value: lb.loadBalancerArn, + exportName: 'NlbArn', +}); -const appUnderTest = new cdk.App(); -const stackLookup = new IntegTestCaseStack(appUnderTest, 'aws-cdk-elbv2-integ-StackUnderTest', { +const stackLookup = new IntegTestCaseStack(app, 'aws-cdk-elbv2-integ-StackUnderTest', { env: { account: process.env.CDK_INTEG_ACCOUNT ?? process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_INTEG_REGION ?? process.env.CDK_DEFAULT_REGION, }, }); -const lbByAttributes = elbv2.NetworkLoadBalancer.fromNetworkLoadBalancerAttributes(stackLookup, 'NlbByAttributes', { - loadBalancerArn: lbArn, +const lbByHardcodedArn = elbv2.NetworkLoadBalancer.fromNetworkLoadBalancerAttributes(stackLookup, 'NlbByHardcodedArn', { + loadBalancerArn: 'arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/network/my-load-balancer/50dc6c495c0c9188', +}); +lbByHardcodedArn.metrics.activeFlowCount().createAlarm(stackLookup, 'NlbByHardcodedArn_AlarmFlowCount', { + evaluationPeriods: 1, + threshold: 0, +}); + +const lbByCfnOutputsFromAnotherStackOutsideCdk = elbv2.NetworkLoadBalancer.fromNetworkLoadBalancerAttributes(stackLookup, 'NlbByCfnOutputsFromAnotherStackOutsideCdk', { + loadBalancerArn: cdk.Fn.importValue('NlbArn'), +}); +lbByCfnOutputsFromAnotherStackOutsideCdk.metrics.activeFlowCount().createAlarm(stackLookup, 'NlbByCfnOutputsFromAnotherStackOutsideCdk_AlarmFlowCount', { + evaluationPeriods: 1, + threshold: 0, }); -lbByAttributes.metrics.activeFlowCount().createAlarm(stackLookup, 'NlbByAttributes_AlarmFlowCount', { +const lbByCfnOutputsFromAnotherStackWithinCdk = elbv2.NetworkLoadBalancer.fromNetworkLoadBalancerAttributes(stackLookup, 'NlbByCfnOutputsFromAnotherStackWithinCdk', { + loadBalancerArn: lb.loadBalancerArn, +}); +lbByCfnOutputsFromAnotherStackWithinCdk.metrics.activeFlowCount().createAlarm(stackLookup, 'NlbByCfnOutputsFromAnotherStackWithinCdk_AlarmFlowCount', { evaluationPeriods: 1, threshold: 0, }); -new integ.IntegTest(appUnderTest, 'elbv2-integ', { +new integ.IntegTest(app, 'elbv2-integ', { testCases: [stackLookup], + enableLookups: true, }); -appWithLb.synth(); -appUnderTest.synth(); +app.synth(); diff --git a/packages/@aws-cdk/aws-glue/README.md b/packages/@aws-cdk/aws-glue/README.md index d5260e12d771d..83cb4a1c82434 100644 --- a/packages/@aws-cdk/aws-glue/README.md +++ b/packages/@aws-cdk/aws-glue/README.md @@ -90,6 +90,23 @@ new glue.Job(this, 'PythonShellJob', { }); ``` +### Ray Jobs + +These jobs run in a Ray environment managed by AWS Glue. + +```ts +new glue.Job(this, 'RayJob', { + executable: glue.JobExecutable.pythonRay({ + glueVersion: glue.GlueVersion.V4_0, + pythonVersion: glue.PythonVersion.THREE_NINE, + script: glue.Code.fromAsset(path.join(__dirname, 'job-script/hello_world.py')), + }), + workerType: glue.WorkerType.Z_2X, + workerCount: 2, + description: 'an example Ray job' +}); +``` + See [documentation](https://docs.aws.amazon.com/glue/latest/dg/add-job.html) for more information on adding jobs in Glue. ## Connection diff --git a/packages/@aws-cdk/aws-glue/lib/job-executable.ts b/packages/@aws-cdk/aws-glue/lib/job-executable.ts index 092cdca11552d..8ad29c1108b70 100644 --- a/packages/@aws-cdk/aws-glue/lib/job-executable.ts +++ b/packages/@aws-cdk/aws-glue/lib/job-executable.ts @@ -95,12 +95,12 @@ export enum PythonVersion { */ export class JobType { /** - * Command for running a Glue ETL job. + * Command for running a Glue Spark job. */ public static readonly ETL = new JobType('glueetl'); /** - * Command for running a Glue streaming job. + * Command for running a Glue Spark streaming job. */ public static readonly STREAMING = new JobType('gluestreaming'); @@ -109,6 +109,11 @@ export class JobType { */ public static readonly PYTHON_SHELL = new JobType('pythonshell'); + /** + * Command for running a Glue Ray job. + */ + public static readonly RAY = new JobType('glueray'); + /** * Custom type name * @param name type name @@ -211,6 +216,11 @@ export interface PythonSparkJobExecutableProps extends SharedSparkJobExecutableP */ export interface PythonShellExecutableProps extends SharedJobExecutableProps, PythonExecutableProps {} +/** + * Props for creating a Python Ray job executable + */ +export interface PythonRayExecutableProps extends SharedJobExecutableProps, PythonExecutableProps {} + /** * The executable properties related to the Glue job's GlueVersion, JobType and code */ @@ -281,6 +291,19 @@ export class JobExecutable { }); } + /** + * Create Python executable props for Ray jobs. + * + * @param props Ray Job props. + */ + public static pythonRay(props: PythonRayExecutableProps): JobExecutable { + return new JobExecutable({ + ...props, + type: JobType.RAY, + language: JobLanguage.PYTHON, + }); + } + /** * Create a custom JobExecutable. * @@ -297,10 +320,18 @@ export class JobExecutable { if (config.language !== JobLanguage.PYTHON) { throw new Error('Python shell requires the language to be set to Python'); } - if ([GlueVersion.V0_9, GlueVersion.V2_0, GlueVersion.V3_0, GlueVersion.V4_0].includes(config.glueVersion)) { + if ([GlueVersion.V0_9, GlueVersion.V3_0, GlueVersion.V4_0].includes(config.glueVersion)) { throw new Error(`Specified GlueVersion ${config.glueVersion.name} does not support Python Shell`); } } + if (JobType.RAY === config.type) { + if (config.language !== JobLanguage.PYTHON) { + throw new Error('Ray requires the language to be set to Python'); + } + if ([GlueVersion.V0_9, GlueVersion.V1_0, GlueVersion.V2_0, GlueVersion.V3_0].includes(config.glueVersion)) { + throw new Error(`Specified GlueVersion ${config.glueVersion.name} does not support Ray`); + } + } if (config.extraJarsFirst && [GlueVersion.V0_9, GlueVersion.V1_0].includes(config.glueVersion)) { throw new Error(`Specified GlueVersion ${config.glueVersion.name} does not support extraJarsFirst`); } @@ -310,8 +341,11 @@ export class JobExecutable { if (JobLanguage.PYTHON !== config.language && config.extraPythonFiles) { throw new Error('extraPythonFiles is not supported for languages other than JobLanguage.PYTHON'); } - if (config.pythonVersion === PythonVersion.THREE_NINE && config.type !== JobType.PYTHON_SHELL) { - throw new Error('Specified PythonVersion PythonVersion.THREE_NINE is only supported for JobType Python Shell'); + if (config.pythonVersion === PythonVersion.THREE_NINE && config.type !== JobType.PYTHON_SHELL && config.type !== JobType.RAY) { + throw new Error('Specified PythonVersion PythonVersion.THREE_NINE is only supported for JobType Python Shell and Ray'); + } + if (config.pythonVersion === PythonVersion.THREE && config.type === JobType.RAY) { + throw new Error('Specified PythonVersion PythonVersion.THREE is not supported for Ray'); } this.config = config; } diff --git a/packages/@aws-cdk/aws-glue/lib/job.ts b/packages/@aws-cdk/aws-glue/lib/job.ts index eebb8b1acbdb0..95c208bdc930f 100644 --- a/packages/@aws-cdk/aws-glue/lib/job.ts +++ b/packages/@aws-cdk/aws-glue/lib/job.ts @@ -32,6 +32,16 @@ export class WorkerType { */ public static readonly G_2X = new WorkerType('G.2X'); + /** + * Each worker maps to 0.25 DPU (2 vCPU, 4 GB of memory, 64 GB disk), and provides 1 executor per worker. Suitable for low volume streaming jobs. + */ + public static readonly G_025X = new WorkerType('G.025X'); + + /** + * Each worker maps to 2 high-memory DPU [M-DPU] (8 vCPU, 64 GB of memory, 128 GB disk). Supported in Ray jobs. + */ + public static readonly Z_2X = new WorkerType('Z.2X'); + /** * Custom worker type * @param workerType custom worker type @@ -726,6 +736,8 @@ export class Job extends JobBase { private setupSparkUI(executable: JobExecutableConfig, role: iam.IRole, props: SparkUIProps) { if (JobType.PYTHON_SHELL === executable.type) { throw new Error('Spark UI is not available for JobType.PYTHON_SHELL jobs'); + } else if (JobType.RAY === executable.type) { + throw new Error('Spark UI is not available for JobType.RAY jobs'); } const bucket = props.bucket ?? new s3.Bucket(this, 'SparkUIBucket'); diff --git a/packages/@aws-cdk/aws-glue/test/integ.job.js.snapshot/aws-glue-job.assets.json b/packages/@aws-cdk/aws-glue/test/integ.job.js.snapshot/aws-glue-job.assets.json index 8e739ca15edca..5519b93f322d2 100644 --- a/packages/@aws-cdk/aws-glue/test/integ.job.js.snapshot/aws-glue-job.assets.json +++ b/packages/@aws-cdk/aws-glue/test/integ.job.js.snapshot/aws-glue-job.assets.json @@ -14,7 +14,7 @@ } } }, - "977a2f07e22679bb04b03ce83cc1fac3e6cc269a794e38248ec67106ee39f0a2": { + "b553fef631f82898c826f3c20e1de0d155dbd3a35339ef92d0893052a5be69ce": { "source": { "path": "aws-glue-job.template.json", "packaging": "file" @@ -22,7 +22,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "977a2f07e22679bb04b03ce83cc1fac3e6cc269a794e38248ec67106ee39f0a2.json", + "objectKey": "b553fef631f82898c826f3c20e1de0d155dbd3a35339ef92d0893052a5be69ce.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk/aws-glue/test/integ.job.js.snapshot/aws-glue-job.template.json b/packages/@aws-cdk/aws-glue/test/integ.job.js.snapshot/aws-glue-job.template.json index 47f34d95c01f7..06115404e2a3c 100644 --- a/packages/@aws-cdk/aws-glue/test/integ.job.js.snapshot/aws-glue-job.template.json +++ b/packages/@aws-cdk/aws-glue/test/integ.job.js.snapshot/aws-glue-job.template.json @@ -350,9 +350,11 @@ }, "GlueVersion": "2.0", "Name": "StreamingJob2.0", + "NumberOfWorkers": 10, "Tags": { "key": "value" - } + }, + "WorkerType": "G.025X" } }, "EtlJob30ServiceRole8E675579": { @@ -705,9 +707,11 @@ }, "GlueVersion": "3.0", "Name": "StreamingJob3.0", + "NumberOfWorkers": 10, "Tags": { "key": "value" - } + }, + "WorkerType": "G.025X" } }, "EtlJob40ServiceRoleBDD9998A": { @@ -1060,9 +1064,11 @@ }, "GlueVersion": "4.0", "Name": "StreamingJob4.0", + "NumberOfWorkers": 10, "Tags": { "key": "value" - } + }, + "WorkerType": "G.025X" } }, "ShellJobServiceRoleCF97BC4B": { @@ -1314,6 +1320,133 @@ "key": "value" } } + }, + "RayJobServiceRole51433C3D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "glue.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSGlueServiceRole" + ] + ] + } + ] + } + }, + "RayJobServiceRoleDefaultPolicyA615640D": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "/*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + } + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "RayJobServiceRoleDefaultPolicyA615640D", + "Roles": [ + { + "Ref": "RayJobServiceRole51433C3D" + } + ] + } + }, + "RayJob2F7864D9": { + "Type": "AWS::Glue::Job", + "Properties": { + "Command": { + "Name": "glueray", + "PythonVersion": "3.9", + "ScriptLocation": { + "Fn::Join": [ + "", + [ + "s3://", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "/432033e3218068a915d2532fa9be7858a12b228a2ae6e5c10faccd9097b1e855.py" + ] + ] + } + }, + "Role": { + "Fn::GetAtt": [ + "RayJobServiceRole51433C3D", + "Arn" + ] + }, + "DefaultArguments": { + "--job-language": "python", + "arg1": "value1", + "arg2": "value2" + }, + "GlueVersion": "4.0", + "Name": "RayJob", + "NumberOfWorkers": 2, + "Tags": { + "key": "value" + }, + "WorkerType": "Z.2X" + } } }, "Parameters": { diff --git a/packages/@aws-cdk/aws-glue/test/integ.job.js.snapshot/manifest.json b/packages/@aws-cdk/aws-glue/test/integ.job.js.snapshot/manifest.json index 9a6172107f0bc..e12d21e1befbd 100644 --- a/packages/@aws-cdk/aws-glue/test/integ.job.js.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-glue/test/integ.job.js.snapshot/manifest.json @@ -17,7 +17,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/977a2f07e22679bb04b03ce83cc1fac3e6cc269a794e38248ec67106ee39f0a2.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/b553fef631f82898c826f3c20e1de0d155dbd3a35339ef92d0893052a5be69ce.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -213,6 +213,24 @@ "data": "ShellJob390C141361" } ], + "/aws-glue-job/RayJob/ServiceRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "RayJobServiceRole51433C3D" + } + ], + "/aws-glue-job/RayJob/ServiceRole/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "RayJobServiceRoleDefaultPolicyA615640D" + } + ], + "/aws-glue-job/RayJob/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "RayJob2F7864D9" + } + ], "/aws-glue-job/BootstrapVersion": [ { "type": "aws:cdk:logicalId", diff --git a/packages/@aws-cdk/aws-glue/test/integ.job.js.snapshot/tree.json b/packages/@aws-cdk/aws-glue/test/integ.job.js.snapshot/tree.json index 712c057e96df5..e641a09da50d7 100644 --- a/packages/@aws-cdk/aws-glue/test/integ.job.js.snapshot/tree.json +++ b/packages/@aws-cdk/aws-glue/test/integ.job.js.snapshot/tree.json @@ -532,9 +532,11 @@ }, "glueVersion": "2.0", "name": "StreamingJob2.0", + "numberOfWorkers": 10, "tags": { "key": "value" - } + }, + "workerType": "G.025X" } }, "constructInfo": { @@ -1046,9 +1048,11 @@ }, "glueVersion": "3.0", "name": "StreamingJob3.0", + "numberOfWorkers": 10, "tags": { "key": "value" - } + }, + "workerType": "G.025X" } }, "constructInfo": { @@ -1560,9 +1564,11 @@ }, "glueVersion": "4.0", "name": "StreamingJob4.0", + "numberOfWorkers": 10, "tags": { "key": "value" - } + }, + "workerType": "G.025X" } }, "constructInfo": { @@ -1950,6 +1956,195 @@ "version": "0.0.0" } }, + "RayJob": { + "id": "RayJob", + "path": "aws-glue-job/RayJob", + "children": { + "ServiceRole": { + "id": "ServiceRole", + "path": "aws-glue-job/RayJob/ServiceRole", + "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "aws-glue-job/RayJob/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-glue-job/RayJob/ServiceRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "glue.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "managedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSGlueServiceRole" + ] + ] + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "aws-glue-job/RayJob/ServiceRole/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-glue-job/RayJob/ServiceRole/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "/*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + } + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "policyName": "RayJobServiceRoleDefaultPolicyA615640D", + "roles": [ + { + "Ref": "RayJobServiceRole51433C3D" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-glue-job/RayJob/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Glue::Job", + "aws:cdk:cloudformation:props": { + "command": { + "name": "glueray", + "scriptLocation": { + "Fn::Join": [ + "", + [ + "s3://", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "/432033e3218068a915d2532fa9be7858a12b228a2ae6e5c10faccd9097b1e855.py" + ] + ] + }, + "pythonVersion": "3.9" + }, + "role": { + "Fn::GetAtt": [ + "RayJobServiceRole51433C3D", + "Arn" + ] + }, + "defaultArguments": { + "--job-language": "python", + "arg1": "value1", + "arg2": "value2" + }, + "glueVersion": "4.0", + "name": "RayJob", + "numberOfWorkers": 2, + "tags": { + "key": "value" + }, + "workerType": "Z.2X" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-glue.CfnJob", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-glue.Job", + "version": "0.0.0" + } + }, "BootstrapVersion": { "id": "BootstrapVersion", "path": "aws-glue-job/BootstrapVersion", diff --git a/packages/@aws-cdk/aws-glue/test/integ.job.ts b/packages/@aws-cdk/aws-glue/test/integ.job.ts index 791fd734fb0ca..28450cfaf6a3a 100644 --- a/packages/@aws-cdk/aws-glue/test/integ.job.ts +++ b/packages/@aws-cdk/aws-glue/test/integ.job.ts @@ -63,6 +63,8 @@ const script = glue.Code.fromAsset(path.join(__dirname, 'job-script/hello_world. glueVersion, script, }), + workerType: glue.WorkerType.G_025X, + workerCount: 10, defaultArguments: { arg1: 'value1', arg2: 'value2', @@ -105,4 +107,22 @@ new glue.Job(stack, 'ShellJob39', { }, }); +new glue.Job(stack, 'RayJob', { + jobName: 'RayJob', + executable: glue.JobExecutable.pythonRay({ + glueVersion: glue.GlueVersion.V4_0, + pythonVersion: glue.PythonVersion.THREE_NINE, + script, + }), + workerType: glue.WorkerType.Z_2X, + workerCount: 2, + defaultArguments: { + arg1: 'value1', + arg2: 'value2', + }, + tags: { + key: 'value', + }, +}); + app.synth(); diff --git a/packages/@aws-cdk/aws-glue/test/job-executable.test.ts b/packages/@aws-cdk/aws-glue/test/job-executable.test.ts index 6de1ad9859509..ca5beb95d59bb 100644 --- a/packages/@aws-cdk/aws-glue/test/job-executable.test.ts +++ b/packages/@aws-cdk/aws-glue/test/job-executable.test.ts @@ -31,6 +31,8 @@ describe('JobType', () => { test('.PYTHON_SHELL should set the name correctly', () => expect(glue.JobType.PYTHON_SHELL.name).toEqual('pythonshell')); + test('.RAY should set the name correctly', () => expect(glue.JobType.RAY.name).toEqual('glueray')); + test('of(customName) should set the name correctly', () => expect(glue.JobType.of('CustomName').name).toEqual('CustomName')); }); @@ -65,6 +67,15 @@ describe('JobExecutable', () => { })).toThrow(/Python shell requires the language to be set to Python/); }); + test('with JobType.RAY and a language other than JobLanguage.PYTHON should throw', () => { + expect(() => glue.JobExecutable.of({ + glueVersion: glue.GlueVersion.V4_0, + type: glue.JobType.RAY, + language: glue.JobLanguage.SCALA, + script, + })).toThrow(/Ray requires the language to be set to Python/); + }); + test('with a non JobLanguage.PYTHON and extraPythonFiles set should throw', () => { expect(() => glue.JobExecutable.of({ glueVersion: glue.GlueVersion.V3_0, @@ -76,7 +87,7 @@ describe('JobExecutable', () => { })).toThrow(/extraPythonFiles is not supported for languages other than JobLanguage.PYTHON/); }); - [glue.GlueVersion.V0_9, glue.GlueVersion.V2_0, glue.GlueVersion.V3_0, glue.GlueVersion.V4_0].forEach((glueVersion) => { + [glue.GlueVersion.V0_9, glue.GlueVersion.V3_0, glue.GlueVersion.V4_0].forEach((glueVersion) => { test(`with JobType.PYTHON_SHELL and GlueVersion ${glueVersion} should throw`, () => { expect(() => glue.JobExecutable.of({ type: glue.JobType.PYTHON_SHELL, @@ -113,7 +124,7 @@ describe('JobExecutable', () => { }); }); - test('with PythonVersion set to PythonVersion.THREE_NINE and JobType not pythonshell should throw', () => { + test('with PythonVersion set to PythonVersion.THREE_NINE and JobType etl should throw', () => { expect(() => glue.JobExecutable.of({ type: glue.JobType.ETL, language: glue.JobLanguage.PYTHON, @@ -132,5 +143,15 @@ describe('JobExecutable', () => { script, })).toBeDefined(); }); + + test('with PythonVersion PythonVersion.THREE_NINE and JobType ray should succeed', () => { + expect(glue.JobExecutable.of({ + type: glue.JobType.RAY, + glueVersion: glue.GlueVersion.V4_0, + language: glue.JobLanguage.PYTHON, + pythonVersion: glue.PythonVersion.THREE_NINE, + script, + })).toBeDefined(); + }); }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-glue/test/job.test.ts b/packages/@aws-cdk/aws-glue/test/job.test.ts index f03d41d243494..31b6a2421e9a5 100644 --- a/packages/@aws-cdk/aws-glue/test/job.test.ts +++ b/packages/@aws-cdk/aws-glue/test/job.test.ts @@ -14,6 +14,10 @@ describe('WorkerType', () => { test('.G_2X should set the name correctly', () => expect(glue.WorkerType.G_2X.name).toEqual('G.2X')); + test('.G_025X should set the name correctly', () => expect(glue.WorkerType.G_025X.name).toEqual('G.025X')); + + test('.Z_2X should set the name correctly', () => expect(glue.WorkerType.Z_2X.name).toEqual('Z.2X')); + test('of(customType) should set name correctly', () => expect(glue.WorkerType.of('CustomType').name).toEqual('CustomType')); }); @@ -604,6 +608,33 @@ describe('Job', () => { }); }); + describe('ray job', () => { + test('with unsupported glue version should throw', () => { + expect(() => new glue.Job(stack, 'Job', { + executable: glue.JobExecutable.pythonRay({ + glueVersion: glue.GlueVersion.V3_0, + pythonVersion: glue.PythonVersion.THREE_NINE, + script, + }), + workerType: glue.WorkerType.Z_2X, + workerCount: 2, + })).toThrow('Specified GlueVersion 3.0 does not support Ray'); + }); + + test('with unsupported Spark UI prop should throw', () => { + expect(() => new glue.Job(stack, 'Job', { + executable: glue.JobExecutable.pythonRay({ + glueVersion: glue.GlueVersion.V4_0, + pythonVersion: glue.PythonVersion.THREE_NINE, + script, + }), + workerType: glue.WorkerType.Z_2X, + workerCount: 2, + sparkUI: { enabled: true }, + })).toThrow('Spark UI is not available for JobType.RAY'); + }); + }); + test('etl job with all props should synthesize correctly', () => { new glue.Job(stack, 'Job', { diff --git a/packages/@aws-cdk/aws-iam/README.md b/packages/@aws-cdk/aws-iam/README.md index f300378bffc38..0b5f20cea3758 100644 --- a/packages/@aws-cdk/aws-iam/README.md +++ b/packages/@aws-cdk/aws-iam/README.md @@ -60,7 +60,7 @@ declare const table: dynamodb.Table; table.grant(fn, 'dynamodb:PutItem'); ``` -The `grant*` methods accept an `IGrantable` object. This interface is implemented by IAM principal resources (groups, users and roles) and resources that assume a role such as a Lambda function, EC2 instance or a Codebuild project. +The `grant*` methods accept an `IGrantable` object. This interface is implemented by IAM principal resources (groups, users and roles), policies, managed policies and resources that assume a role such as a Lambda function, EC2 instance or a Codebuild project. You can find which `grant*` methods exist for a resource in the [AWS CDK API Reference](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-construct-library.html). diff --git a/packages/@aws-cdk/aws-iam/lib/managed-policy.ts b/packages/@aws-cdk/aws-iam/lib/managed-policy.ts index 0b6285cd56323..a2f3547e35e82 100644 --- a/packages/@aws-cdk/aws-iam/lib/managed-policy.ts +++ b/packages/@aws-cdk/aws-iam/lib/managed-policy.ts @@ -5,6 +5,7 @@ import { IGroup } from './group'; import { CfnManagedPolicy } from './iam.generated'; import { PolicyDocument } from './policy-document'; import { PolicyStatement } from './policy-statement'; +import { AddToPrincipalPolicyResult, IGrantable, IPrincipal, PrincipalPolicyFragment } from './principals'; import { undefinedIfEmpty } from './private/util'; import { IRole } from './role'; import { IUser } from './user'; @@ -100,7 +101,7 @@ export interface ManagedPolicyProps { * Managed policy * */ -export class ManagedPolicy extends Resource implements IManagedPolicy { +export class ManagedPolicy extends Resource implements IManagedPolicy, IGrantable { /** * Import a customer managed policy from the managedPolicyName. * @@ -202,6 +203,8 @@ export class ManagedPolicy extends Resource implements IManagedPolicy { */ public readonly path: string; + public readonly grantPrincipal: IPrincipal; + private readonly roles = new Array(); private readonly users = new Array(); private readonly groups = new Array(); @@ -263,6 +266,8 @@ export class ManagedPolicy extends Resource implements IManagedPolicy { props.statements.forEach(p => this.addStatements(p)); } + this.grantPrincipal = new ManagedPolicyGrantPrincipal(this); + this.node.addValidation({ validate: () => this.validateManagedPolicy() }); } @@ -316,3 +321,30 @@ export class ManagedPolicy extends Resource implements IManagedPolicy { return result; } } + +class ManagedPolicyGrantPrincipal implements IPrincipal { + public readonly assumeRoleAction = 'sts:AssumeRole'; + public readonly grantPrincipal: IPrincipal; + public readonly principalAccount?: string; + + constructor(private _managedPolicy: ManagedPolicy) { + this.grantPrincipal = this; + this.principalAccount = _managedPolicy.env.account; + } + + public get policyFragment(): PrincipalPolicyFragment { + // This property is referenced to add policy statements as a resource-based policy. + // We should fail because a managed policy cannot be used as a principal of a policy document. + // cf. https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html#Principal_specifying + throw new Error(`Cannot use a ManagedPolicy '${this._managedPolicy.node.path}' as the 'Principal' or 'NotPrincipal' in an IAM Policy`); + } + + public addToPolicy(statement: PolicyStatement): boolean { + return this.addToPrincipalPolicy(statement).statementAdded; + } + + public addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult { + this._managedPolicy.addStatements(statement); + return { statementAdded: true, policyDependable: this._managedPolicy }; + } +} diff --git a/packages/@aws-cdk/aws-iam/lib/policy.ts b/packages/@aws-cdk/aws-iam/lib/policy.ts index f46afbc151ec3..f881df8661ebe 100644 --- a/packages/@aws-cdk/aws-iam/lib/policy.ts +++ b/packages/@aws-cdk/aws-iam/lib/policy.ts @@ -4,6 +4,7 @@ import { IGroup } from './group'; import { CfnPolicy } from './iam.generated'; import { PolicyDocument } from './policy-document'; import { PolicyStatement } from './policy-statement'; +import { AddToPrincipalPolicyResult, IGrantable, IPrincipal, PrincipalPolicyFragment } from './principals'; import { generatePolicyName, undefinedIfEmpty } from './private/util'; import { IRole } from './role'; import { IUser } from './user'; @@ -100,7 +101,7 @@ export interface PolicyProps { * Policies](http://docs.aws.amazon.com/IAM/latest/UserGuide/policies_overview.html) * in the IAM User Guide guide. */ -export class Policy extends Resource implements IPolicy { +export class Policy extends Resource implements IPolicy, IGrantable { /** * Import a policy in this app based on its name @@ -118,6 +119,8 @@ export class Policy extends Resource implements IPolicy { */ public readonly document = new PolicyDocument(); + public readonly grantPrincipal: IPrincipal; + private readonly _policyName: string; private readonly roles = new Array(); private readonly users = new Array(); @@ -178,6 +181,8 @@ export class Policy extends Resource implements IPolicy { props.statements.forEach(p => this.addStatements(p)); } + this.grantPrincipal = new PolicyGrantPrincipal(this); + this.node.addValidation({ validate: () => this.validatePolicy() }); } @@ -260,3 +265,30 @@ export class Policy extends Resource implements IPolicy { return this.groups.length + this.users.length + this.roles.length > 0; } } + +class PolicyGrantPrincipal implements IPrincipal { + public readonly assumeRoleAction = 'sts:AssumeRole'; + public readonly grantPrincipal: IPrincipal; + public readonly principalAccount?: string; + + constructor(private _policy: Policy) { + this.grantPrincipal = this; + this.principalAccount = _policy.env.account; + } + + public get policyFragment(): PrincipalPolicyFragment { + // This property is referenced to add policy statements as a resource-based policy. + // We should fail because a policy cannot be used as a principal of a policy document. + // cf. https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html#Principal_specifying + throw new Error(`Cannot use a Policy '${this._policy.node.path}' as the 'Principal' or 'NotPrincipal' in an IAM Policy`); + } + + public addToPolicy(statement: PolicyStatement): boolean { + return this.addToPrincipalPolicy(statement).statementAdded; + } + + public addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult { + this._policy.addStatements(statement); + return { statementAdded: true, policyDependable: this._policy }; + } +} diff --git a/packages/@aws-cdk/aws-iam/lib/principals.ts b/packages/@aws-cdk/aws-iam/lib/principals.ts index bfdc8a2369644..895cfa8961798 100644 --- a/packages/@aws-cdk/aws-iam/lib/principals.ts +++ b/packages/@aws-cdk/aws-iam/lib/principals.ts @@ -116,7 +116,7 @@ export class ComparablePrincipal { */ export interface IAssumeRolePrincipal extends IPrincipal { /** - * Add the princpial to the AssumeRolePolicyDocument + * Add the principal to the AssumeRolePolicyDocument * * Add the statements to the AssumeRolePolicyDocument necessary to give this principal * permissions to assume the given role. diff --git a/packages/@aws-cdk/aws-iam/package.json b/packages/@aws-cdk/aws-iam/package.json index f42f7619e20d2..3372f6f53fb5a 100644 --- a/packages/@aws-cdk/aws-iam/package.json +++ b/packages/@aws-cdk/aws-iam/package.json @@ -82,6 +82,7 @@ "devDependencies": { "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", + "@aws-cdk/integ-tests": "0.0.0", "@aws-cdk/integ-runner": "0.0.0", "@aws-cdk/integ-tests": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/ManagedPolicyIntegDefaultTestDeployAssert27007DC6.assets.json b/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/ManagedPolicyIntegDefaultTestDeployAssert27007DC6.assets.json new file mode 100644 index 0000000000000..f89489c6f3c67 --- /dev/null +++ b/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/ManagedPolicyIntegDefaultTestDeployAssert27007DC6.assets.json @@ -0,0 +1,19 @@ +{ + "version": "21.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "ManagedPolicyIntegDefaultTestDeployAssert27007DC6.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/ManagedPolicyIntegDefaultTestDeployAssert27007DC6.template.json b/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/ManagedPolicyIntegDefaultTestDeployAssert27007DC6.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/ManagedPolicyIntegDefaultTestDeployAssert27007DC6.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/aws-cdk-iam-managed-policy.assets.json b/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/aws-cdk-iam-managed-policy.assets.json index a596a54bf5272..01d6e9e7b41fe 100644 --- a/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/aws-cdk-iam-managed-policy.assets.json +++ b/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/aws-cdk-iam-managed-policy.assets.json @@ -1,7 +1,7 @@ { - "version": "20.0.0", + "version": "21.0.0", "files": { - "368422c5e22c8b7707ddebb8f45bea20bd1c541adb0778d8e86c2e1854900523": { + "df45fa697f19036987d5bfe10fdcfba6de6d5d93be8e406edfc43fcc13fedc33": { "source": { "path": "aws-cdk-iam-managed-policy.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "368422c5e22c8b7707ddebb8f45bea20bd1c541adb0778d8e86c2e1854900523.json", + "objectKey": "df45fa697f19036987d5bfe10fdcfba6de6d5d93be8e406edfc43fcc13fedc33.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/aws-cdk-iam-managed-policy.template.json b/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/aws-cdk-iam-managed-policy.template.json index f817978f06600..0ed3d9e61a60d 100644 --- a/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/aws-cdk-iam-managed-policy.template.json +++ b/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/aws-cdk-iam-managed-policy.template.json @@ -31,6 +31,16 @@ "Action": "sqs:SendMessage", "Effect": "Allow", "Resource": "*" + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Role1ABCC5F0", + "Arn" + ] + } } ], "Version": "2012-10-17" @@ -54,6 +64,16 @@ "Action": "lambda:InvokeFunction", "Effect": "Allow", "Resource": "*" + }, + { + "Action": "iam:*", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Role1ABCC5F0", + "Arn" + ] + } } ], "Version": "2012-10-17" @@ -61,6 +81,38 @@ "Description": "", "Path": "/" } + }, + "Role1ABCC5F0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } } }, "Parameters": { diff --git a/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/cdk.out b/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/cdk.out index 588d7b269d34f..8ecc185e9dbee 100644 --- a/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/cdk.out +++ b/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"20.0.0"} \ No newline at end of file +{"version":"21.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/integ.json b/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/integ.json index 7b99bf8392eaf..93febf67f8ef7 100644 --- a/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/integ.json +++ b/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/integ.json @@ -1,14 +1,12 @@ { - "version": "20.0.0", + "version": "21.0.0", "testCases": { - "integ.managed-policy": { + "ManagedPolicyInteg/DefaultTest": { "stacks": [ "aws-cdk-iam-managed-policy" ], - "diffAssets": false, - "stackUpdateWorkflow": true + "assertionStack": "ManagedPolicyInteg/DefaultTest/DeployAssert", + "assertionStackName": "ManagedPolicyIntegDefaultTestDeployAssert27007DC6" } - }, - "synthContext": {}, - "enableLookups": false + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/manifest.json b/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/manifest.json index a6533c2f27703..e3d71b2ae9d91 100644 --- a/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "20.0.0", + "version": "21.0.0", "artifacts": { "Tree": { "type": "cdk:tree", @@ -23,7 +23,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/368422c5e22c8b7707ddebb8f45bea20bd1c541adb0778d8e86c2e1854900523.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/df45fa697f19036987d5bfe10fdcfba6de6d5d93be8e406edfc43fcc13fedc33.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -57,6 +57,12 @@ "data": "TwoManagedPolicy7E701864" } ], + "/aws-cdk-iam-managed-policy/Role/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Role1ABCC5F0" + } + ], "/aws-cdk-iam-managed-policy/BootstrapVersion": [ { "type": "aws:cdk:logicalId", @@ -71,6 +77,53 @@ ] }, "displayName": "aws-cdk-iam-managed-policy" + }, + "ManagedPolicyIntegDefaultTestDeployAssert27007DC6.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "ManagedPolicyIntegDefaultTestDeployAssert27007DC6.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "ManagedPolicyIntegDefaultTestDeployAssert27007DC6": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "ManagedPolicyIntegDefaultTestDeployAssert27007DC6.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "ManagedPolicyIntegDefaultTestDeployAssert27007DC6.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "ManagedPolicyIntegDefaultTestDeployAssert27007DC6.assets" + ], + "metadata": { + "/ManagedPolicyInteg/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/ManagedPolicyInteg/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "ManagedPolicyInteg/DefaultTest/DeployAssert" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/tree.json b/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/tree.json index ca62fedfdf3ed..55254491202f6 100644 --- a/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/tree.json +++ b/packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/tree.json @@ -9,7 +9,7 @@ "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.85" + "version": "10.1.140" } }, "aws-cdk-iam-managed-policy": { @@ -72,6 +72,16 @@ "Action": "sqs:SendMessage", "Effect": "Allow", "Resource": "*" + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Role1ABCC5F0", + "Arn" + ] + } } ], "Version": "2012-10-17" @@ -113,6 +123,16 @@ "Action": "lambda:InvokeFunction", "Effect": "Allow", "Resource": "*" + }, + { + "Action": "iam:*", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Role1ABCC5F0", + "Arn" + ] + } } ], "Version": "2012-10-17" @@ -131,17 +151,103 @@ "fqn": "@aws-cdk/aws-iam.ManagedPolicy", "version": "0.0.0" } + }, + "Role": { + "id": "Role", + "path": "aws-cdk-iam-managed-policy/Role", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-iam-managed-policy/Role/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } } }, "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.85" + "version": "10.1.140" + } + }, + "ManagedPolicyInteg": { + "id": "ManagedPolicyInteg", + "path": "ManagedPolicyInteg", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "ManagedPolicyInteg/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "ManagedPolicyInteg/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.140" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "ManagedPolicyInteg/DefaultTest/DeployAssert", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.140" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTest", + "version": "0.0.0" } } }, "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.85" + "version": "10.1.140" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/test/integ.managed-policy.ts b/packages/@aws-cdk/aws-iam/test/integ.managed-policy.ts index a250a0290d72c..26496c79e4b38 100644 --- a/packages/@aws-cdk/aws-iam/test/integ.managed-policy.ts +++ b/packages/@aws-cdk/aws-iam/test/integ.managed-policy.ts @@ -1,5 +1,6 @@ import { App, Stack } from '@aws-cdk/core'; -import { ManagedPolicy, PolicyStatement } from '../lib'; +import { IntegTest } from '@aws-cdk/integ-tests'; +import { AccountRootPrincipal, Grant, ManagedPolicy, PolicyStatement, Role } from '../lib'; import { User } from '../lib/user'; const app = new App(); @@ -23,4 +24,10 @@ user.addManagedPolicy(policy2); const policy3 = ManagedPolicy.fromAwsManagedPolicyName('SecurityAudit'); user.addManagedPolicy(policy3); -app.synth(); +const role = new Role(stack, 'Role', { assumedBy: new AccountRootPrincipal() }); +role.grantAssumeRole(policy.grantPrincipal); +Grant.addToPrincipal({ actions: ['iam:*'], resourceArns: [role.roleArn], grantee: policy2 }); + +new IntegTest(app, 'ManagedPolicyInteg', { + testCases: [stack], +}); diff --git a/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/PolicyIntegDefaultTestDeployAssert274BB918.assets.json b/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/PolicyIntegDefaultTestDeployAssert274BB918.assets.json new file mode 100644 index 0000000000000..e3f3a3767e2cc --- /dev/null +++ b/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/PolicyIntegDefaultTestDeployAssert274BB918.assets.json @@ -0,0 +1,19 @@ +{ + "version": "21.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "PolicyIntegDefaultTestDeployAssert274BB918.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/PolicyIntegDefaultTestDeployAssert274BB918.template.json b/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/PolicyIntegDefaultTestDeployAssert274BB918.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/PolicyIntegDefaultTestDeployAssert274BB918.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/aws-cdk-iam-policy.assets.json b/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/aws-cdk-iam-policy.assets.json index c36fdbef2e8c6..f758663a791a6 100644 --- a/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/aws-cdk-iam-policy.assets.json +++ b/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/aws-cdk-iam-policy.assets.json @@ -1,7 +1,7 @@ { - "version": "20.0.0", + "version": "21.0.0", "files": { - "ba8f70654832696e6df0cd04b1ceb498833915cdb77e0b4e2749036c59851533": { + "d898a04332095cb0948a67a0182d64a7d0604bb19454a2ce9dcd09153e09bb59": { "source": { "path": "aws-cdk-iam-policy.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "ba8f70654832696e6df0cd04b1ceb498833915cdb77e0b4e2749036c59851533.json", + "objectKey": "d898a04332095cb0948a67a0182d64a7d0604bb19454a2ce9dcd09153e09bb59.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/aws-cdk-iam-policy.template.json b/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/aws-cdk-iam-policy.template.json index fe198c4b4b5e2..39d726b0bea03 100644 --- a/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/aws-cdk-iam-policy.template.json +++ b/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/aws-cdk-iam-policy.template.json @@ -12,6 +12,16 @@ "Action": "sqs:SendMessage", "Effect": "Allow", "Resource": "*" + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Role1ABCC5F0", + "Arn" + ] + } } ], "Version": "2012-10-17" @@ -33,6 +43,16 @@ "Action": "lambda:InvokeFunction", "Effect": "Allow", "Resource": "*" + }, + { + "Action": "iam:*", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Role1ABCC5F0", + "Arn" + ] + } } ], "Version": "2012-10-17" @@ -44,6 +64,38 @@ } ] } + }, + "Role1ABCC5F0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } } }, "Parameters": { diff --git a/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/cdk.out b/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/cdk.out index 588d7b269d34f..8ecc185e9dbee 100644 --- a/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/cdk.out +++ b/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"20.0.0"} \ No newline at end of file +{"version":"21.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/integ.json b/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/integ.json index 4ea4d599d25d3..654d941e4fd37 100644 --- a/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/integ.json +++ b/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/integ.json @@ -1,14 +1,12 @@ { - "version": "20.0.0", + "version": "21.0.0", "testCases": { - "integ.policy": { + "PolicyInteg/DefaultTest": { "stacks": [ "aws-cdk-iam-policy" ], - "diffAssets": false, - "stackUpdateWorkflow": true + "assertionStack": "PolicyInteg/DefaultTest/DeployAssert", + "assertionStackName": "PolicyIntegDefaultTestDeployAssert274BB918" } - }, - "synthContext": {}, - "enableLookups": false + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/manifest.json b/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/manifest.json index c58a44ec9d248..ad07206570a75 100644 --- a/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "20.0.0", + "version": "21.0.0", "artifacts": { "Tree": { "type": "cdk:tree", @@ -23,7 +23,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/ba8f70654832696e6df0cd04b1ceb498833915cdb77e0b4e2749036c59851533.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/d898a04332095cb0948a67a0182d64a7d0604bb19454a2ce9dcd09153e09bb59.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -57,6 +57,12 @@ "data": "GoodbyePolicy739B8974" } ], + "/aws-cdk-iam-policy/Role/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Role1ABCC5F0" + } + ], "/aws-cdk-iam-policy/BootstrapVersion": [ { "type": "aws:cdk:logicalId", @@ -71,6 +77,53 @@ ] }, "displayName": "aws-cdk-iam-policy" + }, + "PolicyIntegDefaultTestDeployAssert274BB918.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "PolicyIntegDefaultTestDeployAssert274BB918.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "PolicyIntegDefaultTestDeployAssert274BB918": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "PolicyIntegDefaultTestDeployAssert274BB918.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "PolicyIntegDefaultTestDeployAssert274BB918.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "PolicyIntegDefaultTestDeployAssert274BB918.assets" + ], + "metadata": { + "/PolicyInteg/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/PolicyInteg/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "PolicyInteg/DefaultTest/DeployAssert" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/tree.json b/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/tree.json index 44f160e810b92..5bc1ca1cb8e4d 100644 --- a/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/tree.json +++ b/packages/@aws-cdk/aws-iam/test/integ.policy.js.snapshot/tree.json @@ -9,7 +9,7 @@ "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.85" + "version": "10.1.140" } }, "aws-cdk-iam-policy": { @@ -54,6 +54,16 @@ "Action": "sqs:SendMessage", "Effect": "Allow", "Resource": "*" + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Role1ABCC5F0", + "Arn" + ] + } } ], "Version": "2012-10-17" @@ -93,6 +103,16 @@ "Action": "lambda:InvokeFunction", "Effect": "Allow", "Resource": "*" + }, + { + "Action": "iam:*", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Role1ABCC5F0", + "Arn" + ] + } } ], "Version": "2012-10-17" @@ -115,17 +135,103 @@ "fqn": "@aws-cdk/aws-iam.Policy", "version": "0.0.0" } + }, + "Role": { + "id": "Role", + "path": "aws-cdk-iam-policy/Role", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-iam-policy/Role/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } } }, "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.85" + "version": "10.1.140" + } + }, + "PolicyInteg": { + "id": "PolicyInteg", + "path": "PolicyInteg", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "PolicyInteg/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "PolicyInteg/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.140" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "PolicyInteg/DefaultTest/DeployAssert", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.140" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTest", + "version": "0.0.0" } } }, "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.85" + "version": "10.1.140" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/test/integ.policy.ts b/packages/@aws-cdk/aws-iam/test/integ.policy.ts index ecb40dc55eb4e..058390178413d 100644 --- a/packages/@aws-cdk/aws-iam/test/integ.policy.ts +++ b/packages/@aws-cdk/aws-iam/test/integ.policy.ts @@ -1,5 +1,6 @@ import { App, Stack } from '@aws-cdk/core'; -import { Policy, PolicyStatement } from '../lib'; +import { IntegTest } from '@aws-cdk/integ-tests'; +import { AccountRootPrincipal, Grant, Policy, PolicyStatement, Role } from '../lib'; import { User } from '../lib/user'; const app = new App(); @@ -16,4 +17,10 @@ const policy2 = new Policy(stack, 'GoodbyePolicy'); policy2.addStatements(new PolicyStatement({ resources: ['*'], actions: ['lambda:InvokeFunction'] })); policy2.attachToUser(user); -app.synth(); +const role = new Role(stack, 'Role', { assumedBy: new AccountRootPrincipal() }); +role.grantAssumeRole(policy.grantPrincipal); +Grant.addToPrincipal({ actions: ['iam:*'], resourceArns: [role.roleArn], grantee: policy2 }); + +new IntegTest(app, 'PolicyInteg', { + testCases: [stack], +}); diff --git a/packages/@aws-cdk/aws-iam/test/managed-policy.test.ts b/packages/@aws-cdk/aws-iam/test/managed-policy.test.ts index 9c12f248e6de8..8ea91b509a6ee 100644 --- a/packages/@aws-cdk/aws-iam/test/managed-policy.test.ts +++ b/packages/@aws-cdk/aws-iam/test/managed-policy.test.ts @@ -1,6 +1,6 @@ import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; -import { Group, ManagedPolicy, PolicyDocument, PolicyStatement, Role, ServicePrincipal, User, Effect } from '../lib'; +import { AddToPrincipalPolicyResult, Grant, Group, IResourceWithPolicy, ManagedPolicy, PolicyDocument, PolicyStatement, Role, ServicePrincipal, User, Effect } from '../lib'; describe('managed policy', () => { let app: cdk.App; @@ -614,6 +614,85 @@ describe('managed policy', () => { }); }); + test('Policies can be granted principal permissions', () => { + const mp = new ManagedPolicy(stack, 'Policy', { + managedPolicyName: 'MyManagedPolicyName', + }); + Grant.addToPrincipal({ actions: ['dummy:Action'], grantee: mp, resourceArns: ['*'] }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::ManagedPolicy', { + ManagedPolicyName: 'MyManagedPolicyName', + PolicyDocument: { + Statement: [ + { Action: 'dummy:Action', Effect: 'Allow', Resource: '*' }, + ], + Version: '2012-10-17', + }, + Path: '/', + Description: '', + }); + }); + + test('addPrincipalOrResource() correctly grants Policies permissions', () => { + const mp = new ManagedPolicy(stack, 'Policy', { + managedPolicyName: 'MyManagedPolicyName', + }); + + class DummyResource extends cdk.Resource implements IResourceWithPolicy { + addToResourcePolicy(_statement: PolicyStatement): AddToPrincipalPolicyResult { + throw new Error('should not be called.'); + } + }; + const resource = new DummyResource(stack, 'Dummy'); + Grant.addToPrincipalOrResource({ actions: ['dummy:Action'], grantee: mp, resourceArns: ['*'], resource }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::ManagedPolicy', { + ManagedPolicyName: 'MyManagedPolicyName', + PolicyDocument: { + Statement: [ + { Action: 'dummy:Action', Effect: 'Allow', Resource: '*' }, + ], + Version: '2012-10-17', + }, + Path: '/', + Description: '', + }); + }); + + test('Policies cannot be granted principal permissions across accounts', () => { + const mp = new ManagedPolicy(stack, 'Policy', { + managedPolicyName: 'MyManagedPolicyName', + }); + + class DummyResource extends cdk.Resource implements IResourceWithPolicy { + addToResourcePolicy(_statement: PolicyStatement): AddToPrincipalPolicyResult { + throw new Error('should not be called.'); + } + }; + const resource = new DummyResource(stack, 'Dummy', { account: '5678' }); + + expect(() => { + Grant.addToPrincipalOrResource({ actions: ['dummy:Action'], grantee: mp, resourceArns: ['*'], resource }); + }).toThrow(/Cannot use a ManagedPolicy 'MyStack\/Policy'/); + }); + + test('Policies cannot be granted resource permissions', () => { + const mp = new ManagedPolicy(stack, 'Policy', { + managedPolicyName: 'MyManagedPolicyName', + }); + + class DummyResource extends cdk.Resource implements IResourceWithPolicy { + addToResourcePolicy(_statement: PolicyStatement): AddToPrincipalPolicyResult { + throw new Error('should not be called.'); + } + }; + const resource = new DummyResource(stack, 'Dummy'); + + expect(() => { + Grant.addToPrincipalAndResource({ actions: ['dummy:Action'], grantee: mp, resourceArns: ['*'], resource }); + }).toThrow(/Cannot use a ManagedPolicy 'MyStack\/Policy'/); + }); + test('prevent creation when customizeRoles is configured', () => { // GIVEN const otherStack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-iam/test/policy.test.ts b/packages/@aws-cdk/aws-iam/test/policy.test.ts index ea40450756935..8bc43abbc819b 100644 --- a/packages/@aws-cdk/aws-iam/test/policy.test.ts +++ b/packages/@aws-cdk/aws-iam/test/policy.test.ts @@ -1,6 +1,6 @@ import { Template } from '@aws-cdk/assertions'; -import { App, CfnResource, Stack } from '@aws-cdk/core'; -import { AnyPrincipal, CfnPolicy, Group, Policy, PolicyDocument, PolicyStatement, Role, ServicePrincipal, User } from '../lib'; +import { App, CfnResource, Resource, Stack } from '@aws-cdk/core'; +import { AddToPrincipalPolicyResult, AnyPrincipal, CfnPolicy, Grant, Group, IResourceWithPolicy, Policy, PolicyDocument, PolicyStatement, Role, ServicePrincipal, User } from '../lib'; /* eslint-disable quote-props */ @@ -440,6 +440,83 @@ describe('IAM policy', () => { expect(() => app.synth()).toThrow(/A PolicyStatement used in an identity-based policy cannot specify any IAM principals/); }); + + test('Policies can be granted principal permissions', () => { + const pol = new Policy(stack, 'Policy', { + policyName: 'MyPolicyName', + }); + Grant.addToPrincipal({ actions: ['dummy:Action'], grantee: pol, resourceArns: ['*'] }); + pol.attachToUser(new User(stack, 'User')); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyName: 'MyPolicyName', + PolicyDocument: { + Statement: [ + { Action: 'dummy:Action', Effect: 'Allow', Resource: '*' }, + ], + Version: '2012-10-17', + }, + }); + }); + + test('addPrincipalOrResource() correctly grants Policies permissions', () => { + const pol = new Policy(stack, 'Policy', { + policyName: 'MyPolicyName', + }); + pol.attachToUser(new User(stack, 'User')); + + class DummyResource extends Resource implements IResourceWithPolicy { + addToResourcePolicy(_statement: PolicyStatement): AddToPrincipalPolicyResult { + throw new Error('should not be called.'); + } + }; + const resource = new DummyResource(stack, 'Dummy'); + Grant.addToPrincipalOrResource({ actions: ['dummy:Action'], grantee: pol, resource, resourceArns: ['*'] }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyName: 'MyPolicyName', + PolicyDocument: { + Statement: [ + { Action: 'dummy:Action', Effect: 'Allow', Resource: '*' }, + ], + Version: '2012-10-17', + }, + }); + }); + + test('Policies cannot be granted principal permissions across accounts', () => { + const pol = new Policy(stack, 'Policy', { + policyName: 'MyPolicyName', + }); + + class DummyResource extends Resource implements IResourceWithPolicy { + addToResourcePolicy(_statement: PolicyStatement): AddToPrincipalPolicyResult { + throw new Error('should not be called.'); + } + }; + const resource = new DummyResource(stack, 'Dummy', { account: '5678' }); + + expect(() => { + Grant.addToPrincipalOrResource({ actions: ['dummy:Action'], grantee: pol, resourceArns: ['*'], resource }); + }).toThrow(/Cannot use a Policy 'MyStack\/Policy'/); + }); + + test('Policies cannot be granted resource permissions', () => { + const pol = new Policy(stack, 'Policy', { + policyName: 'MyPolicyName', + }); + + class DummyResource extends Resource implements IResourceWithPolicy { + addToResourcePolicy(_statement: PolicyStatement): AddToPrincipalPolicyResult { + throw new Error('should not be called.'); + } + }; + const resource = new DummyResource(stack, 'Dummy'); + + expect(() => { + Grant.addToPrincipalAndResource({ actions: ['dummy:Action'], grantee: pol, resourceArns: ['*'], resource }); + }).toThrow(/Cannot use a Policy 'MyStack\/Policy'/); + }); }); function createPolicyWithLogicalId(stack: Stack, logicalId: string): void { diff --git a/packages/@aws-cdk/aws-lambda/README.md b/packages/@aws-cdk/aws-lambda/README.md index f1b4d22719bc3..c48640e5e65b0 100644 --- a/packages/@aws-cdk/aws-lambda/README.md +++ b/packages/@aws-cdk/aws-lambda/README.md @@ -1042,3 +1042,28 @@ new lambda.Function(this, 'Function', { code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')), }); ``` + +## Runtime updates + +Lambda runtime management controls help reduce the risk of impact to your workloads in the rare event of a runtime version incompatibility. +For more information, see [Runtime management controls](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-update.html#runtime-management-controls) + +```ts +new Function(stack, 'Lambda', { + runtimeManagementMode: RuntimeManagementMode.AUTO, + runtime: lambda.Runtime.NODEJS_18_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')), +}); +``` + +If you want to set the "Manual" setting, using the ARN of the runtime version as the argument. + +```ts +new Function(stack, 'Lambda', { + runtimeManagementMode: RuntimeManagementMode.manual('runtimeVersion-arn'), + runtime: lambda.Runtime.NODEJS_18_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')), +}); +``` diff --git a/packages/@aws-cdk/aws-lambda/lib/adot-layers.ts b/packages/@aws-cdk/aws-lambda/lib/adot-layers.ts index 9c54da9041071..ba2c7a0ed2a04 100644 --- a/packages/@aws-cdk/aws-lambda/lib/adot-layers.ts +++ b/packages/@aws-cdk/aws-lambda/lib/adot-layers.ts @@ -1,10 +1,10 @@ import { RegionInfo } from '@aws-cdk/region-info'; import { IConstruct } from 'constructs'; +import { Architecture } from './architecture'; +import { IFunction } from './function-base'; import { Stack } from '../../core/lib/stack'; import { Token } from '../../core/lib/token'; import { FactName } from '../../region-info/lib/fact'; -import { Architecture } from './architecture'; -import { IFunction } from './function-base'; /** * The type of ADOT Lambda layer diff --git a/packages/@aws-cdk/aws-lambda/lib/function.ts b/packages/@aws-cdk/aws-lambda/lib/function.ts index a998fad63ae57..789ccf8295907 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function.ts @@ -26,6 +26,7 @@ import { CfnFunction } from './lambda.generated'; import { LayerVersion, ILayerVersion } from './layers'; import { LogRetentionRetryOptions } from './log-retention'; import { Runtime } from './runtime'; +import { RuntimeManagementMode } from './runtime-management'; import { addAlias } from './util'; /** @@ -359,6 +360,12 @@ export interface FunctionOptions extends EventInvokeConfigOptions { * @default Architecture.X86_64 */ readonly architecture?: Architecture; + + /** + * Sets the runtime management configuration for a function's version. + * @default Auto + */ + readonly runtimeManagementMode?: RuntimeManagementMode; } export interface FunctionProps extends FunctionOptions { @@ -814,6 +821,7 @@ export class Function extends FunctionBase { fileSystemConfigs, codeSigningConfigArn: props.codeSigningConfig?.codeSigningConfigArn, architectures: this._architecture ? [this._architecture.name] : undefined, + runtimeManagementConfig: props.runtimeManagementMode?.runtimeManagementConfig, }); if ((props.tracing !== undefined) || (props.adotInstrumentation !== undefined)) { diff --git a/packages/@aws-cdk/aws-lambda/lib/index.ts b/packages/@aws-cdk/aws-lambda/lib/index.ts index e317a58bee4bf..2d99e775f29b6 100644 --- a/packages/@aws-cdk/aws-lambda/lib/index.ts +++ b/packages/@aws-cdk/aws-lambda/lib/index.ts @@ -23,6 +23,7 @@ export * from './lambda-insights'; export * from './log-retention'; export * from './architecture'; export * from './function-url'; +export * from './runtime-management'; // AWS::Lambda CloudFormation Resources: export * from './lambda.generated'; diff --git a/packages/@aws-cdk/aws-lambda/lib/runtime-management.ts b/packages/@aws-cdk/aws-lambda/lib/runtime-management.ts new file mode 100644 index 0000000000000..f83423ed11ae1 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/lib/runtime-management.ts @@ -0,0 +1,47 @@ +import { CfnFunction } from './lambda.generated'; + +/** + * Specify the runtime update mode. + */ +export class RuntimeManagementMode { + /** + * Automatically update to the most recent and secure runtime version using Two-phase runtime version rollout. + * We recommend this mode for most customers so that you always benefit from runtime updates. + */ + public static readonly AUTO = new RuntimeManagementMode('Auto'); + /** + * When you update your function, Lambda updates the runtime of your function to the most recent and secure runtime version. + * This approach synchronizes runtime updates with function deployments, + * giving you control over when Lambda applies runtime updates. + * With this mode, you can detect and mitigate rare runtime update incompatibilities early. + * When using this mode, you must regularly update your functions to keep their runtime up to date. + */ + public static readonly FUNCTION_UPDATE = new RuntimeManagementMode('Function update'); + /** + * You specify a runtime version in your function configuration. + * The function uses this runtime version indefinitely. + * In the rare case in which a new runtime version is incompatible with an existing function, + * you can use this mode to roll back your function to an earlier runtime version. + */ + public static manual(arn: string): RuntimeManagementMode { + return new RuntimeManagementMode('Manual', arn); + } + + /** + * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-runtimemanagementconfig.html + */ + readonly runtimeManagementConfig: CfnFunction.RuntimeManagementConfigProperty; + + protected constructor(public readonly mode: string, public readonly arn?: string) { + if (arn) { + this.runtimeManagementConfig = { + runtimeVersionArn: arn, + updateRuntimeOn: mode, + }; + } else { + this.runtimeManagementConfig = { + updateRuntimeOn: mode, + }; + } + } +} diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index 08db54a7964ef..6fe5c6d2e7833 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -190,7 +190,9 @@ "props-physical-name:@aws-cdk/aws-lambda.EventInvokeConfigProps", "props-physical-name:@aws-cdk/aws-lambda.CodeSigningConfigProps", "props-physical-name:@aws-cdk/aws-lambda.FunctionUrlProps", - "from-method:@aws-cdk/aws-lambda.FunctionUrl" + "from-method:@aws-cdk/aws-lambda.FunctionUrl", + "docs-public-apis:@aws-cdk/aws-lambda.RuntimeManagementMode.mode", + "docs-public-apis:@aws-cdk/aws-lambda.RuntimeManagementMode.arn" ] }, "stability": "stable", diff --git a/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.js.snapshot/aws-cdk-lambda-runtime-management.assets.json b/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.js.snapshot/aws-cdk-lambda-runtime-management.assets.json new file mode 100644 index 0000000000000..276849ed5d69d --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.js.snapshot/aws-cdk-lambda-runtime-management.assets.json @@ -0,0 +1,19 @@ +{ + "version": "29.0.0", + "files": { + "45968e77d38b164ece946e2a09ba83ed011953b9ee4b075f276fd124c61df607": { + "source": { + "path": "aws-cdk-lambda-runtime-management.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "45968e77d38b164ece946e2a09ba83ed011953b9ee4b075f276fd124c61df607.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.js.snapshot/aws-cdk-lambda-runtime-management.template.json b/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.js.snapshot/aws-cdk-lambda-runtime-management.template.json new file mode 100644 index 0000000000000..16b2840fee01d --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.js.snapshot/aws-cdk-lambda-runtime-management.template.json @@ -0,0 +1,91 @@ +{ + "Resources": { + "LambdaServiceRoleA8ED4D3B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "LambdaD247545B": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "foo" + }, + "Role": { + "Fn::GetAtt": [ + "LambdaServiceRoleA8ED4D3B", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs18.x", + "RuntimeManagementConfig": { + "UpdateRuntimeOn": "Auto" + } + }, + "DependsOn": [ + "LambdaServiceRoleA8ED4D3B" + ] + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.js.snapshot/cdk.out b/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.js.snapshot/cdk.out new file mode 100644 index 0000000000000..d8b441d447f8a --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"29.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.js.snapshot/integ.json b/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.js.snapshot/integ.json new file mode 100644 index 0000000000000..cb986e80f624b --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "29.0.0", + "testCases": { + "lambda-runtime-management/DefaultTest": { + "stacks": [ + "aws-cdk-lambda-runtime-management" + ], + "assertionStack": "lambda-runtime-management/DefaultTest/DeployAssert", + "assertionStackName": "lambdaruntimemanagementDefaultTestDeployAssertDE680AF3" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.js.snapshot/lambdaruntimemanagementDefaultTestDeployAssertDE680AF3.assets.json b/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.js.snapshot/lambdaruntimemanagementDefaultTestDeployAssertDE680AF3.assets.json new file mode 100644 index 0000000000000..2aab28e5ef2c8 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.js.snapshot/lambdaruntimemanagementDefaultTestDeployAssertDE680AF3.assets.json @@ -0,0 +1,19 @@ +{ + "version": "29.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "lambdaruntimemanagementDefaultTestDeployAssertDE680AF3.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.js.snapshot/lambdaruntimemanagementDefaultTestDeployAssertDE680AF3.template.json b/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.js.snapshot/lambdaruntimemanagementDefaultTestDeployAssertDE680AF3.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.js.snapshot/lambdaruntimemanagementDefaultTestDeployAssertDE680AF3.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.js.snapshot/manifest.json b/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.js.snapshot/manifest.json new file mode 100644 index 0000000000000..5593557616b3f --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.js.snapshot/manifest.json @@ -0,0 +1,117 @@ +{ + "version": "29.0.0", + "artifacts": { + "aws-cdk-lambda-runtime-management.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-cdk-lambda-runtime-management.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-cdk-lambda-runtime-management": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-cdk-lambda-runtime-management.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/45968e77d38b164ece946e2a09ba83ed011953b9ee4b075f276fd124c61df607.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-cdk-lambda-runtime-management.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "aws-cdk-lambda-runtime-management.assets" + ], + "metadata": { + "/aws-cdk-lambda-runtime-management/Lambda/ServiceRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "LambdaServiceRoleA8ED4D3B" + } + ], + "/aws-cdk-lambda-runtime-management/Lambda/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "LambdaD247545B" + } + ], + "/aws-cdk-lambda-runtime-management/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/aws-cdk-lambda-runtime-management/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "aws-cdk-lambda-runtime-management" + }, + "lambdaruntimemanagementDefaultTestDeployAssertDE680AF3.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "lambdaruntimemanagementDefaultTestDeployAssertDE680AF3.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "lambdaruntimemanagementDefaultTestDeployAssertDE680AF3": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "lambdaruntimemanagementDefaultTestDeployAssertDE680AF3.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "lambdaruntimemanagementDefaultTestDeployAssertDE680AF3.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "lambdaruntimemanagementDefaultTestDeployAssertDE680AF3.assets" + ], + "metadata": { + "/lambda-runtime-management/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/lambda-runtime-management/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "lambda-runtime-management/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.js.snapshot/tree.json b/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.js.snapshot/tree.json new file mode 100644 index 0000000000000..055a6d9c200f0 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.js.snapshot/tree.json @@ -0,0 +1,195 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "aws-cdk-lambda-runtime-management": { + "id": "aws-cdk-lambda-runtime-management", + "path": "aws-cdk-lambda-runtime-management", + "children": { + "Lambda": { + "id": "Lambda", + "path": "aws-cdk-lambda-runtime-management/Lambda", + "children": { + "ServiceRole": { + "id": "ServiceRole", + "path": "aws-cdk-lambda-runtime-management/Lambda/ServiceRole", + "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "aws-cdk-lambda-runtime-management/Lambda/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-cdk-lambda-runtime-management/Lambda/ServiceRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "managedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-cdk-lambda-runtime-management/Lambda/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Lambda::Function", + "aws:cdk:cloudformation:props": { + "code": { + "zipFile": "foo" + }, + "role": { + "Fn::GetAtt": [ + "LambdaServiceRoleA8ED4D3B", + "Arn" + ] + }, + "handler": "index.handler", + "runtime": "nodejs18.x", + "runtimeManagementConfig": { + "updateRuntimeOn": "Auto" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.CfnFunction", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.Function", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "aws-cdk-lambda-runtime-management/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "aws-cdk-lambda-runtime-management/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "lambda-runtime-management": { + "id": "lambda-runtime-management", + "path": "lambda-runtime-management", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "lambda-runtime-management/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "lambda-runtime-management/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.235" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "lambda-runtime-management/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "lambda-runtime-management/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "lambda-runtime-management/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.235" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.ts b/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.ts new file mode 100644 index 0000000000000..cc0682721f4ed --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.runtime-management.ts @@ -0,0 +1,20 @@ +import { App, Stack } from '@aws-cdk/core'; +import * as integ from '@aws-cdk/integ-tests'; +import { Function, InlineCode, Runtime, RuntimeManagementMode } from '../lib'; + +const app = new App(); + +const stack = new Stack(app, 'aws-cdk-lambda-runtime-management'); + +new Function(stack, 'Lambda', { + code: new InlineCode('foo'), + handler: 'index.handler', + runtime: Runtime.NODEJS_18_X, + runtimeManagementMode: RuntimeManagementMode.AUTO, +}); + +new integ.IntegTest(app, 'lambda-runtime-management', { + testCases: [stack], +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-lambda/test/runtime-management.test.ts b/packages/@aws-cdk/aws-lambda/test/runtime-management.test.ts new file mode 100644 index 0000000000000..1d0491f508755 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/runtime-management.test.ts @@ -0,0 +1,60 @@ +import { Template } from '@aws-cdk/assertions'; +import * as cdk from '@aws-cdk/core'; +import * as lambda from '../lib'; + +describe('runtime', () => { + test('Runtime Management Auto', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'stack'); + new lambda.Function(stack, 'Lambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_18_X, + runtimeManagementMode: lambda.RuntimeManagementMode.AUTO, + }); + // WHEN + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { + RuntimeManagementConfig: { + UpdateRuntimeOn: 'Auto', + }, + }); + }); + test('Runtime Management Function Update', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'stack'); + new lambda.Function(stack, 'Lambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_18_X, + runtimeManagementMode: lambda.RuntimeManagementMode.FUNCTION_UPDATE, + }); + // WHEN + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { + RuntimeManagementConfig: { + UpdateRuntimeOn: 'Function update', + }, + }); + }); + test('Runtime Management MANUAL', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'stack'); + new lambda.Function(stack, 'Lambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_18_X, + runtimeManagementMode: lambda.RuntimeManagementMode.manual( + 'arn:aws:lambda:ap-northeast-1::runtime:07a48df201798d627f2b950f03bb227aab4a655a1d019c3296406f95937e2525', + ), + }); + // WHEN + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { + RuntimeManagementConfig: { + RuntimeVersionArn: 'arn:aws:lambda:ap-northeast-1::runtime:07a48df201798d627f2b950f03bb227aab4a655a1d019c3296406f95937e2525', + UpdateRuntimeOn: 'Manual', + }, + }); + }); +}); diff --git a/packages/@aws-cdk/aws-omics/.eslintrc.js b/packages/@aws-cdk/aws-omics/.eslintrc.js new file mode 100644 index 0000000000000..2658ee8727166 --- /dev/null +++ b/packages/@aws-cdk/aws-omics/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-omics/.gitignore b/packages/@aws-cdk/aws-omics/.gitignore new file mode 100644 index 0000000000000..62ebc95d75ce6 --- /dev/null +++ b/packages/@aws-cdk/aws-omics/.gitignore @@ -0,0 +1,19 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk +nyc.config.js +!.eslintrc.js +!jest.config.js +junit.xml diff --git a/packages/@aws-cdk/aws-omics/.npmignore b/packages/@aws-cdk/aws-omics/.npmignore new file mode 100644 index 0000000000000..f931fede67c44 --- /dev/null +++ b/packages/@aws-cdk/aws-omics/.npmignore @@ -0,0 +1,29 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json + +.eslintrc.js +jest.config.js + +# exclude cdk artifacts +**/cdk.out +junit.xml +test/ +!*.lit.ts diff --git a/packages/@aws-cdk/aws-omics/LICENSE b/packages/@aws-cdk/aws-omics/LICENSE new file mode 100644 index 0000000000000..9b722c65c5481 --- /dev/null +++ b/packages/@aws-cdk/aws-omics/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-omics/NOTICE b/packages/@aws-cdk/aws-omics/NOTICE new file mode 100644 index 0000000000000..a27b7dd317649 --- /dev/null +++ b/packages/@aws-cdk/aws-omics/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-omics/README.md b/packages/@aws-cdk/aws-omics/README.md new file mode 100644 index 0000000000000..291af1cba3bb8 --- /dev/null +++ b/packages/@aws-cdk/aws-omics/README.md @@ -0,0 +1,39 @@ +# AWS::Omics Construct Library + + +--- + +![cfn-resources: Stable](https://img.shields.io/badge/cfn--resources-stable-success.svg?style=for-the-badge) + +> All classes with the `Cfn` prefix in this module ([CFN Resources]) are always stable and safe to use. +> +> [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib + +--- + + + +This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. + +```ts nofixture +import * as omics from '@aws-cdk/aws-omics'; +``` + + + +There are no official hand-written ([L2](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) constructs for this service yet. Here are some suggestions on how to proceed: + +- Search [Construct Hub for Omics construct libraries](https://constructs.dev/search?q=omics) +- Use the automatically generated [L1](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_l1_using) constructs, in the same way you would use [the CloudFormation AWS::Omics resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_Omics.html) directly. + + + + +There are no hand-written ([L2](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) constructs for this service yet. +However, you can still use the automatically generated [L1](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_l1_using) constructs, and use this service exactly as you would using CloudFormation directly. + +For more information on the resources and properties available for this service, see the [CloudFormation documentation for AWS::Omics](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_Omics.html). + +(Read the [CDK Contributing Guide](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and submit an RFC if you are interested in contributing to this construct library.) + + diff --git a/packages/@aws-cdk/aws-omics/jest.config.js b/packages/@aws-cdk/aws-omics/jest.config.js new file mode 100644 index 0000000000000..3a2fd93a1228a --- /dev/null +++ b/packages/@aws-cdk/aws-omics/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-omics/lib/index.ts b/packages/@aws-cdk/aws-omics/lib/index.ts new file mode 100644 index 0000000000000..d3d4ee15cf498 --- /dev/null +++ b/packages/@aws-cdk/aws-omics/lib/index.ts @@ -0,0 +1,2 @@ +// AWS::Omics CloudFormation Resources: +export * from './omics.generated'; diff --git a/packages/@aws-cdk/aws-omics/package.json b/packages/@aws-cdk/aws-omics/package.json new file mode 100644 index 0000000000000..eb8734ad85b88 --- /dev/null +++ b/packages/@aws-cdk/aws-omics/package.json @@ -0,0 +1,113 @@ +{ + "name": "@aws-cdk/aws-omics", + "version": "0.0.0", + "description": "AWS::Omics Construct Library", + "private": true, + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "projectReferences": true, + "targets": { + "dotnet": { + "namespace": "Amazon.CDK.AWS.Omics", + "packageId": "Amazon.CDK.AWS.Omics", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/main/logo/default-256-dark.png" + }, + "java": { + "package": "software.amazon.awscdk.services.omics", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "omics" + } + }, + "python": { + "classifiers": [ + "Framework :: AWS CDK", + "Framework :: AWS CDK :: 2" + ], + "distName": "aws-cdk.aws-omics", + "module": "aws_cdk.aws_omics" + } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-omics" + }, + "homepage": "https://github.com/aws/aws-cdk", + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "cfn2ts": "cfn2ts", + "build+test": "yarn build && yarn test", + "build+test+package": "yarn build+test && yarn package", + "compat": "cdk-compat", + "gen": "cfn2ts", + "rosetta:extract": "yarn --silent jsii-rosetta extract", + "build+extract": "yarn build && yarn rosetta:extract", + "build+test+extract": "yarn build+test && yarn rosetta:extract" + }, + "cdk-build": { + "cloudformation": "AWS::Omics", + "jest": true, + "env": { + "AWSLINT_BASE_CONSTRUCT": "true" + } + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "AWS::Omics", + "aws-omics" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assertions": "0.0.0", + "@aws-cdk/cdk-build-tools": "0.0.0", + "@aws-cdk/cfn2ts": "0.0.0", + "@aws-cdk/pkglint": "0.0.0", + "@types/jest": "^27.5.2" + }, + "dependencies": { + "@aws-cdk/core": "0.0.0", + "constructs": "^10.0.0" + }, + "peerDependencies": { + "@aws-cdk/core": "0.0.0", + "constructs": "^10.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "stability": "experimental", + "maturity": "cfn-only", + "awscdkio": { + "announce": false + }, + "publishConfig": { + "tag": "latest" + } +} diff --git a/packages/@aws-cdk/aws-omics/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-omics/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..e208762bca03c --- /dev/null +++ b/packages/@aws-cdk/aws-omics/rosetta/default.ts-fixture @@ -0,0 +1,8 @@ +import { Construct } from 'constructs'; +import { Stack } from '@aws-cdk/core'; + +class MyStack extends Stack { + constructor(scope: Construct, id: string) { + /// here + } +} diff --git a/packages/@aws-cdk/aws-omics/test/omics.test.ts b/packages/@aws-cdk/aws-omics/test/omics.test.ts new file mode 100644 index 0000000000000..465c7bdea0693 --- /dev/null +++ b/packages/@aws-cdk/aws-omics/test/omics.test.ts @@ -0,0 +1,6 @@ +import '@aws-cdk/assertions'; +import {} from '../lib'; + +test('No tests are specified for this package', () => { + expect(true).toBe(true); +}); diff --git a/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts b/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts index c36ca793bac25..9c43f8350fb6e 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts @@ -645,6 +645,10 @@ export class AuroraPostgresEngineVersion { public static readonly VER_11_15 = AuroraPostgresEngineVersion.of('11.15', '11', { s3Import: true, s3Export: true }); /** Version "11.16". */ public static readonly VER_11_16 = AuroraPostgresEngineVersion.of('11.16', '11', { s3Import: true, s3Export: true }); + /** Version "11.17". */ + public static readonly VER_11_17 = AuroraPostgresEngineVersion.of('11.17', '11', { s3Import: true, s3Export: true }); + /** Version "11.18". */ + public static readonly VER_11_18 = AuroraPostgresEngineVersion.of('11.18', '11', { s3Import: true, s3Export: true }); /** Version "12.4". */ public static readonly VER_12_4 = AuroraPostgresEngineVersion.of('12.4', '12', { s3Import: true, s3Export: true }); /** Version "12.6". */ @@ -659,6 +663,10 @@ export class AuroraPostgresEngineVersion { public static readonly VER_12_10 = AuroraPostgresEngineVersion.of('12.10', '12', { s3Import: true, s3Export: true }); /** Version "12.11". */ public static readonly VER_12_11 = AuroraPostgresEngineVersion.of('12.11', '12', { s3Import: true, s3Export: true }); + /** Version "12.12". */ + public static readonly VER_12_12 = AuroraPostgresEngineVersion.of('12.12', '12', { s3Import: true, s3Export: true }); + /** Version "12.13". */ + public static readonly VER_12_13 = AuroraPostgresEngineVersion.of('12.13', '12', { s3Import: true, s3Export: true }); /** Version "13.3". */ public static readonly VER_13_3 = AuroraPostgresEngineVersion.of('13.3', '13', { s3Import: true, s3Export: true }); /** Version "13.4". */ @@ -669,6 +677,10 @@ export class AuroraPostgresEngineVersion { public static readonly VER_13_6 = AuroraPostgresEngineVersion.of('13.6', '13', { s3Import: true, s3Export: true }); /** Version "13.7". */ public static readonly VER_13_7 = AuroraPostgresEngineVersion.of('13.7', '13', { s3Import: true, s3Export: true }); + /** Version "13.8". */ + public static readonly VER_13_8 = AuroraPostgresEngineVersion.of('13.8', '13', { s3Import: true, s3Export: true }); + /** Version "13.9". */ + public static readonly VER_13_9 = AuroraPostgresEngineVersion.of('13.9', '13', { s3Import: true, s3Export: true }); /** Version "14.3". */ public static readonly VER_14_3 = AuroraPostgresEngineVersion.of('14.3', '14', { s3Import: true, s3Export: true }); /** diff --git a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts index 7fcce2aa7b922..4d85631b02ba1 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts @@ -931,6 +931,8 @@ export class PostgresEngineVersion { public static readonly VER_10_21 = PostgresEngineVersion.of('10.21', '10', { s3Import: true, s3Export: true }); /** Version "10.22". */ public static readonly VER_10_22 = PostgresEngineVersion.of('10.22', '10', { s3Import: true, s3Export: true }); + /** Version "10.23". */ + public static readonly VER_10_23 = PostgresEngineVersion.of('10.23', '10', { s3Import: true, s3Export: true }); /** Version "11" (only a major version, without a specific minor version). */ public static readonly VER_11 = PostgresEngineVersion.of('11', '11', { s3Import: true }); @@ -966,6 +968,8 @@ export class PostgresEngineVersion { public static readonly VER_11_16 = PostgresEngineVersion.of('11.16', '11', { s3Import: true, s3Export: true }); /** Version "11.17". */ public static readonly VER_11_17 = PostgresEngineVersion.of('11.17', '11', { s3Import: true, s3Export: true }); + /** Version "11.18". */ + public static readonly VER_11_18 = PostgresEngineVersion.of('11.18', '11', { s3Import: true, s3Export: true }); /** Version "12" (only a major version, without a specific minor version). */ public static readonly VER_12 = PostgresEngineVersion.of('12', '12', { s3Import: true }); @@ -991,6 +995,8 @@ export class PostgresEngineVersion { public static readonly VER_12_11 = PostgresEngineVersion.of('12.11', '12', { s3Import: true, s3Export: true }); /** Version "12.12". */ public static readonly VER_12_12 = PostgresEngineVersion.of('12.12', '12', { s3Import: true, s3Export: true }); + /** Version "12.13". */ + public static readonly VER_12_13 = PostgresEngineVersion.of('12.13', '12', { s3Import: true, s3Export: true }); /** Version "13" (only a major version, without a specific minor version). */ public static readonly VER_13 = PostgresEngineVersion.of('13', '13', { s3Import: true, s3Export: true }); @@ -1010,6 +1016,8 @@ export class PostgresEngineVersion { public static readonly VER_13_7 = PostgresEngineVersion.of('13.7', '13', { s3Import: true, s3Export: true }); /** Version "13.8". */ public static readonly VER_13_8 = PostgresEngineVersion.of('13.8', '13', { s3Import: true, s3Export: true }); + /** Version "13.9". */ + public static readonly VER_13_9 = PostgresEngineVersion.of('13.9', '13', { s3Import: true, s3Export: true }); /** Version "14" (only a major version, without a specific minor version). */ public static readonly VER_14 = PostgresEngineVersion.of('14', '14', { s3Import: true, s3Export: true }); @@ -1023,6 +1031,8 @@ export class PostgresEngineVersion { public static readonly VER_14_4 = PostgresEngineVersion.of('14.4', '14', { s3Import: true, s3Export: true }); /** Version "14.5". */ public static readonly VER_14_5 = PostgresEngineVersion.of('14.5', '14', { s3Import: true, s3Export: true }); + /** Version "14.6". */ + public static readonly VER_14_6 = PostgresEngineVersion.of('14.6', '14', { s3Import: true, s3Export: true }); /** * Create a new PostgresEngineVersion with an arbitrary version. diff --git a/packages/@aws-cdk/aws-redshift/README.md b/packages/@aws-cdk/aws-redshift/README.md index 685d9b1666273..fcd48b57af0a6 100644 --- a/packages/@aws-cdk/aws-redshift/README.md +++ b/packages/@aws-cdk/aws-redshift/README.md @@ -54,7 +54,7 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as s3 from '@aws-cdk/aws-s3'; const vpc = new ec2.Vpc(this, 'Vpc'); -const bucket = s3.Bucket.fromBucketName(stack, 'bucket', 'logging-bucket'); +const bucket = s3.Bucket.fromBucketName(this, 'bucket', 'logging-bucket'); const cluster = new Cluster(this, 'Redshift', { masterUser: { @@ -62,7 +62,7 @@ const cluster = new Cluster(this, 'Redshift', { }, vpc, loggingProperties: { - loggingBucket = bucket, + loggingBucket: bucket, loggingKeyPrefix: 'prefix', } }); @@ -200,6 +200,20 @@ new Table(this, 'Table', { }); ``` +Tables can also be configured with a comment: + +```ts fixture=cluster +new Table(this, 'Table', { + tableColumns: [ + { name: 'col1', dataType: 'varchar(4)' }, + { name: 'col2', dataType: 'float' } + ], + cluster: cluster, + databaseName: 'databaseName', + comment: 'This is a comment', +}); +``` + ### Granting Privileges You can give a user privileges to perform certain actions on a table by using the @@ -305,7 +319,9 @@ cluster.addRotationMultiUser('MultiUserRotation', { You can add a parameter to a parameter group with`ClusterParameterGroup.addParameter()`. ```ts -const params = new ClusterParameterGroup(stack, 'Params', { +import { ClusterParameterGroup } from '@aws-cdk/aws-redshift'; + +const params = new ClusterParameterGroup(this, 'Params', { description: 'desc', parameters: { require_ssl: 'true', @@ -318,6 +334,8 @@ params.addParameter('enable_user_activity_logging', 'true'); Additionally, you can add a parameter to the cluster's associated parameter group with `Cluster.addToParameterGroup()`. If the cluster does not have an associated parameter group, a new parameter group is created. ```ts +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; declare const vpc: ec2.Vpc; const cluster = new Cluster(this, 'Cluster', { @@ -336,9 +354,11 @@ cluster.addToParameterGroup('enable_user_activity_logging', 'true'); If you configure your cluster to be publicly accessible, you can optionally select an *elastic IP address* to use for the external IP address. An elastic IP address is a static IP address that is associated with your AWS account. You can use an elastic IP address to connect to your cluster from outside the VPC. An elastic IP address gives you the ability to change your underlying configuration without affecting the IP address that clients use to connect to your cluster. This approach can be helpful for situations such as recovery after a failure. ```ts +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; declare const vpc: ec2.Vpc; -new Cluster(stack, 'Redshift', { +new Cluster(this, 'Redshift', { masterUser: { masterUsername: 'admin', masterPassword: cdk.SecretValue.unsafePlainText('tooshort'), @@ -352,6 +372,7 @@ new Cluster(stack, 'Redshift', { If the Cluster is in a VPC and you want to connect to it using the private IP address from within the cluster, it is important to enable *DNS resolution* and *DNS hostnames* in the VPC config. If these parameters would not be set, connections from within the VPC would connect to the elastic IP address and not the private IP address. ```ts +import * as ec2 from '@aws-cdk/aws-ec2'; const vpc = new ec2.Vpc(this, 'VPC', { enableDnsSupport: true, enableDnsHostnames: true, @@ -373,9 +394,11 @@ In some cases, you might want to associate the cluster with an elastic IP addres When you use Amazon Redshift enhanced VPC routing, Amazon Redshift forces all COPY and UNLOAD traffic between your cluster and your data repositories through your virtual private cloud (VPC) based on the Amazon VPC service. By using enhanced VPC routing, you can use standard VPC features, such as VPC security groups, network access control lists (ACLs), VPC endpoints, VPC endpoint policies, internet gateways, and Domain Name System (DNS) servers, as described in the Amazon VPC User Guide. You use these features to tightly manage the flow of data between your Amazon Redshift cluster and other resources. When you use enhanced VPC routing to route traffic through your VPC, you can also use VPC flow logs to monitor COPY and UNLOAD traffic. ```ts +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; declare const vpc: ec2.Vpc; -new Cluster(stack, 'Redshift', { +new Cluster(this, 'Redshift', { masterUser: { masterUsername: 'admin', masterPassword: cdk.SecretValue.unsafePlainText('tooshort'), diff --git a/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/table.ts b/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/table.ts index ce59679f23589..15b206c12c654 100644 --- a/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/table.ts +++ b/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/table.ts @@ -60,6 +60,11 @@ async function createTable( } await executeStatement(statement, tableAndClusterProps); + + if (tableAndClusterProps.tableComment) { + await executeStatement(`COMMENT ON TABLE ${tableName} IS '${tableAndClusterProps.tableComment}'`, tableAndClusterProps); + } + return tableName; } @@ -143,6 +148,12 @@ async function updateTable( } } + const oldComment = oldResourceProperties.tableComment; + const newComment = tableAndClusterProps.tableComment; + if (oldComment !== newComment) { + alterationStatements.push(`COMMENT ON TABLE ${tableName} IS ${newComment ? `'${newComment}'` : 'NULL'}`); + } + await Promise.all(alterationStatements.map(statement => executeStatement(statement, tableAndClusterProps))); return tableName; diff --git a/packages/@aws-cdk/aws-redshift/lib/private/handler-props.ts b/packages/@aws-cdk/aws-redshift/lib/private/handler-props.ts index 97089078f00a2..5937f9dc3009d 100644 --- a/packages/@aws-cdk/aws-redshift/lib/private/handler-props.ts +++ b/packages/@aws-cdk/aws-redshift/lib/private/handler-props.ts @@ -20,6 +20,7 @@ export interface TableHandlerProps { readonly tableColumns: Column[]; readonly distStyle?: TableDistStyle; readonly sortStyle: TableSortStyle; + readonly tableComment?: string; } export interface TablePrivilege { diff --git a/packages/@aws-cdk/aws-redshift/lib/table.ts b/packages/@aws-cdk/aws-redshift/lib/table.ts index 168fd06b5989c..147e50fa1248b 100644 --- a/packages/@aws-cdk/aws-redshift/lib/table.ts +++ b/packages/@aws-cdk/aws-redshift/lib/table.ts @@ -117,6 +117,13 @@ export interface TableProps extends DatabaseOptions { * @default cdk.RemovalPolicy.Retain */ readonly removalPolicy?: cdk.RemovalPolicy; + + /** + * A comment to attach to the table. + * + * @default - no comment + */ + readonly tableComment?: string; } /** @@ -234,6 +241,7 @@ export class Table extends TableBase { tableColumns: this.tableColumns, distStyle: props.distStyle, sortStyle: props.sortStyle ?? this.getDefaultSortStyle(props.tableColumns), + tableComment: props.tableComment, }, }); diff --git a/packages/@aws-cdk/aws-redshift/test/database-query-provider/table.test.ts b/packages/@aws-cdk/aws-redshift/test/database-query-provider/table.test.ts index 4839416fc8fa0..96c1adebf8a1d 100644 --- a/packages/@aws-cdk/aws-redshift/test/database-query-provider/table.test.ts +++ b/packages/@aws-cdk/aws-redshift/test/database-query-provider/table.test.ts @@ -134,6 +134,20 @@ describe('create', () => { Sql: `CREATE TABLE ${tableNamePrefix}${requestIdTruncated} (col1 varchar(4),col2 float,col3 float) DISTSTYLE KEY DISTKEY(col1) COMPOUND SORTKEY(col2,col3)`, })); }); + + test('serializes table comment in statement', async () => { + const event = baseEvent; + const newResourceProperties: ResourcePropertiesType = { + ...resourceProperties, + tableComment: 'table comment', + }; + + await manageTable(newResourceProperties, event); + + expect(mockExecuteStatement).toHaveBeenCalledWith(expect.objectContaining({ + Sql: `COMMENT ON TABLE ${tableNamePrefix}${requestIdTruncated} IS 'table comment'`, + })); + }); }); describe('delete', () => { @@ -502,4 +516,40 @@ describe('update', () => { }); }); + describe('table comment', () => { + test('does not replace if comment added on table', async () => { + const newComment = 'newComment'; + const newResourceProperties = { + ...resourceProperties, + tableComment: newComment, + }; + + await expect(manageTable(newResourceProperties, event)).resolves.toMatchObject({ + PhysicalResourceId: physicalResourceId, + }); + expect(mockExecuteStatement).toHaveBeenCalledWith(expect.objectContaining({ + Sql: `COMMENT ON TABLE ${physicalResourceId} IS '${newComment}'`, + })); + }); + + test('does not replace if comment removed on table', async () => { + const newEvent = { + ...event, + OldResourceProperties: { + ...event.OldResourceProperties, + tableComment: 'oldComment', + }, + }; + const newResourceProperties = { + ...resourceProperties, + }; + + await expect(manageTable(newResourceProperties, newEvent)).resolves.toMatchObject({ + PhysicalResourceId: physicalResourceId, + }); + expect(mockExecuteStatement).toHaveBeenCalledWith(expect.objectContaining({ + Sql: `COMMENT ON TABLE ${physicalResourceId} IS NULL`, + })); + }); + }); }); diff --git a/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.8359857aa9b7c3fbc8bba9a505282ba848915383c4549f21b9f93f9f35b56415/table.js b/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.8359857aa9b7c3fbc8bba9a505282ba848915383c4549f21b9f93f9f35b56415/table.js deleted file mode 100644 index 16942fad1cb3d..0000000000000 --- a/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.8359857aa9b7c3fbc8bba9a505282ba848915383c4549f21b9f93f9f35b56415/table.js +++ /dev/null @@ -1,117 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.handler = void 0; -const redshift_data_1 = require("./redshift-data"); -const types_1 = require("./types"); -const util_1 = require("./util"); -async function handler(props, event) { - const tableNamePrefix = props.tableName.prefix; - const tableNameSuffix = props.tableName.generateSuffix === 'true' ? `${event.RequestId.substring(0, 8)}` : ''; - const tableColumns = props.tableColumns; - const tableAndClusterProps = props; - if (event.RequestType === 'Create') { - const tableName = await createTable(tableNamePrefix, tableNameSuffix, tableColumns, tableAndClusterProps); - return { PhysicalResourceId: tableName }; - } - else if (event.RequestType === 'Delete') { - await dropTable(event.PhysicalResourceId, tableAndClusterProps); - return; - } - else if (event.RequestType === 'Update') { - const tableName = await updateTable(event.PhysicalResourceId, tableNamePrefix, tableNameSuffix, tableColumns, tableAndClusterProps, event.OldResourceProperties); - return { PhysicalResourceId: tableName }; - } - else { - /* eslint-disable-next-line dot-notation */ - throw new Error(`Unrecognized event type: ${event['RequestType']}`); - } -} -exports.handler = handler; -async function createTable(tableNamePrefix, tableNameSuffix, tableColumns, tableAndClusterProps) { - const tableName = tableNamePrefix + tableNameSuffix; - const tableColumnsString = tableColumns.map(column => `${column.name} ${column.dataType}`).join(); - let statement = `CREATE TABLE ${tableName} (${tableColumnsString})`; - if (tableAndClusterProps.distStyle) { - statement += ` DISTSTYLE ${tableAndClusterProps.distStyle}`; - } - const distKeyColumn = util_1.getDistKeyColumn(tableColumns); - if (distKeyColumn) { - statement += ` DISTKEY(${distKeyColumn.name})`; - } - const sortKeyColumns = util_1.getSortKeyColumns(tableColumns); - if (sortKeyColumns.length > 0) { - const sortKeyColumnsString = getSortKeyColumnsString(sortKeyColumns); - statement += ` ${tableAndClusterProps.sortStyle} SORTKEY(${sortKeyColumnsString})`; - } - await redshift_data_1.executeStatement(statement, tableAndClusterProps); - return tableName; -} -async function dropTable(tableName, clusterProps) { - await redshift_data_1.executeStatement(`DROP TABLE ${tableName}`, clusterProps); -} -async function updateTable(tableName, tableNamePrefix, tableNameSuffix, tableColumns, tableAndClusterProps, oldResourceProperties) { - const alterationStatements = []; - const oldClusterProps = oldResourceProperties; - if (tableAndClusterProps.clusterName !== oldClusterProps.clusterName || tableAndClusterProps.databaseName !== oldClusterProps.databaseName) { - return createTable(tableNamePrefix, tableNameSuffix, tableColumns, tableAndClusterProps); - } - const oldTableNamePrefix = oldResourceProperties.tableName.prefix; - if (tableNamePrefix !== oldTableNamePrefix) { - return createTable(tableNamePrefix, tableNameSuffix, tableColumns, tableAndClusterProps); - } - const oldTableColumns = oldResourceProperties.tableColumns; - const columnDeletions = oldTableColumns.filter(oldColumn => (tableColumns.every(column => oldColumn.name !== column.name))); - if (columnDeletions.length > 0) { - alterationStatements.push(...columnDeletions.map(column => `ALTER TABLE ${tableName} DROP COLUMN ${column.name}`)); - } - const columnAdditions = tableColumns.filter(column => { - return !oldTableColumns.some(oldColumn => column.name === oldColumn.name && column.dataType === oldColumn.dataType); - }).map(column => `ADD ${column.name} ${column.dataType}`); - if (columnAdditions.length > 0) { - alterationStatements.push(...columnAdditions.map(addition => `ALTER TABLE ${tableName} ${addition}`)); - } - const oldDistStyle = oldResourceProperties.distStyle; - if ((!oldDistStyle && tableAndClusterProps.distStyle) || - (oldDistStyle && !tableAndClusterProps.distStyle)) { - return createTable(tableNamePrefix, tableNameSuffix, tableColumns, tableAndClusterProps); - } - else if (oldDistStyle !== tableAndClusterProps.distStyle) { - alterationStatements.push(`ALTER TABLE ${tableName} ALTER DISTSTYLE ${tableAndClusterProps.distStyle}`); - } - const oldDistKey = util_1.getDistKeyColumn(oldTableColumns)?.name; - const newDistKey = util_1.getDistKeyColumn(tableColumns)?.name; - if ((!oldDistKey && newDistKey) || (oldDistKey && !newDistKey)) { - return createTable(tableNamePrefix, tableNameSuffix, tableColumns, tableAndClusterProps); - } - else if (oldDistKey !== newDistKey) { - alterationStatements.push(`ALTER TABLE ${tableName} ALTER DISTKEY ${newDistKey}`); - } - const oldSortKeyColumns = util_1.getSortKeyColumns(oldTableColumns); - const newSortKeyColumns = util_1.getSortKeyColumns(tableColumns); - const oldSortStyle = oldResourceProperties.sortStyle; - const newSortStyle = tableAndClusterProps.sortStyle; - if ((oldSortStyle === newSortStyle && !util_1.areColumnsEqual(oldSortKeyColumns, newSortKeyColumns)) - || (oldSortStyle !== newSortStyle)) { - switch (newSortStyle) { - case types_1.TableSortStyle.INTERLEAVED: - // INTERLEAVED sort key addition requires replacement. - // https://docs.aws.amazon.com/redshift/latest/dg/r_ALTER_TABLE.html - return createTable(tableNamePrefix, tableNameSuffix, tableColumns, tableAndClusterProps); - case types_1.TableSortStyle.COMPOUND: { - const sortKeyColumnsString = getSortKeyColumnsString(newSortKeyColumns); - alterationStatements.push(`ALTER TABLE ${tableName} ALTER ${newSortStyle} SORTKEY(${sortKeyColumnsString})`); - break; - } - case types_1.TableSortStyle.AUTO: { - alterationStatements.push(`ALTER TABLE ${tableName} ALTER SORTKEY ${newSortStyle}`); - break; - } - } - } - await Promise.all(alterationStatements.map(statement => redshift_data_1.executeStatement(statement, tableAndClusterProps))); - return tableName; -} -function getSortKeyColumnsString(sortKeyColumns) { - return sortKeyColumns.map(column => column.name).join(); -} -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGFibGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJ0YWJsZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFHQSxtREFBbUQ7QUFDbkQsbUNBQTZFO0FBQzdFLGlDQUE4RTtBQUV2RSxLQUFLLFVBQVUsT0FBTyxDQUFDLEtBQTJCLEVBQUUsS0FBa0Q7SUFDM0csTUFBTSxlQUFlLEdBQUcsS0FBSyxDQUFDLFNBQVMsQ0FBQyxNQUFNLENBQUM7SUFDL0MsTUFBTSxlQUFlLEdBQUcsS0FBSyxDQUFDLFNBQVMsQ0FBQyxjQUFjLEtBQUssTUFBTSxDQUFDLENBQUMsQ0FBQyxHQUFHLEtBQUssQ0FBQyxTQUFTLENBQUMsU0FBUyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7SUFDOUcsTUFBTSxZQUFZLEdBQUcsS0FBSyxDQUFDLFlBQVksQ0FBQztJQUN4QyxNQUFNLG9CQUFvQixHQUFHLEtBQUssQ0FBQztJQUVuQyxJQUFJLEtBQUssQ0FBQyxXQUFXLEtBQUssUUFBUSxFQUFFO1FBQ2xDLE1BQU0sU0FBUyxHQUFHLE1BQU0sV0FBVyxDQUFDLGVBQWUsRUFBRSxlQUFlLEVBQUUsWUFBWSxFQUFFLG9CQUFvQixDQUFDLENBQUM7UUFDMUcsT0FBTyxFQUFFLGtCQUFrQixFQUFFLFNBQVMsRUFBRSxDQUFDO0tBQzFDO1NBQU0sSUFBSSxLQUFLLENBQUMsV0FBVyxLQUFLLFFBQVEsRUFBRTtRQUN6QyxNQUFNLFNBQVMsQ0FBQyxLQUFLLENBQUMsa0JBQWtCLEVBQUUsb0JBQW9CLENBQUMsQ0FBQztRQUNoRSxPQUFPO0tBQ1I7U0FBTSxJQUFJLEtBQUssQ0FBQyxXQUFXLEtBQUssUUFBUSxFQUFFO1FBQ3pDLE1BQU0sU0FBUyxHQUFHLE1BQU0sV0FBVyxDQUNqQyxLQUFLLENBQUMsa0JBQWtCLEVBQ3hCLGVBQWUsRUFDZixlQUFlLEVBQ2YsWUFBWSxFQUNaLG9CQUFvQixFQUNwQixLQUFLLENBQUMscUJBQTZDLENBQ3BELENBQUM7UUFDRixPQUFPLEVBQUUsa0JBQWtCLEVBQUUsU0FBUyxFQUFFLENBQUM7S0FDMUM7U0FBTTtRQUNMLDJDQUEyQztRQUMzQyxNQUFNLElBQUksS0FBSyxDQUFDLDRCQUE0QixLQUFLLENBQUMsYUFBYSxDQUFDLEVBQUUsQ0FBQyxDQUFDO0tBQ3JFO0FBQ0gsQ0FBQztBQTFCRCwwQkEwQkM7QUFFRCxLQUFLLFVBQVUsV0FBVyxDQUN4QixlQUF1QixFQUN2QixlQUF1QixFQUN2QixZQUFzQixFQUN0QixvQkFBMEM7SUFFMUMsTUFBTSxTQUFTLEdBQUcsZUFBZSxHQUFHLGVBQWUsQ0FBQztJQUNwRCxNQUFNLGtCQUFrQixHQUFHLFlBQVksQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxJQUFJLElBQUksTUFBTSxDQUFDLFFBQVEsRUFBRSxDQUFDLENBQUMsSUFBSSxFQUFFLENBQUM7SUFFbEcsSUFBSSxTQUFTLEdBQUcsZ0JBQWdCLFNBQVMsS0FBSyxrQkFBa0IsR0FBRyxDQUFDO0lBRXBFLElBQUksb0JBQW9CLENBQUMsU0FBUyxFQUFFO1FBQ2xDLFNBQVMsSUFBSSxjQUFjLG9CQUFvQixDQUFDLFNBQVMsRUFBRSxDQUFDO0tBQzdEO0lBRUQsTUFBTSxhQUFhLEdBQUcsdUJBQWdCLENBQUMsWUFBWSxDQUFDLENBQUM7SUFDckQsSUFBSSxhQUFhLEVBQUU7UUFDakIsU0FBUyxJQUFJLFlBQVksYUFBYSxDQUFDLElBQUksR0FBRyxDQUFDO0tBQ2hEO0lBRUQsTUFBTSxjQUFjLEdBQUcsd0JBQWlCLENBQUMsWUFBWSxDQUFDLENBQUM7SUFDdkQsSUFBSSxjQUFjLENBQUMsTUFBTSxHQUFHLENBQUMsRUFBRTtRQUM3QixNQUFNLG9CQUFvQixHQUFHLHVCQUF1QixDQUFDLGNBQWMsQ0FBQyxDQUFDO1FBQ3JFLFNBQVMsSUFBSSxJQUFJLG9CQUFvQixDQUFDLFNBQVMsWUFBWSxvQkFBb0IsR0FBRyxDQUFDO0tBQ3BGO0lBRUQsTUFBTSxnQ0FBZ0IsQ0FBQyxTQUFTLEVBQUUsb0JBQW9CLENBQUMsQ0FBQztJQUN4RCxPQUFPLFNBQVMsQ0FBQztBQUNuQixDQUFDO0FBRUQsS0FBSyxVQUFVLFNBQVMsQ0FBQyxTQUFpQixFQUFFLFlBQTBCO0lBQ3BFLE1BQU0sZ0NBQWdCLENBQUMsY0FBYyxTQUFTLEVBQUUsRUFBRSxZQUFZLENBQUMsQ0FBQztBQUNsRSxDQUFDO0FBRUQsS0FBSyxVQUFVLFdBQVcsQ0FDeEIsU0FBaUIsRUFDakIsZUFBdUIsRUFDdkIsZUFBdUIsRUFDdkIsWUFBc0IsRUFDdEIsb0JBQTBDLEVBQzFDLHFCQUEyQztJQUUzQyxNQUFNLG9CQUFvQixHQUFhLEVBQUUsQ0FBQztJQUUxQyxNQUFNLGVBQWUsR0FBRyxxQkFBcUIsQ0FBQztJQUM5QyxJQUFJLG9CQUFvQixDQUFDLFdBQVcsS0FBSyxlQUFlLENBQUMsV0FBVyxJQUFJLG9CQUFvQixDQUFDLFlBQVksS0FBSyxlQUFlLENBQUMsWUFBWSxFQUFFO1FBQzFJLE9BQU8sV0FBVyxDQUFDLGVBQWUsRUFBRSxlQUFlLEVBQUUsWUFBWSxFQUFFLG9CQUFvQixDQUFDLENBQUM7S0FDMUY7SUFFRCxNQUFNLGtCQUFrQixHQUFHLHFCQUFxQixDQUFDLFNBQVMsQ0FBQyxNQUFNLENBQUM7SUFDbEUsSUFBSSxlQUFlLEtBQUssa0JBQWtCLEVBQUU7UUFDMUMsT0FBTyxXQUFXLENBQUMsZUFBZSxFQUFFLGVBQWUsRUFBRSxZQUFZLEVBQUUsb0JBQW9CLENBQUMsQ0FBQztLQUMxRjtJQUVELE1BQU0sZUFBZSxHQUFHLHFCQUFxQixDQUFDLFlBQVksQ0FBQztJQUMzRCxNQUFNLGVBQWUsR0FBRyxlQUFlLENBQUMsTUFBTSxDQUFDLFNBQVMsQ0FBQyxFQUFFLENBQUMsQ0FDMUQsWUFBWSxDQUFDLEtBQUssQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDLFNBQVMsQ0FBQyxJQUFJLEtBQUssTUFBTSxDQUFDLElBQUksQ0FBQyxDQUM3RCxDQUFDLENBQUM7SUFDSCxJQUFJLGVBQWUsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxFQUFFO1FBQzlCLG9CQUFvQixDQUFDLElBQUksQ0FBQyxHQUFHLGVBQWUsQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQyxlQUFlLFNBQVMsZ0JBQWdCLE1BQU0sQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUM7S0FDcEg7SUFFRCxNQUFNLGVBQWUsR0FBRyxZQUFZLENBQUMsTUFBTSxDQUFDLE1BQU0sQ0FBQyxFQUFFO1FBQ25ELE9BQU8sQ0FBQyxlQUFlLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxFQUFFLENBQUMsTUFBTSxDQUFDLElBQUksS0FBSyxTQUFTLENBQUMsSUFBSSxJQUFJLE1BQU0sQ0FBQyxRQUFRLEtBQUssU0FBUyxDQUFDLFFBQVEsQ0FBQyxDQUFDO0lBQ3RILENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDLE9BQU8sTUFBTSxDQUFDLElBQUksSUFBSSxNQUFNLENBQUMsUUFBUSxFQUFFLENBQUMsQ0FBQztJQUMxRCxJQUFJLGVBQWUsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxFQUFFO1FBQzlCLG9CQUFvQixDQUFDLElBQUksQ0FBQyxHQUFHLGVBQWUsQ0FBQyxHQUFHLENBQUMsUUFBUSxDQUFDLEVBQUUsQ0FBQyxlQUFlLFNBQVMsSUFBSSxRQUFRLEVBQUUsQ0FBQyxDQUFDLENBQUM7S0FDdkc7SUFFRCxNQUFNLFlBQVksR0FBRyxxQkFBcUIsQ0FBQyxTQUFTLENBQUM7SUFDckQsSUFBSSxDQUFDLENBQUMsWUFBWSxJQUFJLG9CQUFvQixDQUFDLFNBQVMsQ0FBQztRQUNuRCxDQUFDLFlBQVksSUFBSSxDQUFDLG9CQUFvQixDQUFDLFNBQVMsQ0FBQyxFQUFFO1FBQ25ELE9BQU8sV0FBVyxDQUFDLGVBQWUsRUFBRSxlQUFlLEVBQUUsWUFBWSxFQUFFLG9CQUFvQixDQUFDLENBQUM7S0FDMUY7U0FBTSxJQUFJLFlBQVksS0FBSyxvQkFBb0IsQ0FBQyxTQUFTLEVBQUU7UUFDMUQsb0JBQW9CLENBQUMsSUFBSSxDQUFDLGVBQWUsU0FBUyxvQkFBb0Isb0JBQW9CLENBQUMsU0FBUyxFQUFFLENBQUMsQ0FBQztLQUN6RztJQUVELE1BQU0sVUFBVSxHQUFHLHVCQUFnQixDQUFDLGVBQWUsQ0FBQyxFQUFFLElBQUksQ0FBQztJQUMzRCxNQUFNLFVBQVUsR0FBRyx1QkFBZ0IsQ0FBQyxZQUFZLENBQUMsRUFBRSxJQUFJLENBQUM7SUFDeEQsSUFBSSxDQUFDLENBQUMsVUFBVSxJQUFJLFVBQVUsQ0FBRSxJQUFJLENBQUMsVUFBVSxJQUFJLENBQUMsVUFBVSxDQUFDLEVBQUU7UUFDL0QsT0FBTyxXQUFXLENBQUMsZUFBZSxFQUFFLGVBQWUsRUFBRSxZQUFZLEVBQUUsb0JBQW9CLENBQUMsQ0FBQztLQUMxRjtTQUFNLElBQUksVUFBVSxLQUFLLFVBQVUsRUFBRTtRQUNwQyxvQkFBb0IsQ0FBQyxJQUFJLENBQUMsZUFBZSxTQUFTLGtCQUFrQixVQUFVLEVBQUUsQ0FBQyxDQUFDO0tBQ25GO0lBRUQsTUFBTSxpQkFBaUIsR0FBRyx3QkFBaUIsQ0FBQyxlQUFlLENBQUMsQ0FBQztJQUM3RCxNQUFNLGlCQUFpQixHQUFHLHdCQUFpQixDQUFDLFlBQVksQ0FBQyxDQUFDO0lBQzFELE1BQU0sWUFBWSxHQUFHLHFCQUFxQixDQUFDLFNBQVMsQ0FBQztJQUNyRCxNQUFNLFlBQVksR0FBRyxvQkFBb0IsQ0FBQyxTQUFTLENBQUM7SUFDcEQsSUFBSSxDQUFDLFlBQVksS0FBSyxZQUFZLElBQUksQ0FBQyxzQkFBZSxDQUFDLGlCQUFpQixFQUFFLGlCQUFpQixDQUFDLENBQUM7V0FDeEYsQ0FBQyxZQUFZLEtBQUssWUFBWSxDQUFDLEVBQUU7UUFDcEMsUUFBUSxZQUFZLEVBQUU7WUFDcEIsS0FBSyxzQkFBYyxDQUFDLFdBQVc7Z0JBQzdCLHNEQUFzRDtnQkFDdEQsb0VBQW9FO2dCQUNwRSxPQUFPLFdBQVcsQ0FBQyxlQUFlLEVBQUUsZUFBZSxFQUFFLFlBQVksRUFBRSxvQkFBb0IsQ0FBQyxDQUFDO1lBRTNGLEtBQUssc0JBQWMsQ0FBQyxRQUFRLENBQUMsQ0FBQztnQkFDNUIsTUFBTSxvQkFBb0IsR0FBRyx1QkFBdUIsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO2dCQUN4RSxvQkFBb0IsQ0FBQyxJQUFJLENBQUMsZUFBZSxTQUFTLFVBQVUsWUFBWSxZQUFZLG9CQUFvQixHQUFHLENBQUMsQ0FBQztnQkFDN0csTUFBTTthQUNQO1lBRUQsS0FBSyxzQkFBYyxDQUFDLElBQUksQ0FBQyxDQUFDO2dCQUN4QixvQkFBb0IsQ0FBQyxJQUFJLENBQUMsZUFBZSxTQUFTLGtCQUFrQixZQUFZLEVBQUUsQ0FBQyxDQUFDO2dCQUNwRixNQUFNO2FBQ1A7U0FDRjtLQUNGO0lBRUQsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLG9CQUFvQixDQUFDLEdBQUcsQ0FBQyxTQUFTLENBQUMsRUFBRSxDQUFDLGdDQUFnQixDQUFDLFNBQVMsRUFBRSxvQkFBb0IsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUU1RyxPQUFPLFNBQVMsQ0FBQztBQUNuQixDQUFDO0FBRUQsU0FBUyx1QkFBdUIsQ0FBQyxjQUF3QjtJQUN2RCxPQUFPLGNBQWMsQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUMsSUFBSSxFQUFFLENBQUM7QUFDMUQsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBpbXBvcnQvbm8tdW5yZXNvbHZlZCAqL1xuaW1wb3J0ICogYXMgQVdTTGFtYmRhIGZyb20gJ2F3cy1sYW1iZGEnO1xuaW1wb3J0IHsgQ29sdW1uIH0gZnJvbSAnLi4vLi4vdGFibGUnO1xuaW1wb3J0IHsgZXhlY3V0ZVN0YXRlbWVudCB9IGZyb20gJy4vcmVkc2hpZnQtZGF0YSc7XG5pbXBvcnQgeyBDbHVzdGVyUHJvcHMsIFRhYmxlQW5kQ2x1c3RlclByb3BzLCBUYWJsZVNvcnRTdHlsZSB9IGZyb20gJy4vdHlwZXMnO1xuaW1wb3J0IHsgYXJlQ29sdW1uc0VxdWFsLCBnZXREaXN0S2V5Q29sdW1uLCBnZXRTb3J0S2V5Q29sdW1ucyB9IGZyb20gJy4vdXRpbCc7XG5cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBoYW5kbGVyKHByb3BzOiBUYWJsZUFuZENsdXN0ZXJQcm9wcywgZXZlbnQ6IEFXU0xhbWJkYS5DbG91ZEZvcm1hdGlvbkN1c3RvbVJlc291cmNlRXZlbnQpIHtcbiAgY29uc3QgdGFibGVOYW1lUHJlZml4ID0gcHJvcHMudGFibGVOYW1lLnByZWZpeDtcbiAgY29uc3QgdGFibGVOYW1lU3VmZml4ID0gcHJvcHMudGFibGVOYW1lLmdlbmVyYXRlU3VmZml4ID09PSAndHJ1ZScgPyBgJHtldmVudC5SZXF1ZXN0SWQuc3Vic3RyaW5nKDAsIDgpfWAgOiAnJztcbiAgY29uc3QgdGFibGVDb2x1bW5zID0gcHJvcHMudGFibGVDb2x1bW5zO1xuICBjb25zdCB0YWJsZUFuZENsdXN0ZXJQcm9wcyA9IHByb3BzO1xuXG4gIGlmIChldmVudC5SZXF1ZXN0VHlwZSA9PT0gJ0NyZWF0ZScpIHtcbiAgICBjb25zdCB0YWJsZU5hbWUgPSBhd2FpdCBjcmVhdGVUYWJsZSh0YWJsZU5hbWVQcmVmaXgsIHRhYmxlTmFtZVN1ZmZpeCwgdGFibGVDb2x1bW5zLCB0YWJsZUFuZENsdXN0ZXJQcm9wcyk7XG4gICAgcmV0dXJuIHsgUGh5c2ljYWxSZXNvdXJjZUlkOiB0YWJsZU5hbWUgfTtcbiAgfSBlbHNlIGlmIChldmVudC5SZXF1ZXN0VHlwZSA9PT0gJ0RlbGV0ZScpIHtcbiAgICBhd2FpdCBkcm9wVGFibGUoZXZlbnQuUGh5c2ljYWxSZXNvdXJjZUlkLCB0YWJsZUFuZENsdXN0ZXJQcm9wcyk7XG4gICAgcmV0dXJuO1xuICB9IGVsc2UgaWYgKGV2ZW50LlJlcXVlc3RUeXBlID09PSAnVXBkYXRlJykge1xuICAgIGNvbnN0IHRhYmxlTmFtZSA9IGF3YWl0IHVwZGF0ZVRhYmxlKFxuICAgICAgZXZlbnQuUGh5c2ljYWxSZXNvdXJjZUlkLFxuICAgICAgdGFibGVOYW1lUHJlZml4LFxuICAgICAgdGFibGVOYW1lU3VmZml4LFxuICAgICAgdGFibGVDb2x1bW5zLFxuICAgICAgdGFibGVBbmRDbHVzdGVyUHJvcHMsXG4gICAgICBldmVudC5PbGRSZXNvdXJjZVByb3BlcnRpZXMgYXMgVGFibGVBbmRDbHVzdGVyUHJvcHMsXG4gICAgKTtcbiAgICByZXR1cm4geyBQaHlzaWNhbFJlc291cmNlSWQ6IHRhYmxlTmFtZSB9O1xuICB9IGVsc2Uge1xuICAgIC8qIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBkb3Qtbm90YXRpb24gKi9cbiAgICB0aHJvdyBuZXcgRXJyb3IoYFVucmVjb2duaXplZCBldmVudCB0eXBlOiAke2V2ZW50WydSZXF1ZXN0VHlwZSddfWApO1xuICB9XG59XG5cbmFzeW5jIGZ1bmN0aW9uIGNyZWF0ZVRhYmxlKFxuICB0YWJsZU5hbWVQcmVmaXg6IHN0cmluZyxcbiAgdGFibGVOYW1lU3VmZml4OiBzdHJpbmcsXG4gIHRhYmxlQ29sdW1uczogQ29sdW1uW10sXG4gIHRhYmxlQW5kQ2x1c3RlclByb3BzOiBUYWJsZUFuZENsdXN0ZXJQcm9wcyxcbik6IFByb21pc2U8c3RyaW5nPiB7XG4gIGNvbnN0IHRhYmxlTmFtZSA9IHRhYmxlTmFtZVByZWZpeCArIHRhYmxlTmFtZVN1ZmZpeDtcbiAgY29uc3QgdGFibGVDb2x1bW5zU3RyaW5nID0gdGFibGVDb2x1bW5zLm1hcChjb2x1bW4gPT4gYCR7Y29sdW1uLm5hbWV9ICR7Y29sdW1uLmRhdGFUeXBlfWApLmpvaW4oKTtcblxuICBsZXQgc3RhdGVtZW50ID0gYENSRUFURSBUQUJMRSAke3RhYmxlTmFtZX0gKCR7dGFibGVDb2x1bW5zU3RyaW5nfSlgO1xuXG4gIGlmICh0YWJsZUFuZENsdXN0ZXJQcm9wcy5kaXN0U3R5bGUpIHtcbiAgICBzdGF0ZW1lbnQgKz0gYCBESVNUU1RZTEUgJHt0YWJsZUFuZENsdXN0ZXJQcm9wcy5kaXN0U3R5bGV9YDtcbiAgfVxuXG4gIGNvbnN0IGRpc3RLZXlDb2x1bW4gPSBnZXREaXN0S2V5Q29sdW1uKHRhYmxlQ29sdW1ucyk7XG4gIGlmIChkaXN0S2V5Q29sdW1uKSB7XG4gICAgc3RhdGVtZW50ICs9IGAgRElTVEtFWSgke2Rpc3RLZXlDb2x1bW4ubmFtZX0pYDtcbiAgfVxuXG4gIGNvbnN0IHNvcnRLZXlDb2x1bW5zID0gZ2V0U29ydEtleUNvbHVtbnModGFibGVDb2x1bW5zKTtcbiAgaWYgKHNvcnRLZXlDb2x1bW5zLmxlbmd0aCA+IDApIHtcbiAgICBjb25zdCBzb3J0S2V5Q29sdW1uc1N0cmluZyA9IGdldFNvcnRLZXlDb2x1bW5zU3RyaW5nKHNvcnRLZXlDb2x1bW5zKTtcbiAgICBzdGF0ZW1lbnQgKz0gYCAke3RhYmxlQW5kQ2x1c3RlclByb3BzLnNvcnRTdHlsZX0gU09SVEtFWSgke3NvcnRLZXlDb2x1bW5zU3RyaW5nfSlgO1xuICB9XG5cbiAgYXdhaXQgZXhlY3V0ZVN0YXRlbWVudChzdGF0ZW1lbnQsIHRhYmxlQW5kQ2x1c3RlclByb3BzKTtcbiAgcmV0dXJuIHRhYmxlTmFtZTtcbn1cblxuYXN5bmMgZnVuY3Rpb24gZHJvcFRhYmxlKHRhYmxlTmFtZTogc3RyaW5nLCBjbHVzdGVyUHJvcHM6IENsdXN0ZXJQcm9wcykge1xuICBhd2FpdCBleGVjdXRlU3RhdGVtZW50KGBEUk9QIFRBQkxFICR7dGFibGVOYW1lfWAsIGNsdXN0ZXJQcm9wcyk7XG59XG5cbmFzeW5jIGZ1bmN0aW9uIHVwZGF0ZVRhYmxlKFxuICB0YWJsZU5hbWU6IHN0cmluZyxcbiAgdGFibGVOYW1lUHJlZml4OiBzdHJpbmcsXG4gIHRhYmxlTmFtZVN1ZmZpeDogc3RyaW5nLFxuICB0YWJsZUNvbHVtbnM6IENvbHVtbltdLFxuICB0YWJsZUFuZENsdXN0ZXJQcm9wczogVGFibGVBbmRDbHVzdGVyUHJvcHMsXG4gIG9sZFJlc291cmNlUHJvcGVydGllczogVGFibGVBbmRDbHVzdGVyUHJvcHMsXG4pOiBQcm9taXNlPHN0cmluZz4ge1xuICBjb25zdCBhbHRlcmF0aW9uU3RhdGVtZW50czogc3RyaW5nW10gPSBbXTtcblxuICBjb25zdCBvbGRDbHVzdGVyUHJvcHMgPSBvbGRSZXNvdXJjZVByb3BlcnRpZXM7XG4gIGlmICh0YWJsZUFuZENsdXN0ZXJQcm9wcy5jbHVzdGVyTmFtZSAhPT0gb2xkQ2x1c3RlclByb3BzLmNsdXN0ZXJOYW1lIHx8IHRhYmxlQW5kQ2x1c3RlclByb3BzLmRhdGFiYXNlTmFtZSAhPT0gb2xkQ2x1c3RlclByb3BzLmRhdGFiYXNlTmFtZSkge1xuICAgIHJldHVybiBjcmVhdGVUYWJsZSh0YWJsZU5hbWVQcmVmaXgsIHRhYmxlTmFtZVN1ZmZpeCwgdGFibGVDb2x1bW5zLCB0YWJsZUFuZENsdXN0ZXJQcm9wcyk7XG4gIH1cblxuICBjb25zdCBvbGRUYWJsZU5hbWVQcmVmaXggPSBvbGRSZXNvdXJjZVByb3BlcnRpZXMudGFibGVOYW1lLnByZWZpeDtcbiAgaWYgKHRhYmxlTmFtZVByZWZpeCAhPT0gb2xkVGFibGVOYW1lUHJlZml4KSB7XG4gICAgcmV0dXJuIGNyZWF0ZVRhYmxlKHRhYmxlTmFtZVByZWZpeCwgdGFibGVOYW1lU3VmZml4LCB0YWJsZUNvbHVtbnMsIHRhYmxlQW5kQ2x1c3RlclByb3BzKTtcbiAgfVxuXG4gIGNvbnN0IG9sZFRhYmxlQ29sdW1ucyA9IG9sZFJlc291cmNlUHJvcGVydGllcy50YWJsZUNvbHVtbnM7XG4gIGNvbnN0IGNvbHVtbkRlbGV0aW9ucyA9IG9sZFRhYmxlQ29sdW1ucy5maWx0ZXIob2xkQ29sdW1uID0+IChcbiAgICB0YWJsZUNvbHVtbnMuZXZlcnkoY29sdW1uID0+IG9sZENvbHVtbi5uYW1lICE9PSBjb2x1bW4ubmFtZSlcbiAgKSk7XG4gIGlmIChjb2x1bW5EZWxldGlvbnMubGVuZ3RoID4gMCkge1xuICAgIGFsdGVyYXRpb25TdGF0ZW1lbnRzLnB1c2goLi4uY29sdW1uRGVsZXRpb25zLm1hcChjb2x1bW4gPT4gYEFMVEVSIFRBQkxFICR7dGFibGVOYW1lfSBEUk9QIENPTFVNTiAke2NvbHVtbi5uYW1lfWApKTtcbiAgfVxuXG4gIGNvbnN0IGNvbHVtbkFkZGl0aW9ucyA9IHRhYmxlQ29sdW1ucy5maWx0ZXIoY29sdW1uID0+IHtcbiAgICByZXR1cm4gIW9sZFRhYmxlQ29sdW1ucy5zb21lKG9sZENvbHVtbiA9PiBjb2x1bW4ubmFtZSA9PT0gb2xkQ29sdW1uLm5hbWUgJiYgY29sdW1uLmRhdGFUeXBlID09PSBvbGRDb2x1bW4uZGF0YVR5cGUpO1xuICB9KS5tYXAoY29sdW1uID0+IGBBREQgJHtjb2x1bW4ubmFtZX0gJHtjb2x1bW4uZGF0YVR5cGV9YCk7XG4gIGlmIChjb2x1bW5BZGRpdGlvbnMubGVuZ3RoID4gMCkge1xuICAgIGFsdGVyYXRpb25TdGF0ZW1lbnRzLnB1c2goLi4uY29sdW1uQWRkaXRpb25zLm1hcChhZGRpdGlvbiA9PiBgQUxURVIgVEFCTEUgJHt0YWJsZU5hbWV9ICR7YWRkaXRpb259YCkpO1xuICB9XG5cbiAgY29uc3Qgb2xkRGlzdFN0eWxlID0gb2xkUmVzb3VyY2VQcm9wZXJ0aWVzLmRpc3RTdHlsZTtcbiAgaWYgKCghb2xkRGlzdFN0eWxlICYmIHRhYmxlQW5kQ2x1c3RlclByb3BzLmRpc3RTdHlsZSkgfHxcbiAgICAob2xkRGlzdFN0eWxlICYmICF0YWJsZUFuZENsdXN0ZXJQcm9wcy5kaXN0U3R5bGUpKSB7XG4gICAgcmV0dXJuIGNyZWF0ZVRhYmxlKHRhYmxlTmFtZVByZWZpeCwgdGFibGVOYW1lU3VmZml4LCB0YWJsZUNvbHVtbnMsIHRhYmxlQW5kQ2x1c3RlclByb3BzKTtcbiAgfSBlbHNlIGlmIChvbGREaXN0U3R5bGUgIT09IHRhYmxlQW5kQ2x1c3RlclByb3BzLmRpc3RTdHlsZSkge1xuICAgIGFsdGVyYXRpb25TdGF0ZW1lbnRzLnB1c2goYEFMVEVSIFRBQkxFICR7dGFibGVOYW1lfSBBTFRFUiBESVNUU1RZTEUgJHt0YWJsZUFuZENsdXN0ZXJQcm9wcy5kaXN0U3R5bGV9YCk7XG4gIH1cblxuICBjb25zdCBvbGREaXN0S2V5ID0gZ2V0RGlzdEtleUNvbHVtbihvbGRUYWJsZUNvbHVtbnMpPy5uYW1lO1xuICBjb25zdCBuZXdEaXN0S2V5ID0gZ2V0RGlzdEtleUNvbHVtbih0YWJsZUNvbHVtbnMpPy5uYW1lO1xuICBpZiAoKCFvbGREaXN0S2V5ICYmIG5ld0Rpc3RLZXkgKSB8fCAob2xkRGlzdEtleSAmJiAhbmV3RGlzdEtleSkpIHtcbiAgICByZXR1cm4gY3JlYXRlVGFibGUodGFibGVOYW1lUHJlZml4LCB0YWJsZU5hbWVTdWZmaXgsIHRhYmxlQ29sdW1ucywgdGFibGVBbmRDbHVzdGVyUHJvcHMpO1xuICB9IGVsc2UgaWYgKG9sZERpc3RLZXkgIT09IG5ld0Rpc3RLZXkpIHtcbiAgICBhbHRlcmF0aW9uU3RhdGVtZW50cy5wdXNoKGBBTFRFUiBUQUJMRSAke3RhYmxlTmFtZX0gQUxURVIgRElTVEtFWSAke25ld0Rpc3RLZXl9YCk7XG4gIH1cblxuICBjb25zdCBvbGRTb3J0S2V5Q29sdW1ucyA9IGdldFNvcnRLZXlDb2x1bW5zKG9sZFRhYmxlQ29sdW1ucyk7XG4gIGNvbnN0IG5ld1NvcnRLZXlDb2x1bW5zID0gZ2V0U29ydEtleUNvbHVtbnModGFibGVDb2x1bW5zKTtcbiAgY29uc3Qgb2xkU29ydFN0eWxlID0gb2xkUmVzb3VyY2VQcm9wZXJ0aWVzLnNvcnRTdHlsZTtcbiAgY29uc3QgbmV3U29ydFN0eWxlID0gdGFibGVBbmRDbHVzdGVyUHJvcHMuc29ydFN0eWxlO1xuICBpZiAoKG9sZFNvcnRTdHlsZSA9PT0gbmV3U29ydFN0eWxlICYmICFhcmVDb2x1bW5zRXF1YWwob2xkU29ydEtleUNvbHVtbnMsIG5ld1NvcnRLZXlDb2x1bW5zKSlcbiAgICB8fCAob2xkU29ydFN0eWxlICE9PSBuZXdTb3J0U3R5bGUpKSB7XG4gICAgc3dpdGNoIChuZXdTb3J0U3R5bGUpIHtcbiAgICAgIGNhc2UgVGFibGVTb3J0U3R5bGUuSU5URVJMRUFWRUQ6XG4gICAgICAgIC8vIElOVEVSTEVBVkVEIHNvcnQga2V5IGFkZGl0aW9uIHJlcXVpcmVzIHJlcGxhY2VtZW50LlxuICAgICAgICAvLyBodHRwczovL2RvY3MuYXdzLmFtYXpvbi5jb20vcmVkc2hpZnQvbGF0ZXN0L2RnL3JfQUxURVJfVEFCTEUuaHRtbFxuICAgICAgICByZXR1cm4gY3JlYXRlVGFibGUodGFibGVOYW1lUHJlZml4LCB0YWJsZU5hbWVTdWZmaXgsIHRhYmxlQ29sdW1ucywgdGFibGVBbmRDbHVzdGVyUHJvcHMpO1xuXG4gICAgICBjYXNlIFRhYmxlU29ydFN0eWxlLkNPTVBPVU5EOiB7XG4gICAgICAgIGNvbnN0IHNvcnRLZXlDb2x1bW5zU3RyaW5nID0gZ2V0U29ydEtleUNvbHVtbnNTdHJpbmcobmV3U29ydEtleUNvbHVtbnMpO1xuICAgICAgICBhbHRlcmF0aW9uU3RhdGVtZW50cy5wdXNoKGBBTFRFUiBUQUJMRSAke3RhYmxlTmFtZX0gQUxURVIgJHtuZXdTb3J0U3R5bGV9IFNPUlRLRVkoJHtzb3J0S2V5Q29sdW1uc1N0cmluZ30pYCk7XG4gICAgICAgIGJyZWFrO1xuICAgICAgfVxuXG4gICAgICBjYXNlIFRhYmxlU29ydFN0eWxlLkFVVE86IHtcbiAgICAgICAgYWx0ZXJhdGlvblN0YXRlbWVudHMucHVzaChgQUxURVIgVEFCTEUgJHt0YWJsZU5hbWV9IEFMVEVSIFNPUlRLRVkgJHtuZXdTb3J0U3R5bGV9YCk7XG4gICAgICAgIGJyZWFrO1xuICAgICAgfVxuICAgIH1cbiAgfVxuXG4gIGF3YWl0IFByb21pc2UuYWxsKGFsdGVyYXRpb25TdGF0ZW1lbnRzLm1hcChzdGF0ZW1lbnQgPT4gZXhlY3V0ZVN0YXRlbWVudChzdGF0ZW1lbnQsIHRhYmxlQW5kQ2x1c3RlclByb3BzKSkpO1xuXG4gIHJldHVybiB0YWJsZU5hbWU7XG59XG5cbmZ1bmN0aW9uIGdldFNvcnRLZXlDb2x1bW5zU3RyaW5nKHNvcnRLZXlDb2x1bW5zOiBDb2x1bW5bXSkge1xuICByZXR1cm4gc29ydEtleUNvbHVtbnMubWFwKGNvbHVtbiA9PiBjb2x1bW4ubmFtZSkuam9pbigpO1xufVxuIl19 \ No newline at end of file diff --git a/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.8359857aa9b7c3fbc8bba9a505282ba848915383c4549f21b9f93f9f35b56415/handler-name.js b/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.ab58b1384030fef0fc8663c06f6fd62196fb3ae8807ab82e4559967d3b885b08/handler-name.js similarity index 100% rename from packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.8359857aa9b7c3fbc8bba9a505282ba848915383c4549f21b9f93f9f35b56415/handler-name.js rename to packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.ab58b1384030fef0fc8663c06f6fd62196fb3ae8807ab82e4559967d3b885b08/handler-name.js diff --git a/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.8359857aa9b7c3fbc8bba9a505282ba848915383c4549f21b9f93f9f35b56415/index.js b/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.ab58b1384030fef0fc8663c06f6fd62196fb3ae8807ab82e4559967d3b885b08/index.js similarity index 100% rename from packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.8359857aa9b7c3fbc8bba9a505282ba848915383c4549f21b9f93f9f35b56415/index.js rename to packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.ab58b1384030fef0fc8663c06f6fd62196fb3ae8807ab82e4559967d3b885b08/index.js diff --git a/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.8359857aa9b7c3fbc8bba9a505282ba848915383c4549f21b9f93f9f35b56415/privileges.js b/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.ab58b1384030fef0fc8663c06f6fd62196fb3ae8807ab82e4559967d3b885b08/privileges.js similarity index 100% rename from packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.8359857aa9b7c3fbc8bba9a505282ba848915383c4549f21b9f93f9f35b56415/privileges.js rename to packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.ab58b1384030fef0fc8663c06f6fd62196fb3ae8807ab82e4559967d3b885b08/privileges.js diff --git a/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.8359857aa9b7c3fbc8bba9a505282ba848915383c4549f21b9f93f9f35b56415/redshift-data.js b/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.ab58b1384030fef0fc8663c06f6fd62196fb3ae8807ab82e4559967d3b885b08/redshift-data.js similarity index 100% rename from packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.8359857aa9b7c3fbc8bba9a505282ba848915383c4549f21b9f93f9f35b56415/redshift-data.js rename to packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.ab58b1384030fef0fc8663c06f6fd62196fb3ae8807ab82e4559967d3b885b08/redshift-data.js diff --git a/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.ab58b1384030fef0fc8663c06f6fd62196fb3ae8807ab82e4559967d3b885b08/table.js b/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.ab58b1384030fef0fc8663c06f6fd62196fb3ae8807ab82e4559967d3b885b08/table.js new file mode 100644 index 0000000000000..2566b1d2890c7 --- /dev/null +++ b/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.ab58b1384030fef0fc8663c06f6fd62196fb3ae8807ab82e4559967d3b885b08/table.js @@ -0,0 +1,125 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.handler = void 0; +const redshift_data_1 = require("./redshift-data"); +const types_1 = require("./types"); +const util_1 = require("./util"); +async function handler(props, event) { + const tableNamePrefix = props.tableName.prefix; + const tableNameSuffix = props.tableName.generateSuffix === 'true' ? `${event.RequestId.substring(0, 8)}` : ''; + const tableColumns = props.tableColumns; + const tableAndClusterProps = props; + if (event.RequestType === 'Create') { + const tableName = await createTable(tableNamePrefix, tableNameSuffix, tableColumns, tableAndClusterProps); + return { PhysicalResourceId: tableName }; + } + else if (event.RequestType === 'Delete') { + await dropTable(event.PhysicalResourceId, tableAndClusterProps); + return; + } + else if (event.RequestType === 'Update') { + const tableName = await updateTable(event.PhysicalResourceId, tableNamePrefix, tableNameSuffix, tableColumns, tableAndClusterProps, event.OldResourceProperties); + return { PhysicalResourceId: tableName }; + } + else { + /* eslint-disable-next-line dot-notation */ + throw new Error(`Unrecognized event type: ${event['RequestType']}`); + } +} +exports.handler = handler; +async function createTable(tableNamePrefix, tableNameSuffix, tableColumns, tableAndClusterProps) { + const tableName = tableNamePrefix + tableNameSuffix; + const tableColumnsString = tableColumns.map(column => `${column.name} ${column.dataType}`).join(); + let statement = `CREATE TABLE ${tableName} (${tableColumnsString})`; + if (tableAndClusterProps.distStyle) { + statement += ` DISTSTYLE ${tableAndClusterProps.distStyle}`; + } + const distKeyColumn = util_1.getDistKeyColumn(tableColumns); + if (distKeyColumn) { + statement += ` DISTKEY(${distKeyColumn.name})`; + } + const sortKeyColumns = util_1.getSortKeyColumns(tableColumns); + if (sortKeyColumns.length > 0) { + const sortKeyColumnsString = getSortKeyColumnsString(sortKeyColumns); + statement += ` ${tableAndClusterProps.sortStyle} SORTKEY(${sortKeyColumnsString})`; + } + await redshift_data_1.executeStatement(statement, tableAndClusterProps); + if (tableAndClusterProps.tableComment) { + await redshift_data_1.executeStatement(`COMMENT ON TABLE ${tableName} IS '${tableAndClusterProps.tableComment}'`, tableAndClusterProps); + } + return tableName; +} +async function dropTable(tableName, clusterProps) { + await redshift_data_1.executeStatement(`DROP TABLE ${tableName}`, clusterProps); +} +async function updateTable(tableName, tableNamePrefix, tableNameSuffix, tableColumns, tableAndClusterProps, oldResourceProperties) { + const alterationStatements = []; + const oldClusterProps = oldResourceProperties; + if (tableAndClusterProps.clusterName !== oldClusterProps.clusterName || tableAndClusterProps.databaseName !== oldClusterProps.databaseName) { + return createTable(tableNamePrefix, tableNameSuffix, tableColumns, tableAndClusterProps); + } + const oldTableNamePrefix = oldResourceProperties.tableName.prefix; + if (tableNamePrefix !== oldTableNamePrefix) { + return createTable(tableNamePrefix, tableNameSuffix, tableColumns, tableAndClusterProps); + } + const oldTableColumns = oldResourceProperties.tableColumns; + const columnDeletions = oldTableColumns.filter(oldColumn => (tableColumns.every(column => oldColumn.name !== column.name))); + if (columnDeletions.length > 0) { + alterationStatements.push(...columnDeletions.map(column => `ALTER TABLE ${tableName} DROP COLUMN ${column.name}`)); + } + const columnAdditions = tableColumns.filter(column => { + return !oldTableColumns.some(oldColumn => column.name === oldColumn.name && column.dataType === oldColumn.dataType); + }).map(column => `ADD ${column.name} ${column.dataType}`); + if (columnAdditions.length > 0) { + alterationStatements.push(...columnAdditions.map(addition => `ALTER TABLE ${tableName} ${addition}`)); + } + const oldDistStyle = oldResourceProperties.distStyle; + if ((!oldDistStyle && tableAndClusterProps.distStyle) || + (oldDistStyle && !tableAndClusterProps.distStyle)) { + return createTable(tableNamePrefix, tableNameSuffix, tableColumns, tableAndClusterProps); + } + else if (oldDistStyle !== tableAndClusterProps.distStyle) { + alterationStatements.push(`ALTER TABLE ${tableName} ALTER DISTSTYLE ${tableAndClusterProps.distStyle}`); + } + const oldDistKey = util_1.getDistKeyColumn(oldTableColumns)?.name; + const newDistKey = util_1.getDistKeyColumn(tableColumns)?.name; + if ((!oldDistKey && newDistKey) || (oldDistKey && !newDistKey)) { + return createTable(tableNamePrefix, tableNameSuffix, tableColumns, tableAndClusterProps); + } + else if (oldDistKey !== newDistKey) { + alterationStatements.push(`ALTER TABLE ${tableName} ALTER DISTKEY ${newDistKey}`); + } + const oldSortKeyColumns = util_1.getSortKeyColumns(oldTableColumns); + const newSortKeyColumns = util_1.getSortKeyColumns(tableColumns); + const oldSortStyle = oldResourceProperties.sortStyle; + const newSortStyle = tableAndClusterProps.sortStyle; + if ((oldSortStyle === newSortStyle && !util_1.areColumnsEqual(oldSortKeyColumns, newSortKeyColumns)) + || (oldSortStyle !== newSortStyle)) { + switch (newSortStyle) { + case types_1.TableSortStyle.INTERLEAVED: + // INTERLEAVED sort key addition requires replacement. + // https://docs.aws.amazon.com/redshift/latest/dg/r_ALTER_TABLE.html + return createTable(tableNamePrefix, tableNameSuffix, tableColumns, tableAndClusterProps); + case types_1.TableSortStyle.COMPOUND: { + const sortKeyColumnsString = getSortKeyColumnsString(newSortKeyColumns); + alterationStatements.push(`ALTER TABLE ${tableName} ALTER ${newSortStyle} SORTKEY(${sortKeyColumnsString})`); + break; + } + case types_1.TableSortStyle.AUTO: { + alterationStatements.push(`ALTER TABLE ${tableName} ALTER SORTKEY ${newSortStyle}`); + break; + } + } + } + const oldComment = oldResourceProperties.tableComment; + const newComment = tableAndClusterProps.tableComment; + if (oldComment !== newComment) { + alterationStatements.push(`COMMENT ON TABLE ${tableName} IS ${newComment ? `'${newComment}'` : 'NULL'}`); + } + await Promise.all(alterationStatements.map(statement => redshift_data_1.executeStatement(statement, tableAndClusterProps))); + return tableName; +} +function getSortKeyColumnsString(sortKeyColumns) { + return sortKeyColumns.map(column => column.name).join(); +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGFibGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJ0YWJsZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFHQSxtREFBbUQ7QUFDbkQsbUNBQTZFO0FBQzdFLGlDQUE4RTtBQUV2RSxLQUFLLFVBQVUsT0FBTyxDQUFDLEtBQTJCLEVBQUUsS0FBa0Q7SUFDM0csTUFBTSxlQUFlLEdBQUcsS0FBSyxDQUFDLFNBQVMsQ0FBQyxNQUFNLENBQUM7SUFDL0MsTUFBTSxlQUFlLEdBQUcsS0FBSyxDQUFDLFNBQVMsQ0FBQyxjQUFjLEtBQUssTUFBTSxDQUFDLENBQUMsQ0FBQyxHQUFHLEtBQUssQ0FBQyxTQUFTLENBQUMsU0FBUyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7SUFDOUcsTUFBTSxZQUFZLEdBQUcsS0FBSyxDQUFDLFlBQVksQ0FBQztJQUN4QyxNQUFNLG9CQUFvQixHQUFHLEtBQUssQ0FBQztJQUVuQyxJQUFJLEtBQUssQ0FBQyxXQUFXLEtBQUssUUFBUSxFQUFFO1FBQ2xDLE1BQU0sU0FBUyxHQUFHLE1BQU0sV0FBVyxDQUFDLGVBQWUsRUFBRSxlQUFlLEVBQUUsWUFBWSxFQUFFLG9CQUFvQixDQUFDLENBQUM7UUFDMUcsT0FBTyxFQUFFLGtCQUFrQixFQUFFLFNBQVMsRUFBRSxDQUFDO0tBQzFDO1NBQU0sSUFBSSxLQUFLLENBQUMsV0FBVyxLQUFLLFFBQVEsRUFBRTtRQUN6QyxNQUFNLFNBQVMsQ0FBQyxLQUFLLENBQUMsa0JBQWtCLEVBQUUsb0JBQW9CLENBQUMsQ0FBQztRQUNoRSxPQUFPO0tBQ1I7U0FBTSxJQUFJLEtBQUssQ0FBQyxXQUFXLEtBQUssUUFBUSxFQUFFO1FBQ3pDLE1BQU0sU0FBUyxHQUFHLE1BQU0sV0FBVyxDQUNqQyxLQUFLLENBQUMsa0JBQWtCLEVBQ3hCLGVBQWUsRUFDZixlQUFlLEVBQ2YsWUFBWSxFQUNaLG9CQUFvQixFQUNwQixLQUFLLENBQUMscUJBQTZDLENBQ3BELENBQUM7UUFDRixPQUFPLEVBQUUsa0JBQWtCLEVBQUUsU0FBUyxFQUFFLENBQUM7S0FDMUM7U0FBTTtRQUNMLDJDQUEyQztRQUMzQyxNQUFNLElBQUksS0FBSyxDQUFDLDRCQUE0QixLQUFLLENBQUMsYUFBYSxDQUFDLEVBQUUsQ0FBQyxDQUFDO0tBQ3JFO0FBQ0gsQ0FBQztBQTFCRCwwQkEwQkM7QUFFRCxLQUFLLFVBQVUsV0FBVyxDQUN4QixlQUF1QixFQUN2QixlQUF1QixFQUN2QixZQUFzQixFQUN0QixvQkFBMEM7SUFFMUMsTUFBTSxTQUFTLEdBQUcsZUFBZSxHQUFHLGVBQWUsQ0FBQztJQUNwRCxNQUFNLGtCQUFrQixHQUFHLFlBQVksQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxJQUFJLElBQUksTUFBTSxDQUFDLFFBQVEsRUFBRSxDQUFDLENBQUMsSUFBSSxFQUFFLENBQUM7SUFFbEcsSUFBSSxTQUFTLEdBQUcsZ0JBQWdCLFNBQVMsS0FBSyxrQkFBa0IsR0FBRyxDQUFDO0lBRXBFLElBQUksb0JBQW9CLENBQUMsU0FBUyxFQUFFO1FBQ2xDLFNBQVMsSUFBSSxjQUFjLG9CQUFvQixDQUFDLFNBQVMsRUFBRSxDQUFDO0tBQzdEO0lBRUQsTUFBTSxhQUFhLEdBQUcsdUJBQWdCLENBQUMsWUFBWSxDQUFDLENBQUM7SUFDckQsSUFBSSxhQUFhLEVBQUU7UUFDakIsU0FBUyxJQUFJLFlBQVksYUFBYSxDQUFDLElBQUksR0FBRyxDQUFDO0tBQ2hEO0lBRUQsTUFBTSxjQUFjLEdBQUcsd0JBQWlCLENBQUMsWUFBWSxDQUFDLENBQUM7SUFDdkQsSUFBSSxjQUFjLENBQUMsTUFBTSxHQUFHLENBQUMsRUFBRTtRQUM3QixNQUFNLG9CQUFvQixHQUFHLHVCQUF1QixDQUFDLGNBQWMsQ0FBQyxDQUFDO1FBQ3JFLFNBQVMsSUFBSSxJQUFJLG9CQUFvQixDQUFDLFNBQVMsWUFBWSxvQkFBb0IsR0FBRyxDQUFDO0tBQ3BGO0lBRUQsTUFBTSxnQ0FBZ0IsQ0FBQyxTQUFTLEVBQUUsb0JBQW9CLENBQUMsQ0FBQztJQUV4RCxJQUFJLG9CQUFvQixDQUFDLFlBQVksRUFBRTtRQUNyQyxNQUFNLGdDQUFnQixDQUFDLG9CQUFvQixTQUFTLFFBQVEsb0JBQW9CLENBQUMsWUFBWSxHQUFHLEVBQUUsb0JBQW9CLENBQUMsQ0FBQztLQUN6SDtJQUVELE9BQU8sU0FBUyxDQUFDO0FBQ25CLENBQUM7QUFFRCxLQUFLLFVBQVUsU0FBUyxDQUFDLFNBQWlCLEVBQUUsWUFBMEI7SUFDcEUsTUFBTSxnQ0FBZ0IsQ0FBQyxjQUFjLFNBQVMsRUFBRSxFQUFFLFlBQVksQ0FBQyxDQUFDO0FBQ2xFLENBQUM7QUFFRCxLQUFLLFVBQVUsV0FBVyxDQUN4QixTQUFpQixFQUNqQixlQUF1QixFQUN2QixlQUF1QixFQUN2QixZQUFzQixFQUN0QixvQkFBMEMsRUFDMUMscUJBQTJDO0lBRTNDLE1BQU0sb0JBQW9CLEdBQWEsRUFBRSxDQUFDO0lBRTFDLE1BQU0sZUFBZSxHQUFHLHFCQUFxQixDQUFDO0lBQzlDLElBQUksb0JBQW9CLENBQUMsV0FBVyxLQUFLLGVBQWUsQ0FBQyxXQUFXLElBQUksb0JBQW9CLENBQUMsWUFBWSxLQUFLLGVBQWUsQ0FBQyxZQUFZLEVBQUU7UUFDMUksT0FBTyxXQUFXLENBQUMsZUFBZSxFQUFFLGVBQWUsRUFBRSxZQUFZLEVBQUUsb0JBQW9CLENBQUMsQ0FBQztLQUMxRjtJQUVELE1BQU0sa0JBQWtCLEdBQUcscUJBQXFCLENBQUMsU0FBUyxDQUFDLE1BQU0sQ0FBQztJQUNsRSxJQUFJLGVBQWUsS0FBSyxrQkFBa0IsRUFBRTtRQUMxQyxPQUFPLFdBQVcsQ0FBQyxlQUFlLEVBQUUsZUFBZSxFQUFFLFlBQVksRUFBRSxvQkFBb0IsQ0FBQyxDQUFDO0tBQzFGO0lBRUQsTUFBTSxlQUFlLEdBQUcscUJBQXFCLENBQUMsWUFBWSxDQUFDO0lBQzNELE1BQU0sZUFBZSxHQUFHLGVBQWUsQ0FBQyxNQUFNLENBQUMsU0FBUyxDQUFDLEVBQUUsQ0FBQyxDQUMxRCxZQUFZLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUMsU0FBUyxDQUFDLElBQUksS0FBSyxNQUFNLENBQUMsSUFBSSxDQUFDLENBQzdELENBQUMsQ0FBQztJQUNILElBQUksZUFBZSxDQUFDLE1BQU0sR0FBRyxDQUFDLEVBQUU7UUFDOUIsb0JBQW9CLENBQUMsSUFBSSxDQUFDLEdBQUcsZUFBZSxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDLGVBQWUsU0FBUyxnQkFBZ0IsTUFBTSxDQUFDLElBQUksRUFBRSxDQUFDLENBQUMsQ0FBQztLQUNwSDtJQUVELE1BQU0sZUFBZSxHQUFHLFlBQVksQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLEVBQUU7UUFDbkQsT0FBTyxDQUFDLGVBQWUsQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLEVBQUUsQ0FBQyxNQUFNLENBQUMsSUFBSSxLQUFLLFNBQVMsQ0FBQyxJQUFJLElBQUksTUFBTSxDQUFDLFFBQVEsS0FBSyxTQUFTLENBQUMsUUFBUSxDQUFDLENBQUM7SUFDdEgsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUMsT0FBTyxNQUFNLENBQUMsSUFBSSxJQUFJLE1BQU0sQ0FBQyxRQUFRLEVBQUUsQ0FBQyxDQUFDO0lBQzFELElBQUksZUFBZSxDQUFDLE1BQU0sR0FBRyxDQUFDLEVBQUU7UUFDOUIsb0JBQW9CLENBQUMsSUFBSSxDQUFDLEdBQUcsZUFBZSxDQUFDLEdBQUcsQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDLGVBQWUsU0FBUyxJQUFJLFFBQVEsRUFBRSxDQUFDLENBQUMsQ0FBQztLQUN2RztJQUVELE1BQU0sWUFBWSxHQUFHLHFCQUFxQixDQUFDLFNBQVMsQ0FBQztJQUNyRCxJQUFJLENBQUMsQ0FBQyxZQUFZLElBQUksb0JBQW9CLENBQUMsU0FBUyxDQUFDO1FBQ25ELENBQUMsWUFBWSxJQUFJLENBQUMsb0JBQW9CLENBQUMsU0FBUyxDQUFDLEVBQUU7UUFDbkQsT0FBTyxXQUFXLENBQUMsZUFBZSxFQUFFLGVBQWUsRUFBRSxZQUFZLEVBQUUsb0JBQW9CLENBQUMsQ0FBQztLQUMxRjtTQUFNLElBQUksWUFBWSxLQUFLLG9CQUFvQixDQUFDLFNBQVMsRUFBRTtRQUMxRCxvQkFBb0IsQ0FBQyxJQUFJLENBQUMsZUFBZSxTQUFTLG9CQUFvQixvQkFBb0IsQ0FBQyxTQUFTLEVBQUUsQ0FBQyxDQUFDO0tBQ3pHO0lBRUQsTUFBTSxVQUFVLEdBQUcsdUJBQWdCLENBQUMsZUFBZSxDQUFDLEVBQUUsSUFBSSxDQUFDO0lBQzNELE1BQU0sVUFBVSxHQUFHLHVCQUFnQixDQUFDLFlBQVksQ0FBQyxFQUFFLElBQUksQ0FBQztJQUN4RCxJQUFJLENBQUMsQ0FBQyxVQUFVLElBQUksVUFBVSxDQUFFLElBQUksQ0FBQyxVQUFVLElBQUksQ0FBQyxVQUFVLENBQUMsRUFBRTtRQUMvRCxPQUFPLFdBQVcsQ0FBQyxlQUFlLEVBQUUsZUFBZSxFQUFFLFlBQVksRUFBRSxvQkFBb0IsQ0FBQyxDQUFDO0tBQzFGO1NBQU0sSUFBSSxVQUFVLEtBQUssVUFBVSxFQUFFO1FBQ3BDLG9CQUFvQixDQUFDLElBQUksQ0FBQyxlQUFlLFNBQVMsa0JBQWtCLFVBQVUsRUFBRSxDQUFDLENBQUM7S0FDbkY7SUFFRCxNQUFNLGlCQUFpQixHQUFHLHdCQUFpQixDQUFDLGVBQWUsQ0FBQyxDQUFDO0lBQzdELE1BQU0saUJBQWlCLEdBQUcsd0JBQWlCLENBQUMsWUFBWSxDQUFDLENBQUM7SUFDMUQsTUFBTSxZQUFZLEdBQUcscUJBQXFCLENBQUMsU0FBUyxDQUFDO0lBQ3JELE1BQU0sWUFBWSxHQUFHLG9CQUFvQixDQUFDLFNBQVMsQ0FBQztJQUNwRCxJQUFJLENBQUMsWUFBWSxLQUFLLFlBQVksSUFBSSxDQUFDLHNCQUFlLENBQUMsaUJBQWlCLEVBQUUsaUJBQWlCLENBQUMsQ0FBQztXQUN4RixDQUFDLFlBQVksS0FBSyxZQUFZLENBQUMsRUFBRTtRQUNwQyxRQUFRLFlBQVksRUFBRTtZQUNwQixLQUFLLHNCQUFjLENBQUMsV0FBVztnQkFDN0Isc0RBQXNEO2dCQUN0RCxvRUFBb0U7Z0JBQ3BFLE9BQU8sV0FBVyxDQUFDLGVBQWUsRUFBRSxlQUFlLEVBQUUsWUFBWSxFQUFFLG9CQUFvQixDQUFDLENBQUM7WUFFM0YsS0FBSyxzQkFBYyxDQUFDLFFBQVEsQ0FBQyxDQUFDO2dCQUM1QixNQUFNLG9CQUFvQixHQUFHLHVCQUF1QixDQUFDLGlCQUFpQixDQUFDLENBQUM7Z0JBQ3hFLG9CQUFvQixDQUFDLElBQUksQ0FBQyxlQUFlLFNBQVMsVUFBVSxZQUFZLFlBQVksb0JBQW9CLEdBQUcsQ0FBQyxDQUFDO2dCQUM3RyxNQUFNO2FBQ1A7WUFFRCxLQUFLLHNCQUFjLENBQUMsSUFBSSxDQUFDLENBQUM7Z0JBQ3hCLG9CQUFvQixDQUFDLElBQUksQ0FBQyxlQUFlLFNBQVMsa0JBQWtCLFlBQVksRUFBRSxDQUFDLENBQUM7Z0JBQ3BGLE1BQU07YUFDUDtTQUNGO0tBQ0Y7SUFFRCxNQUFNLFVBQVUsR0FBRyxxQkFBcUIsQ0FBQyxZQUFZLENBQUM7SUFDdEQsTUFBTSxVQUFVLEdBQUcsb0JBQW9CLENBQUMsWUFBWSxDQUFDO0lBQ3JELElBQUksVUFBVSxLQUFLLFVBQVUsRUFBRTtRQUM3QixvQkFBb0IsQ0FBQyxJQUFJLENBQUMsb0JBQW9CLFNBQVMsT0FBTyxVQUFVLENBQUMsQ0FBQyxDQUFDLElBQUksVUFBVSxHQUFHLENBQUMsQ0FBQyxDQUFDLE1BQU0sRUFBRSxDQUFDLENBQUM7S0FDMUc7SUFFRCxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsb0JBQW9CLENBQUMsR0FBRyxDQUFDLFNBQVMsQ0FBQyxFQUFFLENBQUMsZ0NBQWdCLENBQUMsU0FBUyxFQUFFLG9CQUFvQixDQUFDLENBQUMsQ0FBQyxDQUFDO0lBRTVHLE9BQU8sU0FBUyxDQUFDO0FBQ25CLENBQUM7QUFFRCxTQUFTLHVCQUF1QixDQUFDLGNBQXdCO0lBQ3ZELE9BQU8sY0FBYyxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQyxJQUFJLEVBQUUsQ0FBQztBQUMxRCxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyogZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIGltcG9ydC9uby11bnJlc29sdmVkICovXG5pbXBvcnQgKiBhcyBBV1NMYW1iZGEgZnJvbSAnYXdzLWxhbWJkYSc7XG5pbXBvcnQgeyBDb2x1bW4gfSBmcm9tICcuLi8uLi90YWJsZSc7XG5pbXBvcnQgeyBleGVjdXRlU3RhdGVtZW50IH0gZnJvbSAnLi9yZWRzaGlmdC1kYXRhJztcbmltcG9ydCB7IENsdXN0ZXJQcm9wcywgVGFibGVBbmRDbHVzdGVyUHJvcHMsIFRhYmxlU29ydFN0eWxlIH0gZnJvbSAnLi90eXBlcyc7XG5pbXBvcnQgeyBhcmVDb2x1bW5zRXF1YWwsIGdldERpc3RLZXlDb2x1bW4sIGdldFNvcnRLZXlDb2x1bW5zIH0gZnJvbSAnLi91dGlsJztcblxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGhhbmRsZXIocHJvcHM6IFRhYmxlQW5kQ2x1c3RlclByb3BzLCBldmVudDogQVdTTGFtYmRhLkNsb3VkRm9ybWF0aW9uQ3VzdG9tUmVzb3VyY2VFdmVudCkge1xuICBjb25zdCB0YWJsZU5hbWVQcmVmaXggPSBwcm9wcy50YWJsZU5hbWUucHJlZml4O1xuICBjb25zdCB0YWJsZU5hbWVTdWZmaXggPSBwcm9wcy50YWJsZU5hbWUuZ2VuZXJhdGVTdWZmaXggPT09ICd0cnVlJyA/IGAke2V2ZW50LlJlcXVlc3RJZC5zdWJzdHJpbmcoMCwgOCl9YCA6ICcnO1xuICBjb25zdCB0YWJsZUNvbHVtbnMgPSBwcm9wcy50YWJsZUNvbHVtbnM7XG4gIGNvbnN0IHRhYmxlQW5kQ2x1c3RlclByb3BzID0gcHJvcHM7XG5cbiAgaWYgKGV2ZW50LlJlcXVlc3RUeXBlID09PSAnQ3JlYXRlJykge1xuICAgIGNvbnN0IHRhYmxlTmFtZSA9IGF3YWl0IGNyZWF0ZVRhYmxlKHRhYmxlTmFtZVByZWZpeCwgdGFibGVOYW1lU3VmZml4LCB0YWJsZUNvbHVtbnMsIHRhYmxlQW5kQ2x1c3RlclByb3BzKTtcbiAgICByZXR1cm4geyBQaHlzaWNhbFJlc291cmNlSWQ6IHRhYmxlTmFtZSB9O1xuICB9IGVsc2UgaWYgKGV2ZW50LlJlcXVlc3RUeXBlID09PSAnRGVsZXRlJykge1xuICAgIGF3YWl0IGRyb3BUYWJsZShldmVudC5QaHlzaWNhbFJlc291cmNlSWQsIHRhYmxlQW5kQ2x1c3RlclByb3BzKTtcbiAgICByZXR1cm47XG4gIH0gZWxzZSBpZiAoZXZlbnQuUmVxdWVzdFR5cGUgPT09ICdVcGRhdGUnKSB7XG4gICAgY29uc3QgdGFibGVOYW1lID0gYXdhaXQgdXBkYXRlVGFibGUoXG4gICAgICBldmVudC5QaHlzaWNhbFJlc291cmNlSWQsXG4gICAgICB0YWJsZU5hbWVQcmVmaXgsXG4gICAgICB0YWJsZU5hbWVTdWZmaXgsXG4gICAgICB0YWJsZUNvbHVtbnMsXG4gICAgICB0YWJsZUFuZENsdXN0ZXJQcm9wcyxcbiAgICAgIGV2ZW50Lk9sZFJlc291cmNlUHJvcGVydGllcyBhcyBUYWJsZUFuZENsdXN0ZXJQcm9wcyxcbiAgICApO1xuICAgIHJldHVybiB7IFBoeXNpY2FsUmVzb3VyY2VJZDogdGFibGVOYW1lIH07XG4gIH0gZWxzZSB7XG4gICAgLyogZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIGRvdC1ub3RhdGlvbiAqL1xuICAgIHRocm93IG5ldyBFcnJvcihgVW5yZWNvZ25pemVkIGV2ZW50IHR5cGU6ICR7ZXZlbnRbJ1JlcXVlc3RUeXBlJ119YCk7XG4gIH1cbn1cblxuYXN5bmMgZnVuY3Rpb24gY3JlYXRlVGFibGUoXG4gIHRhYmxlTmFtZVByZWZpeDogc3RyaW5nLFxuICB0YWJsZU5hbWVTdWZmaXg6IHN0cmluZyxcbiAgdGFibGVDb2x1bW5zOiBDb2x1bW5bXSxcbiAgdGFibGVBbmRDbHVzdGVyUHJvcHM6IFRhYmxlQW5kQ2x1c3RlclByb3BzLFxuKTogUHJvbWlzZTxzdHJpbmc+IHtcbiAgY29uc3QgdGFibGVOYW1lID0gdGFibGVOYW1lUHJlZml4ICsgdGFibGVOYW1lU3VmZml4O1xuICBjb25zdCB0YWJsZUNvbHVtbnNTdHJpbmcgPSB0YWJsZUNvbHVtbnMubWFwKGNvbHVtbiA9PiBgJHtjb2x1bW4ubmFtZX0gJHtjb2x1bW4uZGF0YVR5cGV9YCkuam9pbigpO1xuXG4gIGxldCBzdGF0ZW1lbnQgPSBgQ1JFQVRFIFRBQkxFICR7dGFibGVOYW1lfSAoJHt0YWJsZUNvbHVtbnNTdHJpbmd9KWA7XG5cbiAgaWYgKHRhYmxlQW5kQ2x1c3RlclByb3BzLmRpc3RTdHlsZSkge1xuICAgIHN0YXRlbWVudCArPSBgIERJU1RTVFlMRSAke3RhYmxlQW5kQ2x1c3RlclByb3BzLmRpc3RTdHlsZX1gO1xuICB9XG5cbiAgY29uc3QgZGlzdEtleUNvbHVtbiA9IGdldERpc3RLZXlDb2x1bW4odGFibGVDb2x1bW5zKTtcbiAgaWYgKGRpc3RLZXlDb2x1bW4pIHtcbiAgICBzdGF0ZW1lbnQgKz0gYCBESVNUS0VZKCR7ZGlzdEtleUNvbHVtbi5uYW1lfSlgO1xuICB9XG5cbiAgY29uc3Qgc29ydEtleUNvbHVtbnMgPSBnZXRTb3J0S2V5Q29sdW1ucyh0YWJsZUNvbHVtbnMpO1xuICBpZiAoc29ydEtleUNvbHVtbnMubGVuZ3RoID4gMCkge1xuICAgIGNvbnN0IHNvcnRLZXlDb2x1bW5zU3RyaW5nID0gZ2V0U29ydEtleUNvbHVtbnNTdHJpbmcoc29ydEtleUNvbHVtbnMpO1xuICAgIHN0YXRlbWVudCArPSBgICR7dGFibGVBbmRDbHVzdGVyUHJvcHMuc29ydFN0eWxlfSBTT1JUS0VZKCR7c29ydEtleUNvbHVtbnNTdHJpbmd9KWA7XG4gIH1cblxuICBhd2FpdCBleGVjdXRlU3RhdGVtZW50KHN0YXRlbWVudCwgdGFibGVBbmRDbHVzdGVyUHJvcHMpO1xuXG4gIGlmICh0YWJsZUFuZENsdXN0ZXJQcm9wcy50YWJsZUNvbW1lbnQpIHtcbiAgICBhd2FpdCBleGVjdXRlU3RhdGVtZW50KGBDT01NRU5UIE9OIFRBQkxFICR7dGFibGVOYW1lfSBJUyAnJHt0YWJsZUFuZENsdXN0ZXJQcm9wcy50YWJsZUNvbW1lbnR9J2AsIHRhYmxlQW5kQ2x1c3RlclByb3BzKTtcbiAgfVxuXG4gIHJldHVybiB0YWJsZU5hbWU7XG59XG5cbmFzeW5jIGZ1bmN0aW9uIGRyb3BUYWJsZSh0YWJsZU5hbWU6IHN0cmluZywgY2x1c3RlclByb3BzOiBDbHVzdGVyUHJvcHMpIHtcbiAgYXdhaXQgZXhlY3V0ZVN0YXRlbWVudChgRFJPUCBUQUJMRSAke3RhYmxlTmFtZX1gLCBjbHVzdGVyUHJvcHMpO1xufVxuXG5hc3luYyBmdW5jdGlvbiB1cGRhdGVUYWJsZShcbiAgdGFibGVOYW1lOiBzdHJpbmcsXG4gIHRhYmxlTmFtZVByZWZpeDogc3RyaW5nLFxuICB0YWJsZU5hbWVTdWZmaXg6IHN0cmluZyxcbiAgdGFibGVDb2x1bW5zOiBDb2x1bW5bXSxcbiAgdGFibGVBbmRDbHVzdGVyUHJvcHM6IFRhYmxlQW5kQ2x1c3RlclByb3BzLFxuICBvbGRSZXNvdXJjZVByb3BlcnRpZXM6IFRhYmxlQW5kQ2x1c3RlclByb3BzLFxuKTogUHJvbWlzZTxzdHJpbmc+IHtcbiAgY29uc3QgYWx0ZXJhdGlvblN0YXRlbWVudHM6IHN0cmluZ1tdID0gW107XG5cbiAgY29uc3Qgb2xkQ2x1c3RlclByb3BzID0gb2xkUmVzb3VyY2VQcm9wZXJ0aWVzO1xuICBpZiAodGFibGVBbmRDbHVzdGVyUHJvcHMuY2x1c3Rlck5hbWUgIT09IG9sZENsdXN0ZXJQcm9wcy5jbHVzdGVyTmFtZSB8fCB0YWJsZUFuZENsdXN0ZXJQcm9wcy5kYXRhYmFzZU5hbWUgIT09IG9sZENsdXN0ZXJQcm9wcy5kYXRhYmFzZU5hbWUpIHtcbiAgICByZXR1cm4gY3JlYXRlVGFibGUodGFibGVOYW1lUHJlZml4LCB0YWJsZU5hbWVTdWZmaXgsIHRhYmxlQ29sdW1ucywgdGFibGVBbmRDbHVzdGVyUHJvcHMpO1xuICB9XG5cbiAgY29uc3Qgb2xkVGFibGVOYW1lUHJlZml4ID0gb2xkUmVzb3VyY2VQcm9wZXJ0aWVzLnRhYmxlTmFtZS5wcmVmaXg7XG4gIGlmICh0YWJsZU5hbWVQcmVmaXggIT09IG9sZFRhYmxlTmFtZVByZWZpeCkge1xuICAgIHJldHVybiBjcmVhdGVUYWJsZSh0YWJsZU5hbWVQcmVmaXgsIHRhYmxlTmFtZVN1ZmZpeCwgdGFibGVDb2x1bW5zLCB0YWJsZUFuZENsdXN0ZXJQcm9wcyk7XG4gIH1cblxuICBjb25zdCBvbGRUYWJsZUNvbHVtbnMgPSBvbGRSZXNvdXJjZVByb3BlcnRpZXMudGFibGVDb2x1bW5zO1xuICBjb25zdCBjb2x1bW5EZWxldGlvbnMgPSBvbGRUYWJsZUNvbHVtbnMuZmlsdGVyKG9sZENvbHVtbiA9PiAoXG4gICAgdGFibGVDb2x1bW5zLmV2ZXJ5KGNvbHVtbiA9PiBvbGRDb2x1bW4ubmFtZSAhPT0gY29sdW1uLm5hbWUpXG4gICkpO1xuICBpZiAoY29sdW1uRGVsZXRpb25zLmxlbmd0aCA+IDApIHtcbiAgICBhbHRlcmF0aW9uU3RhdGVtZW50cy5wdXNoKC4uLmNvbHVtbkRlbGV0aW9ucy5tYXAoY29sdW1uID0+IGBBTFRFUiBUQUJMRSAke3RhYmxlTmFtZX0gRFJPUCBDT0xVTU4gJHtjb2x1bW4ubmFtZX1gKSk7XG4gIH1cblxuICBjb25zdCBjb2x1bW5BZGRpdGlvbnMgPSB0YWJsZUNvbHVtbnMuZmlsdGVyKGNvbHVtbiA9PiB7XG4gICAgcmV0dXJuICFvbGRUYWJsZUNvbHVtbnMuc29tZShvbGRDb2x1bW4gPT4gY29sdW1uLm5hbWUgPT09IG9sZENvbHVtbi5uYW1lICYmIGNvbHVtbi5kYXRhVHlwZSA9PT0gb2xkQ29sdW1uLmRhdGFUeXBlKTtcbiAgfSkubWFwKGNvbHVtbiA9PiBgQUREICR7Y29sdW1uLm5hbWV9ICR7Y29sdW1uLmRhdGFUeXBlfWApO1xuICBpZiAoY29sdW1uQWRkaXRpb25zLmxlbmd0aCA+IDApIHtcbiAgICBhbHRlcmF0aW9uU3RhdGVtZW50cy5wdXNoKC4uLmNvbHVtbkFkZGl0aW9ucy5tYXAoYWRkaXRpb24gPT4gYEFMVEVSIFRBQkxFICR7dGFibGVOYW1lfSAke2FkZGl0aW9ufWApKTtcbiAgfVxuXG4gIGNvbnN0IG9sZERpc3RTdHlsZSA9IG9sZFJlc291cmNlUHJvcGVydGllcy5kaXN0U3R5bGU7XG4gIGlmICgoIW9sZERpc3RTdHlsZSAmJiB0YWJsZUFuZENsdXN0ZXJQcm9wcy5kaXN0U3R5bGUpIHx8XG4gICAgKG9sZERpc3RTdHlsZSAmJiAhdGFibGVBbmRDbHVzdGVyUHJvcHMuZGlzdFN0eWxlKSkge1xuICAgIHJldHVybiBjcmVhdGVUYWJsZSh0YWJsZU5hbWVQcmVmaXgsIHRhYmxlTmFtZVN1ZmZpeCwgdGFibGVDb2x1bW5zLCB0YWJsZUFuZENsdXN0ZXJQcm9wcyk7XG4gIH0gZWxzZSBpZiAob2xkRGlzdFN0eWxlICE9PSB0YWJsZUFuZENsdXN0ZXJQcm9wcy5kaXN0U3R5bGUpIHtcbiAgICBhbHRlcmF0aW9uU3RhdGVtZW50cy5wdXNoKGBBTFRFUiBUQUJMRSAke3RhYmxlTmFtZX0gQUxURVIgRElTVFNUWUxFICR7dGFibGVBbmRDbHVzdGVyUHJvcHMuZGlzdFN0eWxlfWApO1xuICB9XG5cbiAgY29uc3Qgb2xkRGlzdEtleSA9IGdldERpc3RLZXlDb2x1bW4ob2xkVGFibGVDb2x1bW5zKT8ubmFtZTtcbiAgY29uc3QgbmV3RGlzdEtleSA9IGdldERpc3RLZXlDb2x1bW4odGFibGVDb2x1bW5zKT8ubmFtZTtcbiAgaWYgKCghb2xkRGlzdEtleSAmJiBuZXdEaXN0S2V5ICkgfHwgKG9sZERpc3RLZXkgJiYgIW5ld0Rpc3RLZXkpKSB7XG4gICAgcmV0dXJuIGNyZWF0ZVRhYmxlKHRhYmxlTmFtZVByZWZpeCwgdGFibGVOYW1lU3VmZml4LCB0YWJsZUNvbHVtbnMsIHRhYmxlQW5kQ2x1c3RlclByb3BzKTtcbiAgfSBlbHNlIGlmIChvbGREaXN0S2V5ICE9PSBuZXdEaXN0S2V5KSB7XG4gICAgYWx0ZXJhdGlvblN0YXRlbWVudHMucHVzaChgQUxURVIgVEFCTEUgJHt0YWJsZU5hbWV9IEFMVEVSIERJU1RLRVkgJHtuZXdEaXN0S2V5fWApO1xuICB9XG5cbiAgY29uc3Qgb2xkU29ydEtleUNvbHVtbnMgPSBnZXRTb3J0S2V5Q29sdW1ucyhvbGRUYWJsZUNvbHVtbnMpO1xuICBjb25zdCBuZXdTb3J0S2V5Q29sdW1ucyA9IGdldFNvcnRLZXlDb2x1bW5zKHRhYmxlQ29sdW1ucyk7XG4gIGNvbnN0IG9sZFNvcnRTdHlsZSA9IG9sZFJlc291cmNlUHJvcGVydGllcy5zb3J0U3R5bGU7XG4gIGNvbnN0IG5ld1NvcnRTdHlsZSA9IHRhYmxlQW5kQ2x1c3RlclByb3BzLnNvcnRTdHlsZTtcbiAgaWYgKChvbGRTb3J0U3R5bGUgPT09IG5ld1NvcnRTdHlsZSAmJiAhYXJlQ29sdW1uc0VxdWFsKG9sZFNvcnRLZXlDb2x1bW5zLCBuZXdTb3J0S2V5Q29sdW1ucykpXG4gICAgfHwgKG9sZFNvcnRTdHlsZSAhPT0gbmV3U29ydFN0eWxlKSkge1xuICAgIHN3aXRjaCAobmV3U29ydFN0eWxlKSB7XG4gICAgICBjYXNlIFRhYmxlU29ydFN0eWxlLklOVEVSTEVBVkVEOlxuICAgICAgICAvLyBJTlRFUkxFQVZFRCBzb3J0IGtleSBhZGRpdGlvbiByZXF1aXJlcyByZXBsYWNlbWVudC5cbiAgICAgICAgLy8gaHR0cHM6Ly9kb2NzLmF3cy5hbWF6b24uY29tL3JlZHNoaWZ0L2xhdGVzdC9kZy9yX0FMVEVSX1RBQkxFLmh0bWxcbiAgICAgICAgcmV0dXJuIGNyZWF0ZVRhYmxlKHRhYmxlTmFtZVByZWZpeCwgdGFibGVOYW1lU3VmZml4LCB0YWJsZUNvbHVtbnMsIHRhYmxlQW5kQ2x1c3RlclByb3BzKTtcblxuICAgICAgY2FzZSBUYWJsZVNvcnRTdHlsZS5DT01QT1VORDoge1xuICAgICAgICBjb25zdCBzb3J0S2V5Q29sdW1uc1N0cmluZyA9IGdldFNvcnRLZXlDb2x1bW5zU3RyaW5nKG5ld1NvcnRLZXlDb2x1bW5zKTtcbiAgICAgICAgYWx0ZXJhdGlvblN0YXRlbWVudHMucHVzaChgQUxURVIgVEFCTEUgJHt0YWJsZU5hbWV9IEFMVEVSICR7bmV3U29ydFN0eWxlfSBTT1JUS0VZKCR7c29ydEtleUNvbHVtbnNTdHJpbmd9KWApO1xuICAgICAgICBicmVhaztcbiAgICAgIH1cblxuICAgICAgY2FzZSBUYWJsZVNvcnRTdHlsZS5BVVRPOiB7XG4gICAgICAgIGFsdGVyYXRpb25TdGF0ZW1lbnRzLnB1c2goYEFMVEVSIFRBQkxFICR7dGFibGVOYW1lfSBBTFRFUiBTT1JUS0VZICR7bmV3U29ydFN0eWxlfWApO1xuICAgICAgICBicmVhaztcbiAgICAgIH1cbiAgICB9XG4gIH1cblxuICBjb25zdCBvbGRDb21tZW50ID0gb2xkUmVzb3VyY2VQcm9wZXJ0aWVzLnRhYmxlQ29tbWVudDtcbiAgY29uc3QgbmV3Q29tbWVudCA9IHRhYmxlQW5kQ2x1c3RlclByb3BzLnRhYmxlQ29tbWVudDtcbiAgaWYgKG9sZENvbW1lbnQgIT09IG5ld0NvbW1lbnQpIHtcbiAgICBhbHRlcmF0aW9uU3RhdGVtZW50cy5wdXNoKGBDT01NRU5UIE9OIFRBQkxFICR7dGFibGVOYW1lfSBJUyAke25ld0NvbW1lbnQgPyBgJyR7bmV3Q29tbWVudH0nYCA6ICdOVUxMJ31gKTtcbiAgfVxuXG4gIGF3YWl0IFByb21pc2UuYWxsKGFsdGVyYXRpb25TdGF0ZW1lbnRzLm1hcChzdGF0ZW1lbnQgPT4gZXhlY3V0ZVN0YXRlbWVudChzdGF0ZW1lbnQsIHRhYmxlQW5kQ2x1c3RlclByb3BzKSkpO1xuXG4gIHJldHVybiB0YWJsZU5hbWU7XG59XG5cbmZ1bmN0aW9uIGdldFNvcnRLZXlDb2x1bW5zU3RyaW5nKHNvcnRLZXlDb2x1bW5zOiBDb2x1bW5bXSkge1xuICByZXR1cm4gc29ydEtleUNvbHVtbnMubWFwKGNvbHVtbiA9PiBjb2x1bW4ubmFtZSkuam9pbigpO1xufVxuIl19 \ No newline at end of file diff --git a/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.8359857aa9b7c3fbc8bba9a505282ba848915383c4549f21b9f93f9f35b56415/types.js b/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.ab58b1384030fef0fc8663c06f6fd62196fb3ae8807ab82e4559967d3b885b08/types.js similarity index 100% rename from packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.8359857aa9b7c3fbc8bba9a505282ba848915383c4549f21b9f93f9f35b56415/types.js rename to packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.ab58b1384030fef0fc8663c06f6fd62196fb3ae8807ab82e4559967d3b885b08/types.js diff --git a/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.8359857aa9b7c3fbc8bba9a505282ba848915383c4549f21b9f93f9f35b56415/user.js b/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.ab58b1384030fef0fc8663c06f6fd62196fb3ae8807ab82e4559967d3b885b08/user.js similarity index 100% rename from packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.8359857aa9b7c3fbc8bba9a505282ba848915383c4549f21b9f93f9f35b56415/user.js rename to packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.ab58b1384030fef0fc8663c06f6fd62196fb3ae8807ab82e4559967d3b885b08/user.js diff --git a/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.8359857aa9b7c3fbc8bba9a505282ba848915383c4549f21b9f93f9f35b56415/util.js b/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.ab58b1384030fef0fc8663c06f6fd62196fb3ae8807ab82e4559967d3b885b08/util.js similarity index 100% rename from packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.8359857aa9b7c3fbc8bba9a505282ba848915383c4549f21b9f93f9f35b56415/util.js rename to packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/asset.ab58b1384030fef0fc8663c06f6fd62196fb3ae8807ab82e4559967d3b885b08/util.js diff --git a/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/aws-cdk-redshift-cluster-database.assets.json b/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/aws-cdk-redshift-cluster-database.assets.json index 1acb843ce81aa..c8185e2fb4f7e 100644 --- a/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/aws-cdk-redshift-cluster-database.assets.json +++ b/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/aws-cdk-redshift-cluster-database.assets.json @@ -1,15 +1,15 @@ { - "version": "22.0.0", + "version": "29.0.0", "files": { - "8359857aa9b7c3fbc8bba9a505282ba848915383c4549f21b9f93f9f35b56415": { + "ab58b1384030fef0fc8663c06f6fd62196fb3ae8807ab82e4559967d3b885b08": { "source": { - "path": "asset.8359857aa9b7c3fbc8bba9a505282ba848915383c4549f21b9f93f9f35b56415", + "path": "asset.ab58b1384030fef0fc8663c06f6fd62196fb3ae8807ab82e4559967d3b885b08", "packaging": "zip" }, "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "8359857aa9b7c3fbc8bba9a505282ba848915383c4549f21b9f93f9f35b56415.zip", + "objectKey": "ab58b1384030fef0fc8663c06f6fd62196fb3ae8807ab82e4559967d3b885b08.zip", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } @@ -27,7 +27,7 @@ } } }, - "fadca82b7c081a8b6de68f952878e92ba5610dce63ccbead865e1b854073bff0": { + "fd9bc22f4d8ca7fabbbe054d374117616ffa6a2393152ecb529d2b385432d259": { "source": { "path": "aws-cdk-redshift-cluster-database.template.json", "packaging": "file" @@ -35,7 +35,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "fadca82b7c081a8b6de68f952878e92ba5610dce63ccbead865e1b854073bff0.json", + "objectKey": "fd9bc22f4d8ca7fabbbe054d374117616ffa6a2393152ecb529d2b385432d259.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/aws-cdk-redshift-cluster-database.template.json b/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/aws-cdk-redshift-cluster-database.template.json index 7d7265311c8af..d5f58647d06cc 100644 --- a/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/aws-cdk-redshift-cluster-database.template.json +++ b/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/aws-cdk-redshift-cluster-database.template.json @@ -1004,7 +1004,7 @@ "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "S3Key": "8359857aa9b7c3fbc8bba9a505282ba848915383c4549f21b9f93f9f35b56415.zip" + "S3Key": "ab58b1384030fef0fc8663c06f6fd62196fb3ae8807ab82e4559967d3b885b08.zip" }, "Role": { "Fn::GetAtt": [ @@ -1176,7 +1176,8 @@ } ], "distStyle": "KEY", - "sortStyle": "INTERLEAVED" + "sortStyle": "INTERLEAVED", + "tableComment": "A test table" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" diff --git a/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/cdk.out b/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/cdk.out index 145739f539580..d8b441d447f8a 100644 --- a/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/cdk.out +++ b/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"22.0.0"} \ No newline at end of file +{"version":"29.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/integ.json b/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/integ.json index f624a18a308e6..7d18d58e00a13 100644 --- a/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/integ.json +++ b/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "22.0.0", + "version": "29.0.0", "testCases": { "redshift-cluster-database-integ/DefaultTest": { "stacks": [ diff --git a/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/manifest.json b/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/manifest.json index 38184219376bc..80db4b0c589f9 100644 --- a/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "22.0.0", + "version": "29.0.0", "artifacts": { "aws-cdk-redshift-cluster-database.assets": { "type": "cdk:asset-manifest", @@ -17,7 +17,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/fadca82b7c081a8b6de68f952878e92ba5610dce63ccbead865e1b854073bff0.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/fd9bc22f4d8ca7fabbbe054d374117616ffa6a2393152ecb529d2b385432d259.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ diff --git a/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/redshiftclusterdatabaseintegDefaultTestDeployAssert4339FB48.assets.json b/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/redshiftclusterdatabaseintegDefaultTestDeployAssert4339FB48.assets.json index 0584a656f04d1..adb32130fe4eb 100644 --- a/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/redshiftclusterdatabaseintegDefaultTestDeployAssert4339FB48.assets.json +++ b/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/redshiftclusterdatabaseintegDefaultTestDeployAssert4339FB48.assets.json @@ -1,5 +1,5 @@ { - "version": "22.0.0", + "version": "29.0.0", "files": { "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { "source": { diff --git a/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/tree.json b/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/tree.json index a8cbab7a274d4..3cad05f851971 100644 --- a/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/tree.json +++ b/packages/@aws-cdk/aws-redshift/test/integ.database.js.snapshot/tree.json @@ -1228,7 +1228,7 @@ }, "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.182" + "version": "10.1.209" } }, "TablePrivileges": { @@ -1470,13 +1470,13 @@ }, "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.182" + "version": "10.1.209" } } }, "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.182" + "version": "10.1.209" } } }, @@ -1639,7 +1639,7 @@ "s3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "s3Key": "8359857aa9b7c3fbc8bba9a505282ba848915383c4549f21b9f93f9f35b56415.zip" + "s3Key": "ab58b1384030fef0fc8663c06f6fd62196fb3ae8807ab82e4559967d3b885b08.zip" }, "role": { "Fn::GetAtt": [ @@ -1902,7 +1902,7 @@ }, "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.182" + "version": "10.1.209" } } }, @@ -1946,7 +1946,7 @@ "path": "redshift-cluster-database-integ/DefaultTest/Default", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.182" + "version": "10.1.209" } }, "DeployAssert": { @@ -1992,7 +1992,7 @@ "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.182" + "version": "10.1.209" } } }, diff --git a/packages/@aws-cdk/aws-redshift/test/integ.database.ts b/packages/@aws-cdk/aws-redshift/test/integ.database.ts index 5938e67652c94..b3997e9e161de 100644 --- a/packages/@aws-cdk/aws-redshift/test/integ.database.ts +++ b/packages/@aws-cdk/aws-redshift/test/integ.database.ts @@ -49,6 +49,7 @@ const table = new redshift.Table(stack, 'Table', { ], distStyle: redshift.TableDistStyle.KEY, sortStyle: redshift.TableSortStyle.INTERLEAVED, + tableComment: 'A test table', }); table.grant(user, redshift.TableAction.INSERT, redshift.TableAction.DELETE); diff --git a/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts b/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts index bd0cc6349f70a..3b4f329e8db7f 100644 --- a/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts +++ b/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts @@ -253,6 +253,7 @@ export class BucketDeployment extends Construct { private readonly cr: cdk.CustomResource; private _deployedBucket?: s3.IBucket; private requestDestinationArn: boolean = false; + private readonly destinationBucket: s3.IBucket; private readonly sources: SourceConfig[]; private readonly handlerRole: iam.IRole; @@ -274,6 +275,8 @@ export class BucketDeployment extends Construct { throw new Error('Vpc must be specified if useEfs is set'); } + this.destinationBucket = props.destinationBucket; + const accessPointPath = '/lambda'; let accessPoint; if (props.useEfs && props.vpc) { @@ -333,9 +336,9 @@ export class BucketDeployment extends Construct { this.sources = props.sources.map((source: ISource) => source.bind(this, { handlerRole: this.handlerRole })); - props.destinationBucket.grantReadWrite(handler); + this.destinationBucket.grantReadWrite(handler); if (props.accessControl) { - props.destinationBucket.grantPutAcl(handler); + this.destinationBucket.grantPutAcl(handler); } if (props.distribution) { handler.addToRolePolicy(new iam.PolicyStatement({ @@ -378,7 +381,7 @@ export class BucketDeployment extends Construct { }, [] as Array>); }, }, { omitEmptyArray: true }), - DestinationBucketName: props.destinationBucket.bucketName, + DestinationBucketName: this.destinationBucket.bucketName, DestinationBucketKeyPrefix: props.destinationKeyPrefix, RetainOnDelete: props.retainOnDelete, Extract: props.extract, @@ -389,8 +392,8 @@ export class BucketDeployment extends Construct { SystemMetadata: mapSystemMetadata(props), DistributionId: props.distribution?.distributionId, DistributionPaths: props.distributionPaths, - // Passing through the ARN sequences dependencees on the deployment - DestinationBucketArn: cdk.Lazy.string({ produce: () => this.requestDestinationArn ? props.destinationBucket.bucketArn : undefined }), + // Passing through the ARN sequences dependency on the deployment + DestinationBucketArn: cdk.Lazy.string({ produce: () => this.requestDestinationArn ? this.destinationBucket.bucketArn : undefined }), }, }); @@ -447,7 +450,7 @@ export class BucketDeployment extends Construct { * want the contents of the bucket to be removed on bucket deletion, then `autoDeleteObjects` property should * be set to true on the Bucket. */ - cdk.Tags.of(props.destinationBucket).add(tagKey, 'true'); + cdk.Tags.of(this.destinationBucket).add(tagKey, 'true'); } @@ -458,11 +461,18 @@ export class BucketDeployment extends Construct { * bucket deployment has happened before the next operation is started, pass the other construct * a reference to `deployment.deployedBucket`. * - * Doing this replaces calling `otherResource.node.addDependency(deployment)`. + * Note that this only returns an immutable reference to the destination bucket. + * If sequenced access to the original destination bucket is required, you may add a dependency + * on the bucket deployment instead: `otherResource.node.addDependency(deployment)` */ public get deployedBucket(): s3.IBucket { this.requestDestinationArn = true; - this._deployedBucket = this._deployedBucket ?? s3.Bucket.fromBucketArn(this, 'DestinationBucket', cdk.Token.asString(this.cr.getAtt('DestinationBucketArn'))); + this._deployedBucket = this._deployedBucket ?? s3.Bucket.fromBucketAttributes(this, 'DestinationBucket', { + bucketArn: cdk.Token.asString(this.cr.getAtt('DestinationBucketArn')), + region: this.destinationBucket.env.region, + account: this.destinationBucket.env.account, + isWebsite: this.destinationBucket.isWebsite, + }); return this._deployedBucket; } diff --git a/packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts b/packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts index 71b2403cb778e..83fcdde0b1982 100644 --- a/packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts +++ b/packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts @@ -1111,6 +1111,32 @@ test('s3 deployment bucket is identical to destination bucket', () => { }); }); +test('s3 deployed bucket in a different region has correct website url', () => { + // GIVEN + const stack = new cdk.Stack(undefined, undefined, { + env: { + region: 'us-east-1', + }, + }); + const bucket = s3.Bucket.fromBucketAttributes(stack, 'Dest', { + bucketName: 'my-bucket', + // Bucket is in a different region than stack + region: 'eu-central-1', + }); + + // WHEN + const bd = new s3deploy.BucketDeployment(stack, 'Deployment', { + destinationBucket: bucket, + sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))], + }); + const websiteUrl = stack.resolve(bd.deployedBucket.bucketWebsiteUrl); + + // THEN + // eu-central-1 uses website endpoint format with a `.` + // see https://docs.aws.amazon.com/general/latest/gr/s3.html#s3_website_region_endpoints + expect(JSON.stringify(websiteUrl)).toContain('.s3-website.eu-central-1.'); +}); + test('using deployment bucket references the destination bucket by means of the CustomResource', () => { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/asset.2bc265c5e0569aeb24a6349c15bd54e76e845892376515e036627ab0cc70bb64/index.py b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/asset.2bc265c5e0569aeb24a6349c15bd54e76e845892376515e036627ab0cc70bb64/index.py new file mode 100644 index 0000000000000..e013fae72f87d --- /dev/null +++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/asset.2bc265c5e0569aeb24a6349c15bd54e76e845892376515e036627ab0cc70bb64/index.py @@ -0,0 +1,308 @@ +import contextlib +import json +import logging +import os +import shutil +import subprocess +import tempfile +from urllib.request import Request, urlopen +from uuid import uuid4 +from zipfile import ZipFile + +import boto3 + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +cloudfront = boto3.client('cloudfront') +s3 = boto3.client('s3') + +CFN_SUCCESS = "SUCCESS" +CFN_FAILED = "FAILED" +ENV_KEY_MOUNT_PATH = "MOUNT_PATH" +ENV_KEY_SKIP_CLEANUP = "SKIP_CLEANUP" + +CUSTOM_RESOURCE_OWNER_TAG = "aws-cdk:cr-owned" + +def handler(event, context): + + def cfn_error(message=None): + logger.error("| cfn_error: %s" % message) + cfn_send(event, context, CFN_FAILED, reason=message) + + try: + # We are not logging ResponseURL as this is a pre-signed S3 URL, and could be used to tamper + # with the response CloudFormation sees from this Custom Resource execution. + logger.info({ key:value for (key, value) in event.items() if key != 'ResponseURL'}) + + # cloudformation request type (create/update/delete) + request_type = event['RequestType'] + + # extract resource properties + props = event['ResourceProperties'] + old_props = event.get('OldResourceProperties', {}) + physical_id = event.get('PhysicalResourceId', None) + + try: + source_bucket_names = props['SourceBucketNames'] + source_object_keys = props['SourceObjectKeys'] + source_markers = props.get('SourceMarkers', None) + dest_bucket_name = props['DestinationBucketName'] + dest_bucket_prefix = props.get('DestinationBucketKeyPrefix', '') + extract = props.get('Extract', 'true') == 'true' + retain_on_delete = props.get('RetainOnDelete', "true") == "true" + distribution_id = props.get('DistributionId', '') + user_metadata = props.get('UserMetadata', {}) + system_metadata = props.get('SystemMetadata', {}) + prune = props.get('Prune', 'true').lower() == 'true' + exclude = props.get('Exclude', []) + include = props.get('Include', []) + + # backwards compatibility - if "SourceMarkers" is not specified, + # assume all sources have an empty market map + if source_markers is None: + source_markers = [{} for i in range(len(source_bucket_names))] + + default_distribution_path = dest_bucket_prefix + if not default_distribution_path.endswith("/"): + default_distribution_path += "/" + if not default_distribution_path.startswith("/"): + default_distribution_path = "/" + default_distribution_path + default_distribution_path += "*" + + distribution_paths = props.get('DistributionPaths', [default_distribution_path]) + except KeyError as e: + cfn_error("missing request resource property %s. props: %s" % (str(e), props)) + return + + # treat "/" as if no prefix was specified + if dest_bucket_prefix == "/": + dest_bucket_prefix = "" + + s3_source_zips = list(map(lambda name, key: "s3://%s/%s" % (name, key), source_bucket_names, source_object_keys)) + s3_dest = "s3://%s/%s" % (dest_bucket_name, dest_bucket_prefix) + old_s3_dest = "s3://%s/%s" % (old_props.get("DestinationBucketName", ""), old_props.get("DestinationBucketKeyPrefix", "")) + + + # obviously this is not + if old_s3_dest == "s3:///": + old_s3_dest = None + + logger.info("| s3_dest: %s" % s3_dest) + logger.info("| old_s3_dest: %s" % old_s3_dest) + + # if we are creating a new resource, allocate a physical id for it + # otherwise, we expect physical id to be relayed by cloudformation + if request_type == "Create": + physical_id = "aws.cdk.s3deployment.%s" % str(uuid4()) + else: + if not physical_id: + cfn_error("invalid request: request type is '%s' but 'PhysicalResourceId' is not defined" % request_type) + return + + # delete or create/update (only if "retain_on_delete" is false) + if request_type == "Delete" and not retain_on_delete: + if not bucket_owned(dest_bucket_name, dest_bucket_prefix): + aws_command("s3", "rm", s3_dest, "--recursive") + + # if we are updating without retention and the destination changed, delete first + if request_type == "Update" and not retain_on_delete and old_s3_dest != s3_dest: + if not old_s3_dest: + logger.warn("cannot delete old resource without old resource properties") + return + + aws_command("s3", "rm", old_s3_dest, "--recursive") + + if request_type == "Update" or request_type == "Create": + s3_deploy(s3_source_zips, s3_dest, user_metadata, system_metadata, prune, exclude, include, source_markers, extract) + + if distribution_id: + cloudfront_invalidate(distribution_id, distribution_paths) + + cfn_send(event, context, CFN_SUCCESS, physicalResourceId=physical_id, responseData={ + # Passing through the ARN sequences dependencees on the deployment + 'DestinationBucketArn': props.get('DestinationBucketArn'), + 'SourceObjectKeys': props.get('SourceObjectKeys'), + }) + except KeyError as e: + cfn_error("invalid request. Missing key %s" % str(e)) + except Exception as e: + logger.exception(e) + cfn_error(str(e)) + +#--------------------------------------------------------------------------------------------------- +# populate all files from s3_source_zips to a destination bucket +def s3_deploy(s3_source_zips, s3_dest, user_metadata, system_metadata, prune, exclude, include, source_markers, extract): + # list lengths are equal + if len(s3_source_zips) != len(source_markers): + raise Exception("'source_markers' and 's3_source_zips' must be the same length") + + # create a temporary working directory in /tmp or if enabled an attached efs volume + if ENV_KEY_MOUNT_PATH in os.environ: + workdir = os.getenv(ENV_KEY_MOUNT_PATH) + "/" + str(uuid4()) + os.mkdir(workdir) + else: + workdir = tempfile.mkdtemp() + + logger.info("| workdir: %s" % workdir) + + # create a directory into which we extract the contents of the zip file + contents_dir=os.path.join(workdir, 'contents') + os.mkdir(contents_dir) + + try: + # download the archive from the source and extract to "contents" + for i in range(len(s3_source_zips)): + s3_source_zip = s3_source_zips[i] + markers = source_markers[i] + + if extract: + archive=os.path.join(workdir, str(uuid4())) + logger.info("archive: %s" % archive) + aws_command("s3", "cp", s3_source_zip, archive) + logger.info("| extracting archive to: %s\n" % contents_dir) + logger.info("| markers: %s" % markers) + extract_and_replace_markers(archive, contents_dir, markers) + else: + logger.info("| copying archive to: %s\n" % contents_dir) + aws_command("s3", "cp", s3_source_zip, contents_dir) + + # sync from "contents" to destination + + s3_command = ["s3", "sync"] + + if prune: + s3_command.append("--delete") + + if exclude: + for filter in exclude: + s3_command.extend(["--exclude", filter]) + + if include: + for filter in include: + s3_command.extend(["--include", filter]) + + s3_command.extend([contents_dir, s3_dest]) + s3_command.extend(create_metadata_args(user_metadata, system_metadata)) + aws_command(*s3_command) + finally: + if not os.getenv(ENV_KEY_SKIP_CLEANUP): + shutil.rmtree(workdir) + +#--------------------------------------------------------------------------------------------------- +# invalidate files in the CloudFront distribution edge caches +def cloudfront_invalidate(distribution_id, distribution_paths): + invalidation_resp = cloudfront.create_invalidation( + DistributionId=distribution_id, + InvalidationBatch={ + 'Paths': { + 'Quantity': len(distribution_paths), + 'Items': distribution_paths + }, + 'CallerReference': str(uuid4()), + }) + # by default, will wait up to 10 minutes + cloudfront.get_waiter('invalidation_completed').wait( + DistributionId=distribution_id, + Id=invalidation_resp['Invalidation']['Id']) + +#--------------------------------------------------------------------------------------------------- +# set metadata +def create_metadata_args(raw_user_metadata, raw_system_metadata): + if len(raw_user_metadata) == 0 and len(raw_system_metadata) == 0: + return [] + + format_system_metadata_key = lambda k: k.lower() + format_user_metadata_key = lambda k: k.lower() + + system_metadata = { format_system_metadata_key(k): v for k, v in raw_system_metadata.items() } + user_metadata = { format_user_metadata_key(k): v for k, v in raw_user_metadata.items() } + + flatten = lambda l: [item for sublist in l for item in sublist] + system_args = flatten([[f"--{k}", v] for k, v in system_metadata.items()]) + user_args = ["--metadata", json.dumps(user_metadata, separators=(',', ':'))] if len(user_metadata) > 0 else [] + + return system_args + user_args + ["--metadata-directive", "REPLACE"] + +#--------------------------------------------------------------------------------------------------- +# executes an "aws" cli command +def aws_command(*args): + aws="/opt/awscli/aws" # from AwsCliLayer + logger.info("| aws %s" % ' '.join(args)) + subprocess.check_call([aws] + list(args)) + +#--------------------------------------------------------------------------------------------------- +# sends a response to cloudformation +def cfn_send(event, context, responseStatus, responseData={}, physicalResourceId=None, noEcho=False, reason=None): + + responseUrl = event['ResponseURL'] + + responseBody = {} + responseBody['Status'] = responseStatus + responseBody['Reason'] = reason or ('See the details in CloudWatch Log Stream: ' + context.log_stream_name) + responseBody['PhysicalResourceId'] = physicalResourceId or context.log_stream_name + responseBody['StackId'] = event['StackId'] + responseBody['RequestId'] = event['RequestId'] + responseBody['LogicalResourceId'] = event['LogicalResourceId'] + responseBody['NoEcho'] = noEcho + responseBody['Data'] = responseData + + body = json.dumps(responseBody) + logger.info("| response body:\n" + body) + + headers = { + 'content-type' : '', + 'content-length' : str(len(body)) + } + + try: + request = Request(responseUrl, method='PUT', data=bytes(body.encode('utf-8')), headers=headers) + with contextlib.closing(urlopen(request)) as response: + logger.info("| status code: " + response.reason) + except Exception as e: + logger.error("| unable to send response to CloudFormation") + logger.exception(e) + + +#--------------------------------------------------------------------------------------------------- +# check if bucket is owned by a custom resource +# if it is then we don't want to delete content +def bucket_owned(bucketName, keyPrefix): + tag = CUSTOM_RESOURCE_OWNER_TAG + if keyPrefix != "": + tag = tag + ':' + keyPrefix + try: + request = s3.get_bucket_tagging( + Bucket=bucketName, + ) + return any((x["Key"].startswith(tag)) for x in request["TagSet"]) + except Exception as e: + logger.info("| error getting tags from bucket") + logger.exception(e) + return False + +# extract archive and replace markers in output files +def extract_and_replace_markers(archive, contents_dir, markers): + with ZipFile(archive, "r") as zip: + zip.extractall(contents_dir) + + # replace markers for this source + for file in zip.namelist(): + file_path = os.path.join(contents_dir, file) + if os.path.isdir(file_path): continue + replace_markers(file_path, markers) + +def replace_markers(filename, markers): + # convert the dict of string markers to binary markers + replace_tokens = dict([(k.encode('utf-8'), v.encode('utf-8')) for k, v in markers.items()]) + + outfile = filename + '.new' + with open(filename, 'rb') as fi, open(outfile, 'wb') as fo: + for line in fi: + for token in replace_tokens: + line = line.replace(token, replace_tokens[token]) + fo.write(line) + + # # delete the original file and rename the new one to the original + os.remove(filename) + os.rename(outfile, filename) diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/asset.33e2651435a0d472a75c1e033c9832b21321d9e56711926b04c5705e5f63874c/__entrypoint__.js b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/asset.33e2651435a0d472a75c1e033c9832b21321d9e56711926b04c5705e5f63874c/__entrypoint__.js new file mode 100644 index 0000000000000..1e3a3093c1706 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/asset.33e2651435a0d472a75c1e033c9832b21321d9e56711926b04c5705e5f63874c/__entrypoint__.js @@ -0,0 +1,144 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withRetries = exports.handler = exports.external = void 0; +const https = require("https"); +const url = require("url"); +// for unit tests +exports.external = { + sendHttpRequest: defaultSendHttpRequest, + log: defaultLog, + includeStackTraces: true, + userHandlerIndex: './index', +}; +const CREATE_FAILED_PHYSICAL_ID_MARKER = 'AWSCDK::CustomResourceProviderFramework::CREATE_FAILED'; +const MISSING_PHYSICAL_ID_MARKER = 'AWSCDK::CustomResourceProviderFramework::MISSING_PHYSICAL_ID'; +async function handler(event, context) { + const sanitizedEvent = { ...event, ResponseURL: '...' }; + exports.external.log(JSON.stringify(sanitizedEvent, undefined, 2)); + // ignore DELETE event when the physical resource ID is the marker that + // indicates that this DELETE is a subsequent DELETE to a failed CREATE + // operation. + if (event.RequestType === 'Delete' && event.PhysicalResourceId === CREATE_FAILED_PHYSICAL_ID_MARKER) { + exports.external.log('ignoring DELETE event caused by a failed CREATE event'); + await submitResponse('SUCCESS', event); + return; + } + try { + // invoke the user handler. this is intentionally inside the try-catch to + // ensure that if there is an error it's reported as a failure to + // cloudformation (otherwise cfn waits). + // eslint-disable-next-line @typescript-eslint/no-require-imports + const userHandler = require(exports.external.userHandlerIndex).handler; + const result = await userHandler(sanitizedEvent, context); + // validate user response and create the combined event + const responseEvent = renderResponse(event, result); + // submit to cfn as success + await submitResponse('SUCCESS', responseEvent); + } + catch (e) { + const resp = { + ...event, + Reason: exports.external.includeStackTraces ? e.stack : e.message, + }; + if (!resp.PhysicalResourceId) { + // special case: if CREATE fails, which usually implies, we usually don't + // have a physical resource id. in this case, the subsequent DELETE + // operation does not have any meaning, and will likely fail as well. to + // address this, we use a marker so the provider framework can simply + // ignore the subsequent DELETE. + if (event.RequestType === 'Create') { + exports.external.log('CREATE failed, responding with a marker physical resource id so that the subsequent DELETE will be ignored'); + resp.PhysicalResourceId = CREATE_FAILED_PHYSICAL_ID_MARKER; + } + else { + // otherwise, if PhysicalResourceId is not specified, something is + // terribly wrong because all other events should have an ID. + exports.external.log(`ERROR: Malformed event. "PhysicalResourceId" is required: ${JSON.stringify(event)}`); + } + } + // this is an actual error, fail the activity altogether and exist. + await submitResponse('FAILED', resp); + } +} +exports.handler = handler; +function renderResponse(cfnRequest, handlerResponse = {}) { + // if physical ID is not returned, we have some defaults for you based + // on the request type. + const physicalResourceId = handlerResponse.PhysicalResourceId ?? cfnRequest.PhysicalResourceId ?? cfnRequest.RequestId; + // if we are in DELETE and physical ID was changed, it's an error. + if (cfnRequest.RequestType === 'Delete' && physicalResourceId !== cfnRequest.PhysicalResourceId) { + throw new Error(`DELETE: cannot change the physical resource ID from "${cfnRequest.PhysicalResourceId}" to "${handlerResponse.PhysicalResourceId}" during deletion`); + } + // merge request event and result event (result prevails). + return { + ...cfnRequest, + ...handlerResponse, + PhysicalResourceId: physicalResourceId, + }; +} +async function submitResponse(status, event) { + const json = { + Status: status, + Reason: event.Reason ?? status, + StackId: event.StackId, + RequestId: event.RequestId, + PhysicalResourceId: event.PhysicalResourceId || MISSING_PHYSICAL_ID_MARKER, + LogicalResourceId: event.LogicalResourceId, + NoEcho: event.NoEcho, + Data: event.Data, + }; + exports.external.log('submit response to cloudformation', json); + const responseBody = JSON.stringify(json); + const parsedUrl = url.parse(event.ResponseURL); + const req = { + hostname: parsedUrl.hostname, + path: parsedUrl.path, + method: 'PUT', + headers: { 'content-type': '', 'content-length': responseBody.length }, + }; + const retryOptions = { + attempts: 5, + sleep: 1000, + }; + await withRetries(retryOptions, exports.external.sendHttpRequest)(req, responseBody); +} +async function defaultSendHttpRequest(options, responseBody) { + return new Promise((resolve, reject) => { + try { + const request = https.request(options, _ => resolve()); + request.on('error', reject); + request.write(responseBody); + request.end(); + } + catch (e) { + reject(e); + } + }); +} +function defaultLog(fmt, ...params) { + // eslint-disable-next-line no-console + console.log(fmt, ...params); +} +function withRetries(options, fn) { + return async (...xs) => { + let attempts = options.attempts; + let ms = options.sleep; + while (true) { + try { + return await fn(...xs); + } + catch (e) { + if (attempts-- <= 0) { + throw e; + } + await sleep(Math.floor(Math.random() * ms)); + ms *= 2; + } + } + }; +} +exports.withRetries = withRetries; +async function sleep(ms) { + return new Promise((ok) => setTimeout(ok, ms)); +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibm9kZWpzLWVudHJ5cG9pbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJub2RlanMtZW50cnlwb2ludC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSwrQkFBK0I7QUFDL0IsMkJBQTJCO0FBRTNCLGlCQUFpQjtBQUNKLFFBQUEsUUFBUSxHQUFHO0lBQ3RCLGVBQWUsRUFBRSxzQkFBc0I7SUFDdkMsR0FBRyxFQUFFLFVBQVU7SUFDZixrQkFBa0IsRUFBRSxJQUFJO0lBQ3hCLGdCQUFnQixFQUFFLFNBQVM7Q0FDNUIsQ0FBQztBQUVGLE1BQU0sZ0NBQWdDLEdBQUcsd0RBQXdELENBQUM7QUFDbEcsTUFBTSwwQkFBMEIsR0FBRyw4REFBOEQsQ0FBQztBQVczRixLQUFLLFVBQVUsT0FBTyxDQUFDLEtBQWtELEVBQUUsT0FBMEI7SUFDMUcsTUFBTSxjQUFjLEdBQUcsRUFBRSxHQUFHLEtBQUssRUFBRSxXQUFXLEVBQUUsS0FBSyxFQUFFLENBQUM7SUFDeEQsZ0JBQVEsQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxjQUFjLEVBQUUsU0FBUyxFQUFFLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFFM0QsdUVBQXVFO0lBQ3ZFLHVFQUF1RTtJQUN2RSxhQUFhO0lBQ2IsSUFBSSxLQUFLLENBQUMsV0FBVyxLQUFLLFFBQVEsSUFBSSxLQUFLLENBQUMsa0JBQWtCLEtBQUssZ0NBQWdDLEVBQUU7UUFDbkcsZ0JBQVEsQ0FBQyxHQUFHLENBQUMsdURBQXVELENBQUMsQ0FBQztRQUN0RSxNQUFNLGNBQWMsQ0FBQyxTQUFTLEVBQUUsS0FBSyxDQUFDLENBQUM7UUFDdkMsT0FBTztLQUNSO0lBRUQsSUFBSTtRQUNGLHlFQUF5RTtRQUN6RSxpRUFBaUU7UUFDakUsd0NBQXdDO1FBQ3hDLGlFQUFpRTtRQUNqRSxNQUFNLFdBQVcsR0FBWSxPQUFPLENBQUMsZ0JBQVEsQ0FBQyxnQkFBZ0IsQ0FBQyxDQUFDLE9BQU8sQ0FBQztRQUN4RSxNQUFNLE1BQU0sR0FBRyxNQUFNLFdBQVcsQ0FBQyxjQUFjLEVBQUUsT0FBTyxDQUFDLENBQUM7UUFFMUQsdURBQXVEO1FBQ3ZELE1BQU0sYUFBYSxHQUFHLGNBQWMsQ0FBQyxLQUFLLEVBQUUsTUFBTSxDQUFDLENBQUM7UUFFcEQsMkJBQTJCO1FBQzNCLE1BQU0sY0FBYyxDQUFDLFNBQVMsRUFBRSxhQUFhLENBQUMsQ0FBQztLQUNoRDtJQUFDLE9BQU8sQ0FBQyxFQUFFO1FBQ1YsTUFBTSxJQUFJLEdBQWE7WUFDckIsR0FBRyxLQUFLO1lBQ1IsTUFBTSxFQUFFLGdCQUFRLENBQUMsa0JBQWtCLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxPQUFPO1NBQzFELENBQUM7UUFFRixJQUFJLENBQUMsSUFBSSxDQUFDLGtCQUFrQixFQUFFO1lBQzVCLHlFQUF5RTtZQUN6RSxtRUFBbUU7WUFDbkUsd0VBQXdFO1lBQ3hFLHFFQUFxRTtZQUNyRSxnQ0FBZ0M7WUFDaEMsSUFBSSxLQUFLLENBQUMsV0FBVyxLQUFLLFFBQVEsRUFBRTtnQkFDbEMsZ0JBQVEsQ0FBQyxHQUFHLENBQUMsNEdBQTRHLENBQUMsQ0FBQztnQkFDM0gsSUFBSSxDQUFDLGtCQUFrQixHQUFHLGdDQUFnQyxDQUFDO2FBQzVEO2lCQUFNO2dCQUNMLGtFQUFrRTtnQkFDbEUsNkRBQTZEO2dCQUM3RCxnQkFBUSxDQUFDLEdBQUcsQ0FBQyw2REFBNkQsSUFBSSxDQUFDLFNBQVMsQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDLENBQUM7YUFDcEc7U0FDRjtRQUVELG1FQUFtRTtRQUNuRSxNQUFNLGNBQWMsQ0FBQyxRQUFRLEVBQUUsSUFBSSxDQUFDLENBQUM7S0FDdEM7QUFDSCxDQUFDO0FBbkRELDBCQW1EQztBQUVELFNBQVMsY0FBYyxDQUNyQixVQUF5RixFQUN6RixrQkFBMEMsRUFBRztJQUU3QyxzRUFBc0U7SUFDdEUsdUJBQXVCO0lBQ3ZCLE1BQU0sa0JBQWtCLEdBQUcsZUFBZSxDQUFDLGtCQUFrQixJQUFJLFVBQVUsQ0FBQyxrQkFBa0IsSUFBSSxVQUFVLENBQUMsU0FBUyxDQUFDO0lBRXZILGtFQUFrRTtJQUNsRSxJQUFJLFVBQVUsQ0FBQyxXQUFXLEtBQUssUUFBUSxJQUFJLGtCQUFrQixLQUFLLFVBQVUsQ0FBQyxrQkFBa0IsRUFBRTtRQUMvRixNQUFNLElBQUksS0FBSyxDQUFDLHdEQUF3RCxVQUFVLENBQUMsa0JBQWtCLFNBQVMsZUFBZSxDQUFDLGtCQUFrQixtQkFBbUIsQ0FBQyxDQUFDO0tBQ3RLO0lBRUQsMERBQTBEO0lBQzFELE9BQU87UUFDTCxHQUFHLFVBQVU7UUFDYixHQUFHLGVBQWU7UUFDbEIsa0JBQWtCLEVBQUUsa0JBQWtCO0tBQ3ZDLENBQUM7QUFDSixDQUFDO0FBRUQsS0FBSyxVQUFVLGNBQWMsQ0FBQyxNQUE0QixFQUFFLEtBQWU7SUFDekUsTUFBTSxJQUFJLEdBQW1EO1FBQzNELE1BQU0sRUFBRSxNQUFNO1FBQ2QsTUFBTSxFQUFFLEtBQUssQ0FBQyxNQUFNLElBQUksTUFBTTtRQUM5QixPQUFPLEVBQUUsS0FBSyxDQUFDLE9BQU87UUFDdEIsU0FBUyxFQUFFLEtBQUssQ0FBQyxTQUFTO1FBQzFCLGtCQUFrQixFQUFFLEtBQUssQ0FBQyxrQkFBa0IsSUFBSSwwQkFBMEI7UUFDMUUsaUJBQWlCLEVBQUUsS0FBSyxDQUFDLGlCQUFpQjtRQUMxQyxNQUFNLEVBQUUsS0FBSyxDQUFDLE1BQU07UUFDcEIsSUFBSSxFQUFFLEtBQUssQ0FBQyxJQUFJO0tBQ2pCLENBQUM7SUFFRixnQkFBUSxDQUFDLEdBQUcsQ0FBQyxtQ0FBbUMsRUFBRSxJQUFJLENBQUMsQ0FBQztJQUV4RCxNQUFNLFlBQVksR0FBRyxJQUFJLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxDQUFDO0lBQzFDLE1BQU0sU0FBUyxHQUFHLEdBQUcsQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLFdBQVcsQ0FBQyxDQUFDO0lBQy9DLE1BQU0sR0FBRyxHQUFHO1FBQ1YsUUFBUSxFQUFFLFNBQVMsQ0FBQyxRQUFRO1FBQzVCLElBQUksRUFBRSxTQUFTLENBQUMsSUFBSTtRQUNwQixNQUFNLEVBQUUsS0FBSztRQUNiLE9BQU8sRUFBRSxFQUFFLGNBQWMsRUFBRSxFQUFFLEVBQUUsZ0JBQWdCLEVBQUUsWUFBWSxDQUFDLE1BQU0sRUFBRTtLQUN2RSxDQUFDO0lBRUYsTUFBTSxZQUFZLEdBQUc7UUFDbkIsUUFBUSxFQUFFLENBQUM7UUFDWCxLQUFLLEVBQUUsSUFBSTtLQUNaLENBQUM7SUFDRixNQUFNLFdBQVcsQ0FBQyxZQUFZLEVBQUUsZ0JBQVEsQ0FBQyxlQUFlLENBQUMsQ0FBQyxHQUFHLEVBQUUsWUFBWSxDQUFDLENBQUM7QUFDL0UsQ0FBQztBQUVELEtBQUssVUFBVSxzQkFBc0IsQ0FBQyxPQUE2QixFQUFFLFlBQW9CO0lBQ3ZGLE9BQU8sSUFBSSxPQUFPLENBQUMsQ0FBQyxPQUFPLEVBQUUsTUFBTSxFQUFFLEVBQUU7UUFDckMsSUFBSTtZQUNGLE1BQU0sT0FBTyxHQUFHLEtBQUssQ0FBQyxPQUFPLENBQUMsT0FBTyxFQUFFLENBQUMsQ0FBQyxFQUFFLENBQUMsT0FBTyxFQUFFLENBQUMsQ0FBQztZQUN2RCxPQUFPLENBQUMsRUFBRSxDQUFDLE9BQU8sRUFBRSxNQUFNLENBQUMsQ0FBQztZQUM1QixPQUFPLENBQUMsS0FBSyxDQUFDLFlBQVksQ0FBQyxDQUFDO1lBQzVCLE9BQU8sQ0FBQyxHQUFHLEVBQUUsQ0FBQztTQUNmO1FBQUMsT0FBTyxDQUFDLEVBQUU7WUFDVixNQUFNLENBQUMsQ0FBQyxDQUFDLENBQUM7U0FDWDtJQUNILENBQUMsQ0FBQyxDQUFDO0FBQ0wsQ0FBQztBQUVELFNBQVMsVUFBVSxDQUFDLEdBQVcsRUFBRSxHQUFHLE1BQWE7SUFDL0Msc0NBQXNDO0lBQ3RDLE9BQU8sQ0FBQyxHQUFHLENBQUMsR0FBRyxFQUFFLEdBQUcsTUFBTSxDQUFDLENBQUM7QUFDOUIsQ0FBQztBQVNELFNBQWdCLFdBQVcsQ0FBMEIsT0FBcUIsRUFBRSxFQUE0QjtJQUN0RyxPQUFPLEtBQUssRUFBRSxHQUFHLEVBQUssRUFBRSxFQUFFO1FBQ3hCLElBQUksUUFBUSxHQUFHLE9BQU8sQ0FBQyxRQUFRLENBQUM7UUFDaEMsSUFBSSxFQUFFLEdBQUcsT0FBTyxDQUFDLEtBQUssQ0FBQztRQUN2QixPQUFPLElBQUksRUFBRTtZQUNYLElBQUk7Z0JBQ0YsT0FBTyxNQUFNLEVBQUUsQ0FBQyxHQUFHLEVBQUUsQ0FBQyxDQUFDO2FBQ3hCO1lBQUMsT0FBTyxDQUFDLEVBQUU7Z0JBQ1YsSUFBSSxRQUFRLEVBQUUsSUFBSSxDQUFDLEVBQUU7b0JBQ25CLE1BQU0sQ0FBQyxDQUFDO2lCQUNUO2dCQUNELE1BQU0sS0FBSyxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLE1BQU0sRUFBRSxHQUFHLEVBQUUsQ0FBQyxDQUFDLENBQUM7Z0JBQzVDLEVBQUUsSUFBSSxDQUFDLENBQUM7YUFDVDtTQUNGO0lBQ0gsQ0FBQyxDQUFDO0FBQ0osQ0FBQztBQWhCRCxrQ0FnQkM7QUFFRCxLQUFLLFVBQVUsS0FBSyxDQUFDLEVBQVU7SUFDN0IsT0FBTyxJQUFJLE9BQU8sQ0FBQyxDQUFDLEVBQUUsRUFBRSxFQUFFLENBQUMsVUFBVSxDQUFDLEVBQUUsRUFBRSxFQUFFLENBQUMsQ0FBQyxDQUFDO0FBQ2pELENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBodHRwcyBmcm9tICdodHRwcyc7XG5pbXBvcnQgKiBhcyB1cmwgZnJvbSAndXJsJztcblxuLy8gZm9yIHVuaXQgdGVzdHNcbmV4cG9ydCBjb25zdCBleHRlcm5hbCA9IHtcbiAgc2VuZEh0dHBSZXF1ZXN0OiBkZWZhdWx0U2VuZEh0dHBSZXF1ZXN0LFxuICBsb2c6IGRlZmF1bHRMb2csXG4gIGluY2x1ZGVTdGFja1RyYWNlczogdHJ1ZSxcbiAgdXNlckhhbmRsZXJJbmRleDogJy4vaW5kZXgnLFxufTtcblxuY29uc3QgQ1JFQVRFX0ZBSUxFRF9QSFlTSUNBTF9JRF9NQVJLRVIgPSAnQVdTQ0RLOjpDdXN0b21SZXNvdXJjZVByb3ZpZGVyRnJhbWV3b3JrOjpDUkVBVEVfRkFJTEVEJztcbmNvbnN0IE1JU1NJTkdfUEhZU0lDQUxfSURfTUFSS0VSID0gJ0FXU0NESzo6Q3VzdG9tUmVzb3VyY2VQcm92aWRlckZyYW1ld29yazo6TUlTU0lOR19QSFlTSUNBTF9JRCc7XG5cbmV4cG9ydCB0eXBlIFJlc3BvbnNlID0gQVdTTGFtYmRhLkNsb3VkRm9ybWF0aW9uQ3VzdG9tUmVzb3VyY2VFdmVudCAmIEhhbmRsZXJSZXNwb25zZTtcbmV4cG9ydCB0eXBlIEhhbmRsZXIgPSAoZXZlbnQ6IEFXU0xhbWJkYS5DbG91ZEZvcm1hdGlvbkN1c3RvbVJlc291cmNlRXZlbnQsIGNvbnRleHQ6IEFXU0xhbWJkYS5Db250ZXh0KSA9PiBQcm9taXNlPEhhbmRsZXJSZXNwb25zZSB8IHZvaWQ+O1xuZXhwb3J0IHR5cGUgSGFuZGxlclJlc3BvbnNlID0gdW5kZWZpbmVkIHwge1xuICBEYXRhPzogYW55O1xuICBQaHlzaWNhbFJlc291cmNlSWQ/OiBzdHJpbmc7XG4gIFJlYXNvbj86IHN0cmluZztcbiAgTm9FY2hvPzogYm9vbGVhbjtcbn07XG5cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBoYW5kbGVyKGV2ZW50OiBBV1NMYW1iZGEuQ2xvdWRGb3JtYXRpb25DdXN0b21SZXNvdXJjZUV2ZW50LCBjb250ZXh0OiBBV1NMYW1iZGEuQ29udGV4dCkge1xuICBjb25zdCBzYW5pdGl6ZWRFdmVudCA9IHsgLi4uZXZlbnQsIFJlc3BvbnNlVVJMOiAnLi4uJyB9O1xuICBleHRlcm5hbC5sb2coSlNPTi5zdHJpbmdpZnkoc2FuaXRpemVkRXZlbnQsIHVuZGVmaW5lZCwgMikpO1xuXG4gIC8vIGlnbm9yZSBERUxFVEUgZXZlbnQgd2hlbiB0aGUgcGh5c2ljYWwgcmVzb3VyY2UgSUQgaXMgdGhlIG1hcmtlciB0aGF0XG4gIC8vIGluZGljYXRlcyB0aGF0IHRoaXMgREVMRVRFIGlzIGEgc3Vic2VxdWVudCBERUxFVEUgdG8gYSBmYWlsZWQgQ1JFQVRFXG4gIC8vIG9wZXJhdGlvbi5cbiAgaWYgKGV2ZW50LlJlcXVlc3RUeXBlID09PSAnRGVsZXRlJyAmJiBldmVudC5QaHlzaWNhbFJlc291cmNlSWQgPT09IENSRUFURV9GQUlMRURfUEhZU0lDQUxfSURfTUFSS0VSKSB7XG4gICAgZXh0ZXJuYWwubG9nKCdpZ25vcmluZyBERUxFVEUgZXZlbnQgY2F1c2VkIGJ5IGEgZmFpbGVkIENSRUFURSBldmVudCcpO1xuICAgIGF3YWl0IHN1Ym1pdFJlc3BvbnNlKCdTVUNDRVNTJywgZXZlbnQpO1xuICAgIHJldHVybjtcbiAgfVxuXG4gIHRyeSB7XG4gICAgLy8gaW52b2tlIHRoZSB1c2VyIGhhbmRsZXIuIHRoaXMgaXMgaW50ZW50aW9uYWxseSBpbnNpZGUgdGhlIHRyeS1jYXRjaCB0b1xuICAgIC8vIGVuc3VyZSB0aGF0IGlmIHRoZXJlIGlzIGFuIGVycm9yIGl0J3MgcmVwb3J0ZWQgYXMgYSBmYWlsdXJlIHRvXG4gICAgLy8gY2xvdWRmb3JtYXRpb24gKG90aGVyd2lzZSBjZm4gd2FpdHMpLlxuICAgIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBAdHlwZXNjcmlwdC1lc2xpbnQvbm8tcmVxdWlyZS1pbXBvcnRzXG4gICAgY29uc3QgdXNlckhhbmRsZXI6IEhhbmRsZXIgPSByZXF1aXJlKGV4dGVybmFsLnVzZXJIYW5kbGVySW5kZXgpLmhhbmRsZXI7XG4gICAgY29uc3QgcmVzdWx0ID0gYXdhaXQgdXNlckhhbmRsZXIoc2FuaXRpemVkRXZlbnQsIGNvbnRleHQpO1xuXG4gICAgLy8gdmFsaWRhdGUgdXNlciByZXNwb25zZSBhbmQgY3JlYXRlIHRoZSBjb21iaW5lZCBldmVudFxuICAgIGNvbnN0IHJlc3BvbnNlRXZlbnQgPSByZW5kZXJSZXNwb25zZShldmVudCwgcmVzdWx0KTtcblxuICAgIC8vIHN1Ym1pdCB0byBjZm4gYXMgc3VjY2Vzc1xuICAgIGF3YWl0IHN1Ym1pdFJlc3BvbnNlKCdTVUNDRVNTJywgcmVzcG9uc2VFdmVudCk7XG4gIH0gY2F0Y2ggKGUpIHtcbiAgICBjb25zdCByZXNwOiBSZXNwb25zZSA9IHtcbiAgICAgIC4uLmV2ZW50LFxuICAgICAgUmVhc29uOiBleHRlcm5hbC5pbmNsdWRlU3RhY2tUcmFjZXMgPyBlLnN0YWNrIDogZS5tZXNzYWdlLFxuICAgIH07XG5cbiAgICBpZiAoIXJlc3AuUGh5c2ljYWxSZXNvdXJjZUlkKSB7XG4gICAgICAvLyBzcGVjaWFsIGNhc2U6IGlmIENSRUFURSBmYWlscywgd2hpY2ggdXN1YWxseSBpbXBsaWVzLCB3ZSB1c3VhbGx5IGRvbid0XG4gICAgICAvLyBoYXZlIGEgcGh5c2ljYWwgcmVzb3VyY2UgaWQuIGluIHRoaXMgY2FzZSwgdGhlIHN1YnNlcXVlbnQgREVMRVRFXG4gICAgICAvLyBvcGVyYXRpb24gZG9lcyBub3QgaGF2ZSBhbnkgbWVhbmluZywgYW5kIHdpbGwgbGlrZWx5IGZhaWwgYXMgd2VsbC4gdG9cbiAgICAgIC8vIGFkZHJlc3MgdGhpcywgd2UgdXNlIGEgbWFya2VyIHNvIHRoZSBwcm92aWRlciBmcmFtZXdvcmsgY2FuIHNpbXBseVxuICAgICAgLy8gaWdub3JlIHRoZSBzdWJzZXF1ZW50IERFTEVURS5cbiAgICAgIGlmIChldmVudC5SZXF1ZXN0VHlwZSA9PT0gJ0NyZWF0ZScpIHtcbiAgICAgICAgZXh0ZXJuYWwubG9nKCdDUkVBVEUgZmFpbGVkLCByZXNwb25kaW5nIHdpdGggYSBtYXJrZXIgcGh5c2ljYWwgcmVzb3VyY2UgaWQgc28gdGhhdCB0aGUgc3Vic2VxdWVudCBERUxFVEUgd2lsbCBiZSBpZ25vcmVkJyk7XG4gICAgICAgIHJlc3AuUGh5c2ljYWxSZXNvdXJjZUlkID0gQ1JFQVRFX0ZBSUxFRF9QSFlTSUNBTF9JRF9NQVJLRVI7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICAvLyBvdGhlcndpc2UsIGlmIFBoeXNpY2FsUmVzb3VyY2VJZCBpcyBub3Qgc3BlY2lmaWVkLCBzb21ldGhpbmcgaXNcbiAgICAgICAgLy8gdGVycmlibHkgd3JvbmcgYmVjYXVzZSBhbGwgb3RoZXIgZXZlbnRzIHNob3VsZCBoYXZlIGFuIElELlxuICAgICAgICBleHRlcm5hbC5sb2coYEVSUk9SOiBNYWxmb3JtZWQgZXZlbnQuIFwiUGh5c2ljYWxSZXNvdXJjZUlkXCIgaXMgcmVxdWlyZWQ6ICR7SlNPTi5zdHJpbmdpZnkoZXZlbnQpfWApO1xuICAgICAgfVxuICAgIH1cblxuICAgIC8vIHRoaXMgaXMgYW4gYWN0dWFsIGVycm9yLCBmYWlsIHRoZSBhY3Rpdml0eSBhbHRvZ2V0aGVyIGFuZCBleGlzdC5cbiAgICBhd2FpdCBzdWJtaXRSZXNwb25zZSgnRkFJTEVEJywgcmVzcCk7XG4gIH1cbn1cblxuZnVuY3Rpb24gcmVuZGVyUmVzcG9uc2UoXG4gIGNmblJlcXVlc3Q6IEFXU0xhbWJkYS5DbG91ZEZvcm1hdGlvbkN1c3RvbVJlc291cmNlRXZlbnQgJiB7IFBoeXNpY2FsUmVzb3VyY2VJZD86IHN0cmluZyB9LFxuICBoYW5kbGVyUmVzcG9uc2U6IHZvaWQgfCBIYW5kbGVyUmVzcG9uc2UgPSB7IH0pOiBSZXNwb25zZSB7XG5cbiAgLy8gaWYgcGh5c2ljYWwgSUQgaXMgbm90IHJldHVybmVkLCB3ZSBoYXZlIHNvbWUgZGVmYXVsdHMgZm9yIHlvdSBiYXNlZFxuICAvLyBvbiB0aGUgcmVxdWVzdCB0eXBlLlxuICBjb25zdCBwaHlzaWNhbFJlc291cmNlSWQgPSBoYW5kbGVyUmVzcG9uc2UuUGh5c2ljYWxSZXNvdXJjZUlkID8/IGNmblJlcXVlc3QuUGh5c2ljYWxSZXNvdXJjZUlkID8/IGNmblJlcXVlc3QuUmVxdWVzdElkO1xuXG4gIC8vIGlmIHdlIGFyZSBpbiBERUxFVEUgYW5kIHBoeXNpY2FsIElEIHdhcyBjaGFuZ2VkLCBpdCdzIGFuIGVycm9yLlxuICBpZiAoY2ZuUmVxdWVzdC5SZXF1ZXN0VHlwZSA9PT0gJ0RlbGV0ZScgJiYgcGh5c2ljYWxSZXNvdXJjZUlkICE9PSBjZm5SZXF1ZXN0LlBoeXNpY2FsUmVzb3VyY2VJZCkge1xuICAgIHRocm93IG5ldyBFcnJvcihgREVMRVRFOiBjYW5ub3QgY2hhbmdlIHRoZSBwaHlzaWNhbCByZXNvdXJjZSBJRCBmcm9tIFwiJHtjZm5SZXF1ZXN0LlBoeXNpY2FsUmVzb3VyY2VJZH1cIiB0byBcIiR7aGFuZGxlclJlc3BvbnNlLlBoeXNpY2FsUmVzb3VyY2VJZH1cIiBkdXJpbmcgZGVsZXRpb25gKTtcbiAgfVxuXG4gIC8vIG1lcmdlIHJlcXVlc3QgZXZlbnQgYW5kIHJlc3VsdCBldmVudCAocmVzdWx0IHByZXZhaWxzKS5cbiAgcmV0dXJuIHtcbiAgICAuLi5jZm5SZXF1ZXN0LFxuICAgIC4uLmhhbmRsZXJSZXNwb25zZSxcbiAgICBQaHlzaWNhbFJlc291cmNlSWQ6IHBoeXNpY2FsUmVzb3VyY2VJZCxcbiAgfTtcbn1cblxuYXN5bmMgZnVuY3Rpb24gc3VibWl0UmVzcG9uc2Uoc3RhdHVzOiAnU1VDQ0VTUycgfCAnRkFJTEVEJywgZXZlbnQ6IFJlc3BvbnNlKSB7XG4gIGNvbnN0IGpzb246IEFXU0xhbWJkYS5DbG91ZEZvcm1hdGlvbkN1c3RvbVJlc291cmNlUmVzcG9uc2UgPSB7XG4gICAgU3RhdHVzOiBzdGF0dXMsXG4gICAgUmVhc29uOiBldmVudC5SZWFzb24gPz8gc3RhdHVzLFxuICAgIFN0YWNrSWQ6IGV2ZW50LlN0YWNrSWQsXG4gICAgUmVxdWVzdElkOiBldmVudC5SZXF1ZXN0SWQsXG4gICAgUGh5c2ljYWxSZXNvdXJjZUlkOiBldmVudC5QaHlzaWNhbFJlc291cmNlSWQgfHwgTUlTU0lOR19QSFlTSUNBTF9JRF9NQVJLRVIsXG4gICAgTG9naWNhbFJlc291cmNlSWQ6IGV2ZW50LkxvZ2ljYWxSZXNvdXJjZUlkLFxuICAgIE5vRWNobzogZXZlbnQuTm9FY2hvLFxuICAgIERhdGE6IGV2ZW50LkRhdGEsXG4gIH07XG5cbiAgZXh0ZXJuYWwubG9nKCdzdWJtaXQgcmVzcG9uc2UgdG8gY2xvdWRmb3JtYXRpb24nLCBqc29uKTtcblxuICBjb25zdCByZXNwb25zZUJvZHkgPSBKU09OLnN0cmluZ2lmeShqc29uKTtcbiAgY29uc3QgcGFyc2VkVXJsID0gdXJsLnBhcnNlKGV2ZW50LlJlc3BvbnNlVVJMKTtcbiAgY29uc3QgcmVxID0ge1xuICAgIGhvc3RuYW1lOiBwYXJzZWRVcmwuaG9zdG5hbWUsXG4gICAgcGF0aDogcGFyc2VkVXJsLnBhdGgsXG4gICAgbWV0aG9kOiAnUFVUJyxcbiAgICBoZWFkZXJzOiB7ICdjb250ZW50LXR5cGUnOiAnJywgJ2NvbnRlbnQtbGVuZ3RoJzogcmVzcG9uc2VCb2R5Lmxlbmd0aCB9LFxuICB9O1xuXG4gIGNvbnN0IHJldHJ5T3B0aW9ucyA9IHtcbiAgICBhdHRlbXB0czogNSxcbiAgICBzbGVlcDogMTAwMCxcbiAgfTtcbiAgYXdhaXQgd2l0aFJldHJpZXMocmV0cnlPcHRpb25zLCBleHRlcm5hbC5zZW5kSHR0cFJlcXVlc3QpKHJlcSwgcmVzcG9uc2VCb2R5KTtcbn1cblxuYXN5bmMgZnVuY3Rpb24gZGVmYXVsdFNlbmRIdHRwUmVxdWVzdChvcHRpb25zOiBodHRwcy5SZXF1ZXN0T3B0aW9ucywgcmVzcG9uc2VCb2R5OiBzdHJpbmcpOiBQcm9taXNlPHZvaWQ+IHtcbiAgcmV0dXJuIG5ldyBQcm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHtcbiAgICB0cnkge1xuICAgICAgY29uc3QgcmVxdWVzdCA9IGh0dHBzLnJlcXVlc3Qob3B0aW9ucywgXyA9PiByZXNvbHZlKCkpO1xuICAgICAgcmVxdWVzdC5vbignZXJyb3InLCByZWplY3QpO1xuICAgICAgcmVxdWVzdC53cml0ZShyZXNwb25zZUJvZHkpO1xuICAgICAgcmVxdWVzdC5lbmQoKTtcbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICByZWplY3QoZSk7XG4gICAgfVxuICB9KTtcbn1cblxuZnVuY3Rpb24gZGVmYXVsdExvZyhmbXQ6IHN0cmluZywgLi4ucGFyYW1zOiBhbnlbXSkge1xuICAvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgbm8tY29uc29sZVxuICBjb25zb2xlLmxvZyhmbXQsIC4uLnBhcmFtcyk7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgUmV0cnlPcHRpb25zIHtcbiAgLyoqIEhvdyBtYW55IHJldHJpZXMgKHdpbGwgYXQgbGVhc3QgdHJ5IG9uY2UpICovXG4gIHJlYWRvbmx5IGF0dGVtcHRzOiBudW1iZXI7XG4gIC8qKiBTbGVlcCBiYXNlLCBpbiBtcyAqL1xuICByZWFkb25seSBzbGVlcDogbnVtYmVyO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gd2l0aFJldHJpZXM8QSBleHRlbmRzIEFycmF5PGFueT4sIEI+KG9wdGlvbnM6IFJldHJ5T3B0aW9ucywgZm46ICguLi54czogQSkgPT4gUHJvbWlzZTxCPik6ICguLi54czogQSkgPT4gUHJvbWlzZTxCPiB7XG4gIHJldHVybiBhc3luYyAoLi4ueHM6IEEpID0+IHtcbiAgICBsZXQgYXR0ZW1wdHMgPSBvcHRpb25zLmF0dGVtcHRzO1xuICAgIGxldCBtcyA9IG9wdGlvbnMuc2xlZXA7XG4gICAgd2hpbGUgKHRydWUpIHtcbiAgICAgIHRyeSB7XG4gICAgICAgIHJldHVybiBhd2FpdCBmbiguLi54cyk7XG4gICAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgIGlmIChhdHRlbXB0cy0tIDw9IDApIHtcbiAgICAgICAgICB0aHJvdyBlO1xuICAgICAgICB9XG4gICAgICAgIGF3YWl0IHNsZWVwKE1hdGguZmxvb3IoTWF0aC5yYW5kb20oKSAqIG1zKSk7XG4gICAgICAgIG1zICo9IDI7XG4gICAgICB9XG4gICAgfVxuICB9O1xufVxuXG5hc3luYyBmdW5jdGlvbiBzbGVlcChtczogbnVtYmVyKTogUHJvbWlzZTx2b2lkPiB7XG4gIHJldHVybiBuZXcgUHJvbWlzZSgob2spID0+IHNldFRpbWVvdXQob2ssIG1zKSk7XG59Il19 \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/asset.33e2651435a0d472a75c1e033c9832b21321d9e56711926b04c5705e5f63874c/index.js b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/asset.33e2651435a0d472a75c1e033c9832b21321d9e56711926b04c5705e5f63874c/index.js new file mode 100644 index 0000000000000..7ce4156d4ba41 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/asset.33e2651435a0d472a75c1e033c9832b21321d9e56711926b04c5705e5f63874c/index.js @@ -0,0 +1,78 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.handler = void 0; +// eslint-disable-next-line import/no-extraneous-dependencies +const aws_sdk_1 = require("aws-sdk"); +const AUTO_DELETE_OBJECTS_TAG = 'aws-cdk:auto-delete-objects'; +const s3 = new aws_sdk_1.S3(); +async function handler(event) { + switch (event.RequestType) { + case 'Create': + return; + case 'Update': + return onUpdate(event); + case 'Delete': + return onDelete(event.ResourceProperties?.BucketName); + } +} +exports.handler = handler; +async function onUpdate(event) { + const updateEvent = event; + const oldBucketName = updateEvent.OldResourceProperties?.BucketName; + const newBucketName = updateEvent.ResourceProperties?.BucketName; + const bucketNameHasChanged = newBucketName != null && oldBucketName != null && newBucketName !== oldBucketName; + /* If the name of the bucket has changed, CloudFormation will try to delete the bucket + and create a new one with the new name. So we have to delete the contents of the + bucket so that this operation does not fail. */ + if (bucketNameHasChanged) { + return onDelete(oldBucketName); + } +} +/** + * Recursively delete all items in the bucket + * + * @param bucketName the bucket name + */ +async function emptyBucket(bucketName) { + const listedObjects = await s3.listObjectVersions({ Bucket: bucketName }).promise(); + const contents = [...listedObjects.Versions ?? [], ...listedObjects.DeleteMarkers ?? []]; + if (contents.length === 0) { + return; + } + const records = contents.map((record) => ({ Key: record.Key, VersionId: record.VersionId })); + await s3.deleteObjects({ Bucket: bucketName, Delete: { Objects: records } }).promise(); + if (listedObjects?.IsTruncated) { + await emptyBucket(bucketName); + } +} +async function onDelete(bucketName) { + if (!bucketName) { + throw new Error('No BucketName was provided.'); + } + if (!await isBucketTaggedForDeletion(bucketName)) { + process.stdout.write(`Bucket does not have '${AUTO_DELETE_OBJECTS_TAG}' tag, skipping cleaning.\n`); + return; + } + try { + await emptyBucket(bucketName); + } + catch (e) { + if (e.code !== 'NoSuchBucket') { + throw e; + } + // Bucket doesn't exist. Ignoring + } +} +/** + * The bucket will only be tagged for deletion if it's being deleted in the same + * deployment as this Custom Resource. + * + * If the Custom Resource is every deleted before the bucket, it must be because + * `autoDeleteObjects` has been switched to false, in which case the tag would have + * been removed before we get to this Delete event. + */ +async function isBucketTaggedForDeletion(bucketName) { + const response = await s3.getBucketTagging({ Bucket: bucketName }).promise(); + return response.TagSet.some(tag => tag.Key === AUTO_DELETE_OBJECTS_TAG && tag.Value === 'true'); +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSw2REFBNkQ7QUFDN0QscUNBQTZCO0FBRTdCLE1BQU0sdUJBQXVCLEdBQUcsNkJBQTZCLENBQUM7QUFFOUQsTUFBTSxFQUFFLEdBQUcsSUFBSSxZQUFFLEVBQUUsQ0FBQztBQUViLEtBQUssVUFBVSxPQUFPLENBQUMsS0FBa0Q7SUFDOUUsUUFBUSxLQUFLLENBQUMsV0FBVyxFQUFFO1FBQ3pCLEtBQUssUUFBUTtZQUNYLE9BQU87UUFDVCxLQUFLLFFBQVE7WUFDWCxPQUFPLFFBQVEsQ0FBQyxLQUFLLENBQUMsQ0FBQztRQUN6QixLQUFLLFFBQVE7WUFDWCxPQUFPLFFBQVEsQ0FBQyxLQUFLLENBQUMsa0JBQWtCLEVBQUUsVUFBVSxDQUFDLENBQUM7S0FDekQ7QUFDSCxDQUFDO0FBVEQsMEJBU0M7QUFFRCxLQUFLLFVBQVUsUUFBUSxDQUFDLEtBQWtEO0lBQ3hFLE1BQU0sV0FBVyxHQUFHLEtBQTBELENBQUM7SUFDL0UsTUFBTSxhQUFhLEdBQUcsV0FBVyxDQUFDLHFCQUFxQixFQUFFLFVBQVUsQ0FBQztJQUNwRSxNQUFNLGFBQWEsR0FBRyxXQUFXLENBQUMsa0JBQWtCLEVBQUUsVUFBVSxDQUFDO0lBQ2pFLE1BQU0sb0JBQW9CLEdBQUcsYUFBYSxJQUFJLElBQUksSUFBSSxhQUFhLElBQUksSUFBSSxJQUFJLGFBQWEsS0FBSyxhQUFhLENBQUM7SUFFL0c7O3NEQUVrRDtJQUNsRCxJQUFJLG9CQUFvQixFQUFFO1FBQ3hCLE9BQU8sUUFBUSxDQUFDLGFBQWEsQ0FBQyxDQUFDO0tBQ2hDO0FBQ0gsQ0FBQztBQUVEOzs7O0dBSUc7QUFDSCxLQUFLLFVBQVUsV0FBVyxDQUFDLFVBQWtCO0lBQzNDLE1BQU0sYUFBYSxHQUFHLE1BQU0sRUFBRSxDQUFDLGtCQUFrQixDQUFDLEVBQUUsTUFBTSxFQUFFLFVBQVUsRUFBRSxDQUFDLENBQUMsT0FBTyxFQUFFLENBQUM7SUFDcEYsTUFBTSxRQUFRLEdBQUcsQ0FBQyxHQUFHLGFBQWEsQ0FBQyxRQUFRLElBQUksRUFBRSxFQUFFLEdBQUcsYUFBYSxDQUFDLGFBQWEsSUFBSSxFQUFFLENBQUMsQ0FBQztJQUN6RixJQUFJLFFBQVEsQ0FBQyxNQUFNLEtBQUssQ0FBQyxFQUFFO1FBQ3pCLE9BQU87S0FDUjtJQUVELE1BQU0sT0FBTyxHQUFHLFFBQVEsQ0FBQyxHQUFHLENBQUMsQ0FBQyxNQUFXLEVBQUUsRUFBRSxDQUFDLENBQUMsRUFBRSxHQUFHLEVBQUUsTUFBTSxDQUFDLEdBQUcsRUFBRSxTQUFTLEVBQUUsTUFBTSxDQUFDLFNBQVMsRUFBRSxDQUFDLENBQUMsQ0FBQztJQUNsRyxNQUFNLEVBQUUsQ0FBQyxhQUFhLENBQUMsRUFBRSxNQUFNLEVBQUUsVUFBVSxFQUFFLE1BQU0sRUFBRSxFQUFFLE9BQU8sRUFBRSxPQUFPLEVBQUUsRUFBRSxDQUFDLENBQUMsT0FBTyxFQUFFLENBQUM7SUFFdkYsSUFBSSxhQUFhLEVBQUUsV0FBVyxFQUFFO1FBQzlCLE1BQU0sV0FBVyxDQUFDLFVBQVUsQ0FBQyxDQUFDO0tBQy9CO0FBQ0gsQ0FBQztBQUVELEtBQUssVUFBVSxRQUFRLENBQUMsVUFBbUI7SUFDekMsSUFBSSxDQUFDLFVBQVUsRUFBRTtRQUNmLE1BQU0sSUFBSSxLQUFLLENBQUMsNkJBQTZCLENBQUMsQ0FBQztLQUNoRDtJQUNELElBQUksQ0FBQyxNQUFNLHlCQUF5QixDQUFDLFVBQVUsQ0FBQyxFQUFFO1FBQ2hELE9BQU8sQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLHlCQUF5Qix1QkFBdUIsNkJBQTZCLENBQUMsQ0FBQztRQUNwRyxPQUFPO0tBQ1I7SUFDRCxJQUFJO1FBQ0YsTUFBTSxXQUFXLENBQUMsVUFBVSxDQUFDLENBQUM7S0FDL0I7SUFBQyxPQUFPLENBQUMsRUFBRTtRQUNWLElBQUksQ0FBQyxDQUFDLElBQUksS0FBSyxjQUFjLEVBQUU7WUFDN0IsTUFBTSxDQUFDLENBQUM7U0FDVDtRQUNELGlDQUFpQztLQUNsQztBQUNILENBQUM7QUFFRDs7Ozs7OztHQU9HO0FBQ0gsS0FBSyxVQUFVLHlCQUF5QixDQUFDLFVBQWtCO0lBQ3pELE1BQU0sUUFBUSxHQUFHLE1BQU0sRUFBRSxDQUFDLGdCQUFnQixDQUFDLEVBQUUsTUFBTSxFQUFFLFVBQVUsRUFBRSxDQUFDLENBQUMsT0FBTyxFQUFFLENBQUM7SUFDN0UsT0FBTyxRQUFRLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDLEdBQUcsQ0FBQyxHQUFHLEtBQUssdUJBQXVCLElBQUksR0FBRyxDQUFDLEtBQUssS0FBSyxNQUFNLENBQUMsQ0FBQztBQUNsRyxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIGltcG9ydC9uby1leHRyYW5lb3VzLWRlcGVuZGVuY2llc1xuaW1wb3J0IHsgUzMgfSBmcm9tICdhd3Mtc2RrJztcblxuY29uc3QgQVVUT19ERUxFVEVfT0JKRUNUU19UQUcgPSAnYXdzLWNkazphdXRvLWRlbGV0ZS1vYmplY3RzJztcblxuY29uc3QgczMgPSBuZXcgUzMoKTtcblxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGhhbmRsZXIoZXZlbnQ6IEFXU0xhbWJkYS5DbG91ZEZvcm1hdGlvbkN1c3RvbVJlc291cmNlRXZlbnQpIHtcbiAgc3dpdGNoIChldmVudC5SZXF1ZXN0VHlwZSkge1xuICAgIGNhc2UgJ0NyZWF0ZSc6XG4gICAgICByZXR1cm47XG4gICAgY2FzZSAnVXBkYXRlJzpcbiAgICAgIHJldHVybiBvblVwZGF0ZShldmVudCk7XG4gICAgY2FzZSAnRGVsZXRlJzpcbiAgICAgIHJldHVybiBvbkRlbGV0ZShldmVudC5SZXNvdXJjZVByb3BlcnRpZXM/LkJ1Y2tldE5hbWUpO1xuICB9XG59XG5cbmFzeW5jIGZ1bmN0aW9uIG9uVXBkYXRlKGV2ZW50OiBBV1NMYW1iZGEuQ2xvdWRGb3JtYXRpb25DdXN0b21SZXNvdXJjZUV2ZW50KSB7XG4gIGNvbnN0IHVwZGF0ZUV2ZW50ID0gZXZlbnQgYXMgQVdTTGFtYmRhLkNsb3VkRm9ybWF0aW9uQ3VzdG9tUmVzb3VyY2VVcGRhdGVFdmVudDtcbiAgY29uc3Qgb2xkQnVja2V0TmFtZSA9IHVwZGF0ZUV2ZW50Lk9sZFJlc291cmNlUHJvcGVydGllcz8uQnVja2V0TmFtZTtcbiAgY29uc3QgbmV3QnVja2V0TmFtZSA9IHVwZGF0ZUV2ZW50LlJlc291cmNlUHJvcGVydGllcz8uQnVja2V0TmFtZTtcbiAgY29uc3QgYnVja2V0TmFtZUhhc0NoYW5nZWQgPSBuZXdCdWNrZXROYW1lICE9IG51bGwgJiYgb2xkQnVja2V0TmFtZSAhPSBudWxsICYmIG5ld0J1Y2tldE5hbWUgIT09IG9sZEJ1Y2tldE5hbWU7XG5cbiAgLyogSWYgdGhlIG5hbWUgb2YgdGhlIGJ1Y2tldCBoYXMgY2hhbmdlZCwgQ2xvdWRGb3JtYXRpb24gd2lsbCB0cnkgdG8gZGVsZXRlIHRoZSBidWNrZXRcbiAgICAgYW5kIGNyZWF0ZSBhIG5ldyBvbmUgd2l0aCB0aGUgbmV3IG5hbWUuIFNvIHdlIGhhdmUgdG8gZGVsZXRlIHRoZSBjb250ZW50cyBvZiB0aGVcbiAgICAgYnVja2V0IHNvIHRoYXQgdGhpcyBvcGVyYXRpb24gZG9lcyBub3QgZmFpbC4gKi9cbiAgaWYgKGJ1Y2tldE5hbWVIYXNDaGFuZ2VkKSB7XG4gICAgcmV0dXJuIG9uRGVsZXRlKG9sZEJ1Y2tldE5hbWUpO1xuICB9XG59XG5cbi8qKlxuICogUmVjdXJzaXZlbHkgZGVsZXRlIGFsbCBpdGVtcyBpbiB0aGUgYnVja2V0XG4gKlxuICogQHBhcmFtIGJ1Y2tldE5hbWUgdGhlIGJ1Y2tldCBuYW1lXG4gKi9cbmFzeW5jIGZ1bmN0aW9uIGVtcHR5QnVja2V0KGJ1Y2tldE5hbWU6IHN0cmluZykge1xuICBjb25zdCBsaXN0ZWRPYmplY3RzID0gYXdhaXQgczMubGlzdE9iamVjdFZlcnNpb25zKHsgQnVja2V0OiBidWNrZXROYW1lIH0pLnByb21pc2UoKTtcbiAgY29uc3QgY29udGVudHMgPSBbLi4ubGlzdGVkT2JqZWN0cy5WZXJzaW9ucyA/PyBbXSwgLi4ubGlzdGVkT2JqZWN0cy5EZWxldGVNYXJrZXJzID8/IFtdXTtcbiAgaWYgKGNvbnRlbnRzLmxlbmd0aCA9PT0gMCkge1xuICAgIHJldHVybjtcbiAgfVxuXG4gIGNvbnN0IHJlY29yZHMgPSBjb250ZW50cy5tYXAoKHJlY29yZDogYW55KSA9PiAoeyBLZXk6IHJlY29yZC5LZXksIFZlcnNpb25JZDogcmVjb3JkLlZlcnNpb25JZCB9KSk7XG4gIGF3YWl0IHMzLmRlbGV0ZU9iamVjdHMoeyBCdWNrZXQ6IGJ1Y2tldE5hbWUsIERlbGV0ZTogeyBPYmplY3RzOiByZWNvcmRzIH0gfSkucHJvbWlzZSgpO1xuXG4gIGlmIChsaXN0ZWRPYmplY3RzPy5Jc1RydW5jYXRlZCkge1xuICAgIGF3YWl0IGVtcHR5QnVja2V0KGJ1Y2tldE5hbWUpO1xuICB9XG59XG5cbmFzeW5jIGZ1bmN0aW9uIG9uRGVsZXRlKGJ1Y2tldE5hbWU/OiBzdHJpbmcpIHtcbiAgaWYgKCFidWNrZXROYW1lKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKCdObyBCdWNrZXROYW1lIHdhcyBwcm92aWRlZC4nKTtcbiAgfVxuICBpZiAoIWF3YWl0IGlzQnVja2V0VGFnZ2VkRm9yRGVsZXRpb24oYnVja2V0TmFtZSkpIHtcbiAgICBwcm9jZXNzLnN0ZG91dC53cml0ZShgQnVja2V0IGRvZXMgbm90IGhhdmUgJyR7QVVUT19ERUxFVEVfT0JKRUNUU19UQUd9JyB0YWcsIHNraXBwaW5nIGNsZWFuaW5nLlxcbmApO1xuICAgIHJldHVybjtcbiAgfVxuICB0cnkge1xuICAgIGF3YWl0IGVtcHR5QnVja2V0KGJ1Y2tldE5hbWUpO1xuICB9IGNhdGNoIChlKSB7XG4gICAgaWYgKGUuY29kZSAhPT0gJ05vU3VjaEJ1Y2tldCcpIHtcbiAgICAgIHRocm93IGU7XG4gICAgfVxuICAgIC8vIEJ1Y2tldCBkb2Vzbid0IGV4aXN0LiBJZ25vcmluZ1xuICB9XG59XG5cbi8qKlxuICogVGhlIGJ1Y2tldCB3aWxsIG9ubHkgYmUgdGFnZ2VkIGZvciBkZWxldGlvbiBpZiBpdCdzIGJlaW5nIGRlbGV0ZWQgaW4gdGhlIHNhbWVcbiAqIGRlcGxveW1lbnQgYXMgdGhpcyBDdXN0b20gUmVzb3VyY2UuXG4gKlxuICogSWYgdGhlIEN1c3RvbSBSZXNvdXJjZSBpcyBldmVyeSBkZWxldGVkIGJlZm9yZSB0aGUgYnVja2V0LCBpdCBtdXN0IGJlIGJlY2F1c2VcbiAqIGBhdXRvRGVsZXRlT2JqZWN0c2AgaGFzIGJlZW4gc3dpdGNoZWQgdG8gZmFsc2UsIGluIHdoaWNoIGNhc2UgdGhlIHRhZyB3b3VsZCBoYXZlXG4gKiBiZWVuIHJlbW92ZWQgYmVmb3JlIHdlIGdldCB0byB0aGlzIERlbGV0ZSBldmVudC5cbiAqL1xuYXN5bmMgZnVuY3Rpb24gaXNCdWNrZXRUYWdnZWRGb3JEZWxldGlvbihidWNrZXROYW1lOiBzdHJpbmcpIHtcbiAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBzMy5nZXRCdWNrZXRUYWdnaW5nKHsgQnVja2V0OiBidWNrZXROYW1lIH0pLnByb21pc2UoKTtcbiAgcmV0dXJuIHJlc3BvbnNlLlRhZ1NldC5zb21lKHRhZyA9PiB0YWcuS2V5ID09PSBBVVRPX0RFTEVURV9PQkpFQ1RTX1RBRyAmJiB0YWcuVmFsdWUgPT09ICd0cnVlJyk7XG59Il19 \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/asset.5d8d1d0aacea23824c62f362e1e3c14b7dd14a31c71b53bfae4d14a6373c5510.zip b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/asset.5d8d1d0aacea23824c62f362e1e3c14b7dd14a31c71b53bfae4d14a6373c5510.zip new file mode 100644 index 0000000000000..298edd40a09fc Binary files /dev/null and b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/asset.5d8d1d0aacea23824c62f362e1e3c14b7dd14a31c71b53bfae4d14a6373c5510.zip differ diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/asset.a94977ede0211fd3b45efa33d6d8d1d7bbe0c5a96d977139d8b16abfa96fe9cb/403.html b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/asset.a94977ede0211fd3b45efa33d6d8d1d7bbe0c5a96d977139d8b16abfa96fe9cb/403.html new file mode 100644 index 0000000000000..2529e8a586eaa --- /dev/null +++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/asset.a94977ede0211fd3b45efa33d6d8d1d7bbe0c5a96d977139d8b16abfa96fe9cb/403.html @@ -0,0 +1,2 @@ +

Hello, S3 bucket deployments!

+ diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/cdk.out b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/cdk.out new file mode 100644 index 0000000000000..d8b441d447f8a --- /dev/null +++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"29.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/integ.json b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/integ.json new file mode 100644 index 0000000000000..d68f240d44905 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "29.0.0", + "testCases": { + "integ-test-bucket-deployments/DefaultTest": { + "stacks": [ + "test-bucket-deployment-deployed-bucket" + ], + "assertionStack": "integ-test-bucket-deployments/DefaultTest/DeployAssert", + "assertionStackName": "integtestbucketdeploymentsDefaultTestDeployAssertCF25A2DF" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/integtestbucketdeploymentsDefaultTestDeployAssertCF25A2DF.assets.json b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/integtestbucketdeploymentsDefaultTestDeployAssertCF25A2DF.assets.json new file mode 100644 index 0000000000000..4930f45442dc5 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/integtestbucketdeploymentsDefaultTestDeployAssertCF25A2DF.assets.json @@ -0,0 +1,19 @@ +{ + "version": "29.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "integtestbucketdeploymentsDefaultTestDeployAssertCF25A2DF.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/integtestbucketdeploymentsDefaultTestDeployAssertCF25A2DF.template.json b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/integtestbucketdeploymentsDefaultTestDeployAssertCF25A2DF.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/integtestbucketdeploymentsDefaultTestDeployAssertCF25A2DF.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/manifest.json b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/manifest.json new file mode 100644 index 0000000000000..652f322970b5a --- /dev/null +++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/manifest.json @@ -0,0 +1,177 @@ +{ + "version": "29.0.0", + "artifacts": { + "test-bucket-deployment-deployed-bucket.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "test-bucket-deployment-deployed-bucket.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "test-bucket-deployment-deployed-bucket": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "test-bucket-deployment-deployed-bucket.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/18f4898813d68713dcc2a2e5f54aa134dea630a42e227be3c03b38c333a4ba03.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "test-bucket-deployment-deployed-bucket.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "test-bucket-deployment-deployed-bucket.assets" + ], + "metadata": { + "/test-bucket-deployment-deployed-bucket/Destination/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Destination920A3C57" + } + ], + "/test-bucket-deployment-deployed-bucket/Destination/Policy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "DestinationPolicy7982387E" + } + ], + "/test-bucket-deployment-deployed-bucket/Destination/AutoDeleteObjectsCustomResource/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "DestinationAutoDeleteObjectsCustomResource15E926BA" + } + ], + "/test-bucket-deployment-deployed-bucket/Custom::S3AutoDeleteObjectsCustomResourceProvider/Role": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092" + } + ], + "/test-bucket-deployment-deployed-bucket/Custom::S3AutoDeleteObjectsCustomResourceProvider/Handler": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F" + } + ], + "/test-bucket-deployment-deployed-bucket/DeployMe5/AwsCliLayer/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "DeployMe5AwsCliLayerF0F79631" + } + ], + "/test-bucket-deployment-deployed-bucket/DeployMe5/CustomResource/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "DeployMe5CustomResource44BEE629" + } + ], + "/test-bucket-deployment-deployed-bucket/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265" + } + ], + "/test-bucket-deployment-deployed-bucket/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF" + } + ], + "/test-bucket-deployment-deployed-bucket/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536" + } + ], + "/test-bucket-deployment-deployed-bucket/ExportWebsiteUrl": [ + { + "type": "aws:cdk:logicalId", + "data": "ExportWebsiteUrl" + } + ], + "/test-bucket-deployment-deployed-bucket/S3-static-websiteMap": [ + { + "type": "aws:cdk:logicalId", + "data": "S3staticwebsiteMap" + } + ], + "/test-bucket-deployment-deployed-bucket/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/test-bucket-deployment-deployed-bucket/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "test-bucket-deployment-deployed-bucket" + }, + "integtestbucketdeploymentsDefaultTestDeployAssertCF25A2DF.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "integtestbucketdeploymentsDefaultTestDeployAssertCF25A2DF.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "integtestbucketdeploymentsDefaultTestDeployAssertCF25A2DF": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "integtestbucketdeploymentsDefaultTestDeployAssertCF25A2DF.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "integtestbucketdeploymentsDefaultTestDeployAssertCF25A2DF.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "integtestbucketdeploymentsDefaultTestDeployAssertCF25A2DF.assets" + ], + "metadata": { + "/integ-test-bucket-deployments/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/integ-test-bucket-deployments/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "integ-test-bucket-deployments/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/test-bucket-deployment-deployed-bucket.assets.json b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/test-bucket-deployment-deployed-bucket.assets.json new file mode 100644 index 0000000000000..622d261d9e8c6 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/test-bucket-deployment-deployed-bucket.assets.json @@ -0,0 +1,71 @@ +{ + "version": "29.0.0", + "files": { + "33e2651435a0d472a75c1e033c9832b21321d9e56711926b04c5705e5f63874c": { + "source": { + "path": "asset.33e2651435a0d472a75c1e033c9832b21321d9e56711926b04c5705e5f63874c", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "33e2651435a0d472a75c1e033c9832b21321d9e56711926b04c5705e5f63874c.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "5d8d1d0aacea23824c62f362e1e3c14b7dd14a31c71b53bfae4d14a6373c5510": { + "source": { + "path": "asset.5d8d1d0aacea23824c62f362e1e3c14b7dd14a31c71b53bfae4d14a6373c5510.zip", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "5d8d1d0aacea23824c62f362e1e3c14b7dd14a31c71b53bfae4d14a6373c5510.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "2bc265c5e0569aeb24a6349c15bd54e76e845892376515e036627ab0cc70bb64": { + "source": { + "path": "asset.2bc265c5e0569aeb24a6349c15bd54e76e845892376515e036627ab0cc70bb64", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "2bc265c5e0569aeb24a6349c15bd54e76e845892376515e036627ab0cc70bb64.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "a94977ede0211fd3b45efa33d6d8d1d7bbe0c5a96d977139d8b16abfa96fe9cb": { + "source": { + "path": "asset.a94977ede0211fd3b45efa33d6d8d1d7bbe0c5a96d977139d8b16abfa96fe9cb", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "a94977ede0211fd3b45efa33d6d8d1d7bbe0c5a96d977139d8b16abfa96fe9cb.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "18f4898813d68713dcc2a2e5f54aa134dea630a42e227be3c03b38c333a4ba03": { + "source": { + "path": "test-bucket-deployment-deployed-bucket.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "18f4898813d68713dcc2a2e5f54aa134dea630a42e227be3c03b38c333a4ba03.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/test-bucket-deployment-deployed-bucket.template.json b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/test-bucket-deployment-deployed-bucket.template.json new file mode 100644 index 0000000000000..769de3e253333 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/test-bucket-deployment-deployed-bucket.template.json @@ -0,0 +1,532 @@ +{ + "Resources": { + "Destination920A3C57": { + "Type": "AWS::S3::Bucket", + "Properties": { + "Tags": [ + { + "Key": "aws-cdk:auto-delete-objects", + "Value": "true" + }, + { + "Key": "aws-cdk:cr-owned:4b49afe7", + "Value": "true" + } + ] + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "DestinationPolicy7982387E": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "Destination920A3C57" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:DeleteObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "Destination920A3C57", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Destination920A3C57", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "DestinationAutoDeleteObjectsCustomResource15E926BA": { + "Type": "Custom::S3AutoDeleteObjects", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F", + "Arn" + ] + }, + "BucketName": { + "Ref": "Destination920A3C57" + } + }, + "DependsOn": [ + "DestinationPolicy7982387E" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ] + } + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "33e2651435a0d472a75c1e033c9832b21321d9e56711926b04c5705e5f63874c.zip" + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "__entrypoint__.handler", + "Role": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + }, + "Runtime": "nodejs14.x", + "Description": { + "Fn::Join": [ + "", + [ + "Lambda function for auto-deleting objects in ", + { + "Ref": "Destination920A3C57" + }, + " S3 bucket." + ] + ] + } + }, + "DependsOn": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092" + ] + }, + "DeployMe5AwsCliLayerF0F79631": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "Content": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "5d8d1d0aacea23824c62f362e1e3c14b7dd14a31c71b53bfae4d14a6373c5510.zip" + }, + "Description": "/opt/awscli/aws" + } + }, + "DeployMe5CustomResource44BEE629": { + "Type": "Custom::CDKBucketDeployment", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536", + "Arn" + ] + }, + "SourceBucketNames": [ + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + } + ], + "SourceObjectKeys": [ + "a94977ede0211fd3b45efa33d6d8d1d7bbe0c5a96d977139d8b16abfa96fe9cb.zip" + ], + "DestinationBucketName": { + "Ref": "Destination920A3C57" + }, + "RetainOnDelete": false, + "Prune": true, + "DestinationBucketArn": { + "Fn::GetAtt": [ + "Destination920A3C57", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "/*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + } + ] + ] + } + ] + }, + { + "Action": [ + "s3:Abort*", + "s3:DeleteObject*", + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "Destination920A3C57", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Destination920A3C57", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", + "Roles": [ + { + "Ref": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265" + } + ] + } + }, + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "2bc265c5e0569aeb24a6349c15bd54e76e845892376515e036627ab0cc70bb64.zip" + }, + "Role": { + "Fn::GetAtt": [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265", + "Arn" + ] + }, + "Handler": "index.handler", + "Layers": [ + { + "Ref": "DeployMe5AwsCliLayerF0F79631" + } + ], + "Runtime": "python3.9", + "Timeout": 900 + }, + "DependsOn": [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265" + ] + } + }, + "Outputs": { + "ExportWebsiteUrl": { + "Value": { + "Fn::Join": [ + "", + [ + "http://", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "DeployMe5CustomResource44BEE629", + "DestinationBucketArn" + ] + } + ] + } + ] + } + ] + } + ] + }, + ".", + { + "Fn::FindInMap": [ + "S3staticwebsiteMap", + { + "Ref": "AWS::Region" + }, + "endpoint" + ] + } + ] + ] + }, + "Export": { + "Name": "WebsiteUrl" + } + } + }, + "Mappings": { + "S3staticwebsiteMap": { + "af-south-1": { + "endpoint": "s3-website.af-south-1.amazonaws.com" + }, + "ap-east-1": { + "endpoint": "s3-website.ap-east-1.amazonaws.com" + }, + "ap-northeast-1": { + "endpoint": "s3-website-ap-northeast-1.amazonaws.com" + }, + "ap-northeast-2": { + "endpoint": "s3-website.ap-northeast-2.amazonaws.com" + }, + "ap-northeast-3": { + "endpoint": "s3-website.ap-northeast-3.amazonaws.com" + }, + "ap-south-1": { + "endpoint": "s3-website.ap-south-1.amazonaws.com" + }, + "ap-south-2": { + "endpoint": "s3-website.ap-south-2.amazonaws.com" + }, + "ap-southeast-1": { + "endpoint": "s3-website-ap-southeast-1.amazonaws.com" + }, + "ap-southeast-2": { + "endpoint": "s3-website-ap-southeast-2.amazonaws.com" + }, + "ap-southeast-3": { + "endpoint": "s3-website.ap-southeast-3.amazonaws.com" + }, + "ca-central-1": { + "endpoint": "s3-website.ca-central-1.amazonaws.com" + }, + "cn-north-1": { + "endpoint": "s3-website.cn-north-1.amazonaws.com.cn" + }, + "cn-northwest-1": { + "endpoint": "s3-website.cn-northwest-1.amazonaws.com.cn" + }, + "eu-central-1": { + "endpoint": "s3-website.eu-central-1.amazonaws.com" + }, + "eu-north-1": { + "endpoint": "s3-website.eu-north-1.amazonaws.com" + }, + "eu-south-1": { + "endpoint": "s3-website.eu-south-1.amazonaws.com" + }, + "eu-south-2": { + "endpoint": "s3-website.eu-south-2.amazonaws.com" + }, + "eu-west-1": { + "endpoint": "s3-website-eu-west-1.amazonaws.com" + }, + "eu-west-2": { + "endpoint": "s3-website.eu-west-2.amazonaws.com" + }, + "eu-west-3": { + "endpoint": "s3-website.eu-west-3.amazonaws.com" + }, + "me-central-1": { + "endpoint": "s3-website.me-central-1.amazonaws.com" + }, + "me-south-1": { + "endpoint": "s3-website.me-south-1.amazonaws.com" + }, + "sa-east-1": { + "endpoint": "s3-website-sa-east-1.amazonaws.com" + }, + "us-east-1": { + "endpoint": "s3-website-us-east-1.amazonaws.com" + }, + "us-east-2": { + "endpoint": "s3-website.us-east-2.amazonaws.com" + }, + "us-gov-east-1": { + "endpoint": "s3-website.us-gov-east-1.amazonaws.com" + }, + "us-gov-west-1": { + "endpoint": "s3-website-us-gov-west-1.amazonaws.com" + }, + "us-iso-east-1": { + "endpoint": "s3-website.us-iso-east-1.c2s.ic.gov" + }, + "us-iso-west-1": { + "endpoint": "s3-website.us-iso-west-1.c2s.ic.gov" + }, + "us-isob-east-1": { + "endpoint": "s3-website.us-isob-east-1.sc2s.sgov.gov" + }, + "us-west-1": { + "endpoint": "s3-website-us-west-1.amazonaws.com" + }, + "us-west-2": { + "endpoint": "s3-website-us-west-2.amazonaws.com" + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/tree.json b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/tree.json new file mode 100644 index 0000000000000..97e123707189c --- /dev/null +++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.js.snapshot/tree.json @@ -0,0 +1,635 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "test-bucket-deployment-deployed-bucket": { + "id": "test-bucket-deployment-deployed-bucket", + "path": "test-bucket-deployment-deployed-bucket", + "children": { + "Destination": { + "id": "Destination", + "path": "test-bucket-deployment-deployed-bucket/Destination", + "children": { + "Resource": { + "id": "Resource", + "path": "test-bucket-deployment-deployed-bucket/Destination/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::Bucket", + "aws:cdk:cloudformation:props": { + "tags": [ + { + "key": "aws-cdk:auto-delete-objects", + "value": "true" + }, + { + "key": "aws-cdk:cr-owned:4b49afe7", + "value": "true" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.CfnBucket", + "version": "0.0.0" + } + }, + "Policy": { + "id": "Policy", + "path": "test-bucket-deployment-deployed-bucket/Destination/Policy", + "children": { + "Resource": { + "id": "Resource", + "path": "test-bucket-deployment-deployed-bucket/Destination/Policy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::BucketPolicy", + "aws:cdk:cloudformation:props": { + "bucket": { + "Ref": "Destination920A3C57" + }, + "policyDocument": { + "Statement": [ + { + "Action": [ + "s3:DeleteObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "Destination920A3C57", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Destination920A3C57", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.CfnBucketPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.BucketPolicy", + "version": "0.0.0" + } + }, + "AutoDeleteObjectsCustomResource": { + "id": "AutoDeleteObjectsCustomResource", + "path": "test-bucket-deployment-deployed-bucket/Destination/AutoDeleteObjectsCustomResource", + "children": { + "Default": { + "id": "Default", + "path": "test-bucket-deployment-deployed-bucket/Destination/AutoDeleteObjectsCustomResource/Default", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CustomResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.Bucket", + "version": "0.0.0" + } + }, + "Custom::S3AutoDeleteObjectsCustomResourceProvider": { + "id": "Custom::S3AutoDeleteObjectsCustomResourceProvider", + "path": "test-bucket-deployment-deployed-bucket/Custom::S3AutoDeleteObjectsCustomResourceProvider", + "children": { + "Staging": { + "id": "Staging", + "path": "test-bucket-deployment-deployed-bucket/Custom::S3AutoDeleteObjectsCustomResourceProvider/Staging", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "Role": { + "id": "Role", + "path": "test-bucket-deployment-deployed-bucket/Custom::S3AutoDeleteObjectsCustomResourceProvider/Role", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + }, + "Handler": { + "id": "Handler", + "path": "test-bucket-deployment-deployed-bucket/Custom::S3AutoDeleteObjectsCustomResourceProvider/Handler", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CustomResourceProvider", + "version": "0.0.0" + } + }, + "DeployMe5": { + "id": "DeployMe5", + "path": "test-bucket-deployment-deployed-bucket/DeployMe5", + "children": { + "AwsCliLayer": { + "id": "AwsCliLayer", + "path": "test-bucket-deployment-deployed-bucket/DeployMe5/AwsCliLayer", + "children": { + "Code": { + "id": "Code", + "path": "test-bucket-deployment-deployed-bucket/DeployMe5/AwsCliLayer/Code", + "children": { + "Stage": { + "id": "Stage", + "path": "test-bucket-deployment-deployed-bucket/DeployMe5/AwsCliLayer/Code/Stage", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "AssetBucket": { + "id": "AssetBucket", + "path": "test-bucket-deployment-deployed-bucket/DeployMe5/AwsCliLayer/Code/AssetBucket", + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.BucketBase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3-assets.Asset", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "test-bucket-deployment-deployed-bucket/DeployMe5/AwsCliLayer/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Lambda::LayerVersion", + "aws:cdk:cloudformation:props": { + "content": { + "s3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "s3Key": "5d8d1d0aacea23824c62f362e1e3c14b7dd14a31c71b53bfae4d14a6373c5510.zip" + }, + "description": "/opt/awscli/aws" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.CfnLayerVersion", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/lambda-layer-awscli.AwsCliLayer", + "version": "0.0.0" + } + }, + "CustomResourceHandler": { + "id": "CustomResourceHandler", + "path": "test-bucket-deployment-deployed-bucket/DeployMe5/CustomResourceHandler", + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.SingletonFunction", + "version": "0.0.0" + } + }, + "Asset1": { + "id": "Asset1", + "path": "test-bucket-deployment-deployed-bucket/DeployMe5/Asset1", + "children": { + "Stage": { + "id": "Stage", + "path": "test-bucket-deployment-deployed-bucket/DeployMe5/Asset1/Stage", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "AssetBucket": { + "id": "AssetBucket", + "path": "test-bucket-deployment-deployed-bucket/DeployMe5/Asset1/AssetBucket", + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.BucketBase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3-assets.Asset", + "version": "0.0.0" + } + }, + "CustomResource": { + "id": "CustomResource", + "path": "test-bucket-deployment-deployed-bucket/DeployMe5/CustomResource", + "children": { + "Default": { + "id": "Default", + "path": "test-bucket-deployment-deployed-bucket/DeployMe5/CustomResource/Default", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CustomResource", + "version": "0.0.0" + } + }, + "DestinationBucket": { + "id": "DestinationBucket", + "path": "test-bucket-deployment-deployed-bucket/DeployMe5/DestinationBucket", + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.BucketBase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3-deployment.BucketDeployment", + "version": "0.0.0" + } + }, + "Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C": { + "id": "Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C", + "path": "test-bucket-deployment-deployed-bucket/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C", + "children": { + "ServiceRole": { + "id": "ServiceRole", + "path": "test-bucket-deployment-deployed-bucket/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole", + "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "test-bucket-deployment-deployed-bucket/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "test-bucket-deployment-deployed-bucket/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "managedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "test-bucket-deployment-deployed-bucket/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "test-bucket-deployment-deployed-bucket/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "/*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + } + ] + ] + } + ] + }, + { + "Action": [ + "s3:Abort*", + "s3:DeleteObject*", + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "Destination920A3C57", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Destination920A3C57", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "policyName": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", + "roles": [ + { + "Ref": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "Code": { + "id": "Code", + "path": "test-bucket-deployment-deployed-bucket/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Code", + "children": { + "Stage": { + "id": "Stage", + "path": "test-bucket-deployment-deployed-bucket/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Code/Stage", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "AssetBucket": { + "id": "AssetBucket", + "path": "test-bucket-deployment-deployed-bucket/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Code/AssetBucket", + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.BucketBase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3-assets.Asset", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "test-bucket-deployment-deployed-bucket/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Lambda::Function", + "aws:cdk:cloudformation:props": { + "code": { + "s3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "s3Key": "2bc265c5e0569aeb24a6349c15bd54e76e845892376515e036627ab0cc70bb64.zip" + }, + "role": { + "Fn::GetAtt": [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265", + "Arn" + ] + }, + "handler": "index.handler", + "layers": [ + { + "Ref": "DeployMe5AwsCliLayerF0F79631" + } + ], + "runtime": "python3.9", + "timeout": 900 + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.CfnFunction", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.Function", + "version": "0.0.0" + } + }, + "ExportWebsiteUrl": { + "id": "ExportWebsiteUrl", + "path": "test-bucket-deployment-deployed-bucket/ExportWebsiteUrl", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + }, + "S3-static-websiteMap": { + "id": "S3-static-websiteMap", + "path": "test-bucket-deployment-deployed-bucket/S3-static-websiteMap", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnMapping", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "test-bucket-deployment-deployed-bucket/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "test-bucket-deployment-deployed-bucket/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "integ-test-bucket-deployments": { + "id": "integ-test-bucket-deployments", + "path": "integ-test-bucket-deployments", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "integ-test-bucket-deployments/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "integ-test-bucket-deployments/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.237" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "integ-test-bucket-deployments/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "integ-test-bucket-deployments/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "integ-test-bucket-deployments/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.237" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.ts b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.ts new file mode 100644 index 0000000000000..7cb2191339c64 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-deployed-bucket.ts @@ -0,0 +1,39 @@ +import * as path from 'path'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import * as integ from '@aws-cdk/integ-tests'; +import { Construct } from 'constructs'; +import * as s3deploy from '../lib'; + +class TestBucketDeployment extends cdk.Stack { + public readonly bucket: s3.IBucket; + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + this.bucket = new s3.Bucket(this, 'Destination', { + publicReadAccess: false, + removalPolicy: cdk.RemovalPolicy.DESTROY, + autoDeleteObjects: true, // needed for integration test cleanup + }); + + const deploy = new s3deploy.BucketDeployment(this, 'DeployMe5', { + sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website-second'))], + destinationBucket: this.bucket, + retainOnDelete: false, // default is true, which will block the integration test cleanup + }); + + this.exportValue(deploy.deployedBucket.bucketWebsiteUrl, { + name: 'WebsiteUrl', + }); + } +} + +const app = new cdk.App(); +const testCase = new TestBucketDeployment(app, 'test-bucket-deployment-deployed-bucket'); + + +new integ.IntegTest(app, 'integ-test-bucket-deployments', { + testCases: [testCase], +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-s3-deployment/test/lambda/debug.sh b/packages/@aws-cdk/aws-s3-deployment/test/lambda/debug.sh index 459116e55ab84..9345e6c3ab464 100755 --- a/packages/@aws-cdk/aws-s3-deployment/test/lambda/debug.sh +++ b/packages/@aws-cdk/aws-s3-deployment/test/lambda/debug.sh @@ -1,10 +1,12 @@ #!/bin/sh # starts a debugging container for the python lambda function and tests +DOCKER_CMD=${CDK_DOCKER:-docker} + tag="s3-deployment-test-environment" -docker build -f Dockerfile.debug -t $tag . +$DOCKER_CMD build -f Dockerfile.debug -t $tag . echo "To iterate, run python3 ./test.py inside the container (source code is mapped into the container)." ln -fs /opt/lambda/index.py index.py -docker run -v $PWD:/opt/awscli -v $PWD/../../lib/lambda:/opt/lambda --workdir /opt/awscli -it $tag \ No newline at end of file +$DOCKER_CMD run -v $PWD:/opt/awscli -v $PWD/../../lib/lambda:/opt/lambda --workdir /opt/awscli -it $tag diff --git a/packages/@aws-cdk/aws-s3-deployment/test/lambda/test.sh b/packages/@aws-cdk/aws-s3-deployment/test/lambda/test.sh index 8f0c0d2b473b7..dde97dde4d370 100755 --- a/packages/@aws-cdk/aws-s3-deployment/test/lambda/test.sh +++ b/packages/@aws-cdk/aws-s3-deployment/test/lambda/test.sh @@ -19,4 +19,5 @@ cp -f ${scriptdir}/../../lib/lambda/* $PWD cp -f ${scriptdir}/* $PWD # this will run our tests inside the right environment -docker build . +DOCKER_CMD=${CDK_DOCKER:-docker} +$DOCKER_CMD build . diff --git a/packages/@aws-cdk/aws-s3/README.md b/packages/@aws-cdk/aws-s3/README.md index 9a41dc49369d4..c435a96eee6bf 100644 --- a/packages/@aws-cdk/aws-s3/README.md +++ b/packages/@aws-cdk/aws-s3/README.md @@ -616,3 +616,36 @@ const bucket = new s3.Bucket(this, 'MyBucket', { }] }); ``` + +## Object Lock Configuration + +[Object Lock](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock-overview.html) +can be configured to enable a write-once-read-many model for an S3 bucket. Object Lock must be +configured when a bucket is created; if a bucket is created without Object Lock, it cannot be +enabled later via the CDK. + +Object Lock can be enabled on an S3 bucket by specifying: + +```ts +const bucket = new s3.Bucket(this, 'MyBucket', { + objectLockEnabled: true +}); +``` + +Usually, it is desired to not just enable Object Lock for a bucket but to also configure a +[retention mode](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock-overview.html#object-lock-retention-modes) +and a [retention period](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock-overview.html#object-lock-retention-periods). +These can be specified by providing `objectLockDefaultRetention`: + +```ts +// Configure for governance mode with a duration of 7 years +new s3.Bucket(this, 'Bucket1', { + objectLockDefaultRetention: s3.ObjectLockRetention.governance(cdk.Duration.days(7 * 365)), +}); + +// Configure for compliance mode with a duration of 1 year +new s3.Bucket(this, 'Bucket2', { + objectLockDefaultRetention: s3.ObjectLockRetention.compliance(cdk.Duration.days(365)), +}); + +``` diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index 13e7aea23fbd0..2b2f930dd0f49 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -23,6 +23,7 @@ import { } from '@aws-cdk/core'; import { CfnReference } from '@aws-cdk/core/lib/private/cfn-reference'; import * as cxapi from '@aws-cdk/cx-api'; +import * as regionInformation from '@aws-cdk/region-info'; import { Construct } from 'constructs'; import { BucketPolicy } from './bucket-policy'; import { IBucketNotificationDestination } from './destination'; @@ -408,14 +409,14 @@ export interface BucketAttributes { /** * The domain name of the bucket. * - * @default Inferred from bucket name + * @default - Inferred from bucket name */ readonly bucketDomainName?: string; /** * The website URL of the bucket (if static web hosting is enabled). * - * @default Inferred from bucket name + * @default - Inferred from bucket name and region */ readonly bucketWebsiteUrl?: string; @@ -430,13 +431,22 @@ export interface BucketAttributes { readonly bucketDualStackDomainName?: string; /** - * The format of the website URL of the bucket. This should be true for + * Force the format of the website URL of the bucket. This should be true for * regions launched since 2014. * - * @default false + * @default - inferred from available region information, `false` otherwise + * + * @deprecated The correct website url format can be inferred automatically from the bucket `region`. + * Always provide the bucket region if the `bucketWebsiteUrl` will be used. + * Alternatively provide the full `bucketWebsiteUrl` manually. */ readonly bucketWebsiteNewUrlFormat?: boolean; + /** + * KMS encryption key associated with this bucket. + * + * @default - no encryption key + */ readonly encryptionKey?: kms.IKey; /** @@ -455,6 +465,8 @@ export interface BucketAttributes { /** * The region this existing bucket is in. + * Features that require the region (e.g. `bucketWebsiteUrl`) won't fully work + * if the region cannot be correctly inferred. * * @default - it's assumed the bucket is in the same region as the scope it's being imported into */ @@ -1401,10 +1413,34 @@ export interface BucketProps { /** * Whether this bucket should have versioning turned on or not. * - * @default false + * @default false (unless object lock is enabled, then true) */ readonly versioned?: boolean; + /** + * Enable object lock on the bucket. + * + * Enabling object lock for existing buckets is not supported. Object lock must be + * enabled when the bucket is created. + * + * @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock-overview.html#object-lock-bucket-config-enable + * + * @default false, unless objectLockDefaultRetention is set (then, true) + */ + readonly objectLockEnabled?: boolean; + + /** + * The default retention mode and rules for S3 Object Lock. + * + * Default retention can be configured after a bucket is created if the bucket already + * has object lock enabled. Enabling object lock for existing buckets is not supported. + * + * @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock-overview.html#object-lock-bucket-config-enable + * + * @default no default retention period + */ + readonly objectLockDefaultRetention?: ObjectLockRetention; + /** * Whether this bucket should send notifications to Amazon EventBridge or not. * @@ -1602,7 +1638,8 @@ export class Bucket extends BucketBase { public static fromBucketAttributes(scope: Construct, id: string, attrs: BucketAttributes): IBucket { const stack = Stack.of(scope); const region = attrs.region ?? stack.region; - const urlSuffix = stack.urlSuffix; + const regionInfo = regionInformation.RegionInfo.get(region); + const urlSuffix = regionInfo.domainSuffix ?? stack.urlSuffix; const bucketName = parseBucketName(scope, attrs); if (!bucketName) { @@ -1610,13 +1647,18 @@ export class Bucket extends BucketBase { } Bucket.validateBucketName(bucketName); - const newUrlFormat = attrs.bucketWebsiteNewUrlFormat === undefined - ? false - : attrs.bucketWebsiteNewUrlFormat; + const oldEndpoint = `s3-website-${region}.${urlSuffix}`; + const newEndpoint = `s3-website.${region}.${urlSuffix}`; - const websiteDomain = newUrlFormat - ? `${bucketName}.s3-website.${region}.${urlSuffix}` - : `${bucketName}.s3-website-${region}.${urlSuffix}`; + let staticDomainEndpoint = regionInfo.s3StaticWebsiteEndpoint + ?? Lazy.string({ produce: () => stack.regionalFact(regionInformation.FactName.S3_STATIC_WEBSITE_ENDPOINT, newEndpoint) }); + + // Deprecated use of bucketWebsiteNewUrlFormat + if (attrs.bucketWebsiteNewUrlFormat !== undefined) { + staticDomainEndpoint = attrs.bucketWebsiteNewUrlFormat ? newEndpoint : oldEndpoint; + } + + const websiteDomain = `${bucketName}.${staticDomainEndpoint}`; class Import extends BucketBase { public readonly bucketName = bucketName!; @@ -1626,7 +1668,7 @@ export class Bucket extends BucketBase { public readonly bucketWebsiteDomainName = attrs.bucketWebsiteUrl ? Fn.select(2, Fn.split('/', attrs.bucketWebsiteUrl)) : websiteDomain; public readonly bucketRegionalDomainName = attrs.bucketRegionalDomainName || `${bucketName}.s3.${region}.${urlSuffix}`; public readonly bucketDualStackDomainName = attrs.bucketDualStackDomainName || `${bucketName}.s3.dualstack.${region}.${urlSuffix}`; - public readonly bucketWebsiteNewUrlFormat = newUrlFormat; + public readonly bucketWebsiteNewUrlFormat = attrs.bucketWebsiteNewUrlFormat ?? false; public readonly encryptionKey = attrs.encryptionKey; public readonly isWebsite = attrs.isWebsite ?? false; public policy?: BucketPolicy = undefined; @@ -1787,6 +1829,8 @@ export class Bucket extends BucketBase { const websiteConfiguration = this.renderWebsiteConfiguration(props); this.isWebsite = (websiteConfiguration !== undefined); + const objectLockConfiguration = this.parseObjectLockConfig(props); + const resource = new CfnBucket(this, 'Resource', { bucketName: this.physicalName, bucketEncryption, @@ -1802,6 +1846,8 @@ export class Bucket extends BucketBase { ownershipControls: this.parseOwnershipControls(props), accelerateConfiguration: props.transferAcceleration ? { accelerationStatus: 'Enabled' } : undefined, intelligentTieringConfigurations: this.parseTieringConfig(props), + objectLockEnabled: objectLockConfiguration ? true : props.objectLockEnabled, + objectLockConfiguration: objectLockConfiguration, }); this._resource = resource; @@ -2164,6 +2210,27 @@ export class Bucket extends BucketBase { }); } + private parseObjectLockConfig(props: BucketProps): CfnBucket.ObjectLockConfigurationProperty | undefined { + const { objectLockEnabled, objectLockDefaultRetention } = props; + + if (!objectLockDefaultRetention) { + return undefined; + } + if (objectLockEnabled === false && objectLockDefaultRetention) { + throw new Error('Object Lock must be enabled to configure default retention settings'); + } + + return { + objectLockEnabled: 'Enabled', + rule: { + defaultRetention: { + days: objectLockDefaultRetention.duration.toDays(), + mode: objectLockDefaultRetention.mode, + }, + }, + }; + } + private renderWebsiteConfiguration(props: BucketProps): CfnBucket.WebsiteConfigurationProperty | undefined { if (!props.websiteErrorDocument && !props.websiteIndexDocument && !props.websiteRedirect && !props.websiteRoutingRules) { return undefined; @@ -2231,7 +2298,7 @@ export class Bucket extends BucketBase { effect: iam.Effect.ALLOW, principals: [new iam.ServicePrincipal('logging.s3.amazonaws.com')], actions: ['s3:PutObject'], - resources: [this.arnForObjects(prefix ? `${prefix}*`: '*')], + resources: [this.arnForObjects(prefix ? `${prefix}*` : '*')], conditions: conditions, })); } else if (this.accessControl && this.accessControl !== BucketAccessControl.LOG_DELIVERY_WRITE) { @@ -2742,6 +2809,93 @@ export interface RoutingRule { readonly condition?: RoutingRuleCondition; } +/** + * Modes in which S3 Object Lock retention can be configured. + * + * @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock-overview.html#object-lock-retention-modes + */ +export enum ObjectLockMode { + /** + * The Governance retention mode. + * + * With governance mode, you protect objects against being deleted by most users, but you can + * still grant some users permission to alter the retention settings or delete the object if + * necessary. You can also use governance mode to test retention-period settings before + * creating a compliance-mode retention period. + */ + GOVERNANCE = 'GOVERNANCE', + + /** + * The Compliance retention mode. + * + * When an object is locked in compliance mode, its retention mode can't be changed, and + * its retention period can't be shortened. Compliance mode helps ensure that an object + * version can't be overwritten or deleted for the duration of the retention period. + */ + COMPLIANCE = 'COMPLIANCE', +} + +/** + * The default retention settings for an S3 Object Lock configuration. + * + * @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock-overview.html + */ +export class ObjectLockRetention { + /** + * Configure for Governance retention for a specified duration. + * + * With governance mode, you protect objects against being deleted by most users, but you can + * still grant some users permission to alter the retention settings or delete the object if + * necessary. You can also use governance mode to test retention-period settings before + * creating a compliance-mode retention period. + * + * @param duration the length of time for which objects should retained + * @returns the ObjectLockRetention configuration + */ + public static governance(duration: Duration): ObjectLockRetention { + return new ObjectLockRetention(ObjectLockMode.GOVERNANCE, duration); + } + + /** + * Configure for Compliance retention for a specified duration. + * + * When an object is locked in compliance mode, its retention mode can't be changed, and + * its retention period can't be shortened. Compliance mode helps ensure that an object + * version can't be overwritten or deleted for the duration of the retention period. + * + * @param duration the length of time for which objects should be retained + * @returns the ObjectLockRetention configuration + */ + public static compliance(duration: Duration): ObjectLockRetention { + return new ObjectLockRetention(ObjectLockMode.COMPLIANCE, duration); + } + + /** + * The default period for which objects should be retained. + */ + public readonly duration: Duration; + + /** + * The retention mode to use for the object lock configuration. + * + * @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock-overview.html#object-lock-retention-modes + */ + public readonly mode: ObjectLockMode; + + private constructor(mode: ObjectLockMode, duration: Duration) { + // https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock-managing.html#object-lock-managing-retention-limits + if (duration.toDays() > 365 * 100) { + throw new Error('Object Lock retention duration must be less than 100 years'); + } + if (duration.toDays() < 1) { + throw new Error('Object Lock retention duration must be at least 1 day'); + } + + this.mode = mode; + this.duration = duration; + } +} + /** * Options for creating Virtual-Hosted style URL. */ diff --git a/packages/@aws-cdk/aws-s3/package.json b/packages/@aws-cdk/aws-s3/package.json index 2ef1747d4c09e..b6b9814d26540 100644 --- a/packages/@aws-cdk/aws-s3/package.json +++ b/packages/@aws-cdk/aws-s3/package.json @@ -82,9 +82,9 @@ "devDependencies": { "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", + "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/integ-runner": "0.0.0", "@aws-cdk/integ-tests": "0.0.0", - "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", "@types/aws-lambda": "^8.10.110", "@types/jest": "^27.5.2", @@ -96,6 +96,7 @@ "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/cx-api": "0.0.0", + "@aws-cdk/region-info": "0.0.0", "constructs": "^10.0.0" }, "homepage": "https://github.com/aws/aws-cdk", @@ -105,6 +106,7 @@ "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/cx-api": "0.0.0", + "@aws-cdk/region-info": "0.0.0", "constructs": "^10.0.0" }, "engines": { diff --git a/packages/@aws-cdk/aws-s3/test/bucket.test.ts b/packages/@aws-cdk/aws-s3/test/bucket.test.ts index 39ed37c32823a..e2a6d1238da97 100644 --- a/packages/@aws-cdk/aws-s3/test/bucket.test.ts +++ b/packages/@aws-cdk/aws-s3/test/bucket.test.ts @@ -2,6 +2,7 @@ import { EOL } from 'os'; import { Annotations, Match, Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; +import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import * as cdk from '@aws-cdk/core'; import * as s3 from '../lib'; @@ -381,6 +382,110 @@ describe('bucket', () => { }); }); + test('bucket with object lock enabled but no retention', () => { + const stack = new cdk.Stack(); + new s3.Bucket(stack, 'Bucket', { + objectLockEnabled: true, + }); + Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { + ObjectLockEnabled: true, + ObjectLockConfiguration: Match.absent(), + }); + }); + + test('object lock defaults to disabled', () => { + const stack = new cdk.Stack(); + new s3.Bucket(stack, 'Bucket'); + Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { + ObjectLockEnabled: Match.absent(), + }); + }); + + test('object lock defaults to enabled when default retention is specified', () => { + const stack = new cdk.Stack(); + new s3.Bucket(stack, 'Bucket', { + objectLockDefaultRetention: s3.ObjectLockRetention.governance(cdk.Duration.days(7 * 365)), + }); + Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { + ObjectLockEnabled: true, + ObjectLockConfiguration: { + ObjectLockEnabled: 'Enabled', + Rule: { + DefaultRetention: { + Mode: 'GOVERNANCE', + Days: 7 * 365, + }, + }, + }, + }); + }); + + test('bucket with object lock enabled with governance retention', () => { + const stack = new cdk.Stack(); + new s3.Bucket(stack, 'Bucket', { + objectLockEnabled: true, + objectLockDefaultRetention: s3.ObjectLockRetention.governance(cdk.Duration.days(1)), + }); + + Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { + ObjectLockEnabled: true, + ObjectLockConfiguration: { + ObjectLockEnabled: 'Enabled', + Rule: { + DefaultRetention: { + Mode: 'GOVERNANCE', + Days: 1, + }, + }, + }, + }); + }); + + test('bucket with object lock enabled with compliance retention', () => { + const stack = new cdk.Stack(); + new s3.Bucket(stack, 'Bucket', { + objectLockEnabled: true, + objectLockDefaultRetention: s3.ObjectLockRetention.compliance(cdk.Duration.days(1)), + }); + Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { + ObjectLockEnabled: true, + ObjectLockConfiguration: { + ObjectLockEnabled: 'Enabled', + Rule: { + DefaultRetention: { + Mode: 'COMPLIANCE', + Days: 1, + }, + }, + }, + }); + }); + + test('bucket with object lock disabled throws error with retention set', () => { + const stack = new cdk.Stack(); + expect(() => new s3.Bucket(stack, 'Bucket', { + objectLockEnabled: false, + objectLockDefaultRetention: s3.ObjectLockRetention.governance(cdk.Duration.days(1)), + })).toThrow('Object Lock must be enabled to configure default retention settings'); + }); + + test('bucket with object lock requires duration than one day', () => { + const stack = new cdk.Stack(); + expect(() => new s3.Bucket(stack, 'Bucket', { + objectLockEnabled: true, + objectLockDefaultRetention: s3.ObjectLockRetention.governance(cdk.Duration.days(0)), + })).toThrow('Object Lock retention duration must be at least 1 day'); + }); + + // https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock-managing.html#object-lock-managing-retention-limits + test('bucket with object lock requires duration less than 100 years', () => { + const stack = new cdk.Stack(); + expect(() => new s3.Bucket(stack, 'Bucket', { + objectLockEnabled: true, + objectLockDefaultRetention: s3.ObjectLockRetention.governance(cdk.Duration.days(365 * 101)), + })).toThrow('Object Lock retention duration must be less than 100 years'); + }); + test('bucket with block public access set to BlockAll', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { @@ -701,9 +806,9 @@ describe('bucket', () => { }); }); - test('import can explicitly set bucket region', () => { + test('import can explicitly set bucket region with different suffix than stack', () => { const stack = new cdk.Stack(undefined, undefined, { - env: { region: 'us-east-1' }, + env: { region: 'cn-north-1' }, }); const bucket = s3.Bucket.fromBucketAttributes(stack, 'ImportedBucket', { @@ -711,8 +816,59 @@ describe('bucket', () => { region: 'eu-west-1', }); - expect(bucket.bucketRegionalDomainName).toEqual(`mybucket.s3.eu-west-1.${stack.urlSuffix}`); - expect(bucket.bucketWebsiteDomainName).toEqual(`mybucket.s3-website-eu-west-1.${stack.urlSuffix}`); + expect(bucket.bucketRegionalDomainName).toEqual('mybucket.s3.eu-west-1.amazonaws.com'); + expect(bucket.bucketWebsiteDomainName).toEqual('mybucket.s3-website-eu-west-1.amazonaws.com'); + }); + + test('new bucketWebsiteUrl format for specific region', () => { + const stack = new cdk.Stack(undefined, undefined, { + env: { region: 'us-east-2' }, + }); + + const bucket = s3.Bucket.fromBucketAttributes(stack, 'ImportedBucket', { + bucketName: 'mybucket', + }); + + expect(bucket.bucketWebsiteUrl).toEqual('http://mybucket.s3-website.us-east-2.amazonaws.com'); + }); + + test('new bucketWebsiteUrl format for specific region with cn suffix', () => { + const stack = new cdk.Stack(undefined, undefined, { + env: { region: 'cn-north-1' }, + }); + + const bucket = s3.Bucket.fromBucketAttributes(stack, 'ImportedBucket', { + bucketName: 'mybucket', + }); + + expect(bucket.bucketWebsiteUrl).toEqual('http://mybucket.s3-website.cn-north-1.amazonaws.com.cn'); + }); + + + testDeprecated('new bucketWebsiteUrl format with explicit bucketWebsiteNewUrlFormat', () => { + const stack = new cdk.Stack(undefined, undefined, { + env: { region: 'us-east-1' }, + }); + + const bucket = s3.Bucket.fromBucketAttributes(stack, 'ImportedBucket', { + bucketName: 'mybucket', + bucketWebsiteNewUrlFormat: true, + }); + + expect(bucket.bucketWebsiteUrl).toEqual('http://mybucket.s3-website.us-east-1.amazonaws.com'); + }); + + testDeprecated('old bucketWebsiteUrl format with explicit bucketWebsiteNewUrlFormat', () => { + const stack = new cdk.Stack(undefined, undefined, { + env: { region: 'us-east-2' }, + }); + + const bucket = s3.Bucket.fromBucketAttributes(stack, 'ImportedBucket', { + bucketName: 'mybucket', + bucketWebsiteNewUrlFormat: false, + }); + + expect(bucket.bucketWebsiteUrl).toEqual('http://mybucket.s3-website-us-east-2.amazonaws.com'); }); test('import needs to specify a valid bucket name', () => { @@ -1922,10 +2078,14 @@ describe('bucket', () => { 'Fn::Join': [ '', [ - 'http://my-test-bucket.s3-website-', - { Ref: 'AWS::Region' }, - '.', - { Ref: 'AWS::URLSuffix' }, + 'http://my-test-bucket.', + { + 'Fn::FindInMap': [ + 'S3staticwebsiteMap', + { Ref: 'AWS::Region' }, + 'endpoint', + ], + }, ], ], }); @@ -1933,14 +2093,17 @@ describe('bucket', () => { 'Fn::Join': [ '', [ - 'my-test-bucket.s3-website-', - { Ref: 'AWS::Region' }, - '.', - { Ref: 'AWS::URLSuffix' }, + 'my-test-bucket.', + { + 'Fn::FindInMap': [ + 'S3staticwebsiteMap', + { Ref: 'AWS::Region' }, + 'endpoint', + ], + }, ], ], }); - }); test('exports the WebsiteURL for imported buckets with url', () => { const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.js.snapshot/ServerAccessLogsImportTestDefaultTestDeployAssert076DA7F5.assets.json b/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.js.snapshot/ServerAccessLogsImportTestDefaultTestDeployAssert076DA7F5.assets.json new file mode 100644 index 0000000000000..76ee419f6b0f1 --- /dev/null +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.js.snapshot/ServerAccessLogsImportTestDefaultTestDeployAssert076DA7F5.assets.json @@ -0,0 +1,19 @@ +{ + "version": "29.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "ServerAccessLogsImportTestDefaultTestDeployAssert076DA7F5.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.js.snapshot/ServerAccessLogsImportTestDefaultTestDeployAssert076DA7F5.template.json b/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.js.snapshot/ServerAccessLogsImportTestDefaultTestDeployAssert076DA7F5.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.js.snapshot/ServerAccessLogsImportTestDefaultTestDeployAssert076DA7F5.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.js.snapshot/aws-cdk-s3-bucket-object-lock.assets.json b/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.js.snapshot/aws-cdk-s3-bucket-object-lock.assets.json new file mode 100644 index 0000000000000..5c3ec41a3c2da --- /dev/null +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.js.snapshot/aws-cdk-s3-bucket-object-lock.assets.json @@ -0,0 +1,19 @@ +{ + "version": "29.0.0", + "files": { + "e7897599241ca9562999cb8666f011365be1fbf7f990cfea3947f6026fd8fbb9": { + "source": { + "path": "aws-cdk-s3-bucket-object-lock.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "e7897599241ca9562999cb8666f011365be1fbf7f990cfea3947f6026fd8fbb9.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.js.snapshot/aws-cdk-s3-bucket-object-lock.template.json b/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.js.snapshot/aws-cdk-s3-bucket-object-lock.template.json new file mode 100644 index 0000000000000..90ab58290c227 --- /dev/null +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.js.snapshot/aws-cdk-s3-bucket-object-lock.template.json @@ -0,0 +1,63 @@ +{ + "Resources": { + "ObjectLockBucketA9F4F5AC": { + "Type": "AWS::S3::Bucket", + "Properties": { + "ObjectLockEnabled": true + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "ObjectLockWithRetentionBucket31ED9B51": { + "Type": "AWS::S3::Bucket", + "Properties": { + "ObjectLockConfiguration": { + "ObjectLockEnabled": "Enabled", + "Rule": { + "DefaultRetention": { + "Days": 2, + "Mode": "GOVERNANCE" + } + } + }, + "ObjectLockEnabled": true + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.js.snapshot/cdk.out b/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.js.snapshot/cdk.out new file mode 100644 index 0000000000000..d8b441d447f8a --- /dev/null +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"29.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.js.snapshot/integ.json b/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.js.snapshot/integ.json new file mode 100644 index 0000000000000..52345f6fc37d6 --- /dev/null +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "29.0.0", + "testCases": { + "ServerAccessLogsImportTest/DefaultTest": { + "stacks": [ + "aws-cdk-s3-bucket-object-lock" + ], + "assertionStack": "ServerAccessLogsImportTest/DefaultTest/DeployAssert", + "assertionStackName": "ServerAccessLogsImportTestDefaultTestDeployAssert076DA7F5" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.js.snapshot/manifest.json b/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.js.snapshot/manifest.json new file mode 100644 index 0000000000000..a9877309176d5 --- /dev/null +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.js.snapshot/manifest.json @@ -0,0 +1,117 @@ +{ + "version": "29.0.0", + "artifacts": { + "aws-cdk-s3-bucket-object-lock.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-cdk-s3-bucket-object-lock.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-cdk-s3-bucket-object-lock": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-cdk-s3-bucket-object-lock.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/e7897599241ca9562999cb8666f011365be1fbf7f990cfea3947f6026fd8fbb9.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-cdk-s3-bucket-object-lock.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "aws-cdk-s3-bucket-object-lock.assets" + ], + "metadata": { + "/aws-cdk-s3-bucket-object-lock/ObjectLockBucket/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "ObjectLockBucketA9F4F5AC" + } + ], + "/aws-cdk-s3-bucket-object-lock/ObjectLockWithRetentionBucket/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "ObjectLockWithRetentionBucket31ED9B51" + } + ], + "/aws-cdk-s3-bucket-object-lock/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/aws-cdk-s3-bucket-object-lock/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "aws-cdk-s3-bucket-object-lock" + }, + "ServerAccessLogsImportTestDefaultTestDeployAssert076DA7F5.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "ServerAccessLogsImportTestDefaultTestDeployAssert076DA7F5.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "ServerAccessLogsImportTestDefaultTestDeployAssert076DA7F5": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "ServerAccessLogsImportTestDefaultTestDeployAssert076DA7F5.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "ServerAccessLogsImportTestDefaultTestDeployAssert076DA7F5.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "ServerAccessLogsImportTestDefaultTestDeployAssert076DA7F5.assets" + ], + "metadata": { + "/ServerAccessLogsImportTest/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/ServerAccessLogsImportTest/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "ServerAccessLogsImportTest/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.js.snapshot/tree.json b/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.js.snapshot/tree.json new file mode 100644 index 0000000000000..14e8a242e087e --- /dev/null +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.js.snapshot/tree.json @@ -0,0 +1,158 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "aws-cdk-s3-bucket-object-lock": { + "id": "aws-cdk-s3-bucket-object-lock", + "path": "aws-cdk-s3-bucket-object-lock", + "children": { + "ObjectLockBucket": { + "id": "ObjectLockBucket", + "path": "aws-cdk-s3-bucket-object-lock/ObjectLockBucket", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-s3-bucket-object-lock/ObjectLockBucket/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::Bucket", + "aws:cdk:cloudformation:props": { + "objectLockEnabled": true + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.CfnBucket", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.Bucket", + "version": "0.0.0" + } + }, + "ObjectLockWithRetentionBucket": { + "id": "ObjectLockWithRetentionBucket", + "path": "aws-cdk-s3-bucket-object-lock/ObjectLockWithRetentionBucket", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-s3-bucket-object-lock/ObjectLockWithRetentionBucket/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::Bucket", + "aws:cdk:cloudformation:props": { + "objectLockConfiguration": { + "objectLockEnabled": "Enabled", + "rule": { + "defaultRetention": { + "days": 2, + "mode": "GOVERNANCE" + } + } + }, + "objectLockEnabled": true + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.CfnBucket", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.Bucket", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "aws-cdk-s3-bucket-object-lock/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "aws-cdk-s3-bucket-object-lock/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "ServerAccessLogsImportTest": { + "id": "ServerAccessLogsImportTest", + "path": "ServerAccessLogsImportTest", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "ServerAccessLogsImportTest/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "ServerAccessLogsImportTest/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.216" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "ServerAccessLogsImportTest/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "ServerAccessLogsImportTest/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "ServerAccessLogsImportTest/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.216" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.ts b/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.ts new file mode 100644 index 0000000000000..72434a129e426 --- /dev/null +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket-object-lock.ts @@ -0,0 +1,21 @@ +#!/usr/bin/env node +import * as cdk from '@aws-cdk/core'; +import * as integ from '@aws-cdk/integ-tests'; +import * as s3 from '../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-cdk-s3-bucket-object-lock'); + +new s3.Bucket(stack, 'ObjectLockBucket', { + objectLockEnabled: true, +}); + +new s3.Bucket(stack, 'ObjectLockWithRetentionBucket', { + objectLockDefaultRetention: s3.ObjectLockRetention.governance(cdk.Duration.days(2)), +}); + +new integ.IntegTest(app, 'ServerAccessLogsImportTest', { + testCases: [stack], +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/aspects/stack-associator.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/aspects/stack-associator.ts index 982085a6b9cf3..9a59fc58bbeaf 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/aspects/stack-associator.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/aspects/stack-associator.ts @@ -23,7 +23,7 @@ abstract class StackAssociatorBase implements IAspect { if (Stage.isStage(childNode)) { var stageAssociated = this.applicationAssociator?.isStageAssociated(childNode); if (stageAssociated === false) { - this.error(childNode, 'Associate Stage: ' + childNode.stageName + ' to ensure all stacks in your cdk app are associated with AppRegistry. ' + this.warning(childNode, 'Associate Stage: ' + childNode.stageName + ' to ensure all stacks in your cdk app are associated with AppRegistry. ' + 'You can use ApplicationAssociator.associateStage to associate any stage.'); } } @@ -45,16 +45,6 @@ abstract class StackAssociatorBase implements IAspect { this.application.associateApplicationWithStack(node); } - /** - * Adds an error annotation to a node. - * - * @param node The scope to add the error to. - * @param message The error message. - */ - private error(node: IConstruct, message: string): void { - Annotations.of(node).addError(message); - } - /** * Adds a warning annotation to a node. * diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/target-application.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/target-application.ts index db3cafa031758..68e77d73031ff 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/target-application.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/target-application.ts @@ -11,6 +11,7 @@ export interface TargetApplicationCommonOptions extends cdk.StackProps { * refer to it in the [AWS CDK Toolkit](https://docs.aws.amazon.com/cdk/v2/guide/cli.html). * * @default - ApplicationAssociatorStack + * @deprecated - Use `stackName` instead to control the name of the stack */ readonly stackId?: string; } @@ -91,6 +92,8 @@ class CreateTargetApplication extends TargetApplication { } public bind(scope: Construct): BindTargetApplicationResult { const stackId = this.applicationOptions.stackId ?? 'ApplicationAssociatorStack'; + (this.applicationOptions.stackName as string) = + this.applicationOptions.stackName || `Application-${this.applicationOptions.applicationName}-Stack`; (this.applicationOptions.description as string) = this.applicationOptions.description || 'Stack to create AppRegistry application'; (this.applicationOptions.env as cdk.Environment) = @@ -117,7 +120,11 @@ class ExistingTargetApplication extends TargetApplication { super(); } public bind(scope: Construct): BindTargetApplicationResult { + const arnComponents = cdk.Arn.split(this.applicationOptions.applicationArnValue, cdk.ArnFormat.SLASH_RESOURCE_SLASH_RESOURCE_NAME); + const applicationId = arnComponents.resourceName; const stackId = this.applicationOptions.stackId ?? 'ApplicationAssociatorStack'; + (this.applicationOptions.stackName as string) = + this.applicationOptions.stackName || `Application-${applicationId}-Stack`; const applicationStack = new cdk.Stack(scope, stackId, this.applicationOptions); const appRegApplication = Application.fromApplicationArn(applicationStack, 'ExistingApplication', this.applicationOptions.applicationArnValue); return { diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/package.json b/packages/@aws-cdk/aws-servicecatalogappregistry/package.json index aa5401b6ac812..ddee116e8670f 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/package.json +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/package.json @@ -86,6 +86,7 @@ "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/integ-runner": "0.0.0", + "@aws-cdk/integ-tests": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", "@aws-cdk/aws-codecommit": "0.0.0", diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.test.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.test.ts index fe6bf236b28ae..be0221ab49340 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.test.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.test.ts @@ -139,7 +139,7 @@ describe('Scope based Associations with Application with Cross Region/Account', associateStage: false, }); app.synth(); - Annotations.fromStack(pipelineStack).hasError('*', + Annotations.fromStack(pipelineStack).hasWarning('*', 'Associate Stage: SampleStage to ensure all stacks in your cdk app are associated with AppRegistry. You can use ApplicationAssociator.associateStage to associate any stage.'); }); diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/ApplicationAssociatorStack.assets.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/ApplicationAssociatorStack.assets.json new file mode 100644 index 0000000000000..70221d811cb08 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/ApplicationAssociatorStack.assets.json @@ -0,0 +1,20 @@ +{ + "version": "29.0.0", + "files": { + "c4d674e9642d6dbbd0df5c93890473101fc95bbc70de7e28d79ba771b5284de8": { + "source": { + "path": "ApplicationAssociatorStack.template.json", + "packaging": "file" + }, + "destinations": { + "416623072619-us-east-1": { + "bucketName": "cdk-hnb659fds-assets-416623072619-us-east-1", + "objectKey": "c4d674e9642d6dbbd0df5c93890473101fc95bbc70de7e28d79ba771b5284de8.json", + "region": "us-east-1", + "assumeRoleArn": "arn:${AWS::Partition}:iam::416623072619:role/cdk-hnb659fds-file-publishing-role-416623072619-us-east-1" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/ApplicationAssociatorStack.template.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/ApplicationAssociatorStack.template.json new file mode 100644 index 0000000000000..e9e881bc90625 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/ApplicationAssociatorStack.template.json @@ -0,0 +1,70 @@ +{ + "Description": "Stack to create AppRegistry application", + "Resources": { + "DefaultCdkApplication4573D5A3": { + "Type": "AWS::ServiceCatalogAppRegistry::Application", + "Properties": { + "Name": "AppRegistryAssociatedApplication", + "Description": "Application containing stacks deployed via CDK.", + "Tags": { + "managedBy": "CDK_Application_Associator" + } + } + }, + "AppRegistryAssociation": { + "Type": "AWS::ServiceCatalogAppRegistry::ResourceAssociation", + "Properties": { + "Application": { + "Fn::GetAtt": [ + "DefaultCdkApplication4573D5A3", + "Id" + ] + }, + "Resource": { + "Ref": "AWS::StackId" + }, + "ResourceType": "CFN_STACK" + } + } + }, + "Outputs": { + "DefaultCdkApplicationApplicationManagerUrl27C138EF": { + "Description": "Application manager url for the application created.", + "Value": "https://us-east-1.console.aws.amazon.com/systems-manager/appmanager/application/AWS_AppRegistry_Application-AppRegistryAssociatedApplication" + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/ApplicationAssociatorTestDefaultTestDeployAssert2A5F2DB9.assets.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/ApplicationAssociatorTestDefaultTestDeployAssert2A5F2DB9.assets.json new file mode 100644 index 0000000000000..19cf0cfbe5262 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/ApplicationAssociatorTestDefaultTestDeployAssert2A5F2DB9.assets.json @@ -0,0 +1,19 @@ +{ + "version": "29.0.0", + "files": { + "19dd33f3c17e59cafd22b9459b0a8d9bedbd42252737fedb06b2bcdbcf7809cc": { + "source": { + "path": "ApplicationAssociatorTestDefaultTestDeployAssert2A5F2DB9.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "19dd33f3c17e59cafd22b9459b0a8d9bedbd42252737fedb06b2bcdbcf7809cc.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/ApplicationAssociatorTestDefaultTestDeployAssert2A5F2DB9.template.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/ApplicationAssociatorTestDefaultTestDeployAssert2A5F2DB9.template.json new file mode 100644 index 0000000000000..ecc817b74774a --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/ApplicationAssociatorTestDefaultTestDeployAssert2A5F2DB9.template.json @@ -0,0 +1,48 @@ +{ + "Resources": { + "AppRegistryAssociation": { + "Type": "AWS::ServiceCatalogAppRegistry::ResourceAssociation", + "Properties": { + "Application": "AppRegistryAssociatedApplication", + "Resource": { + "Ref": "AWS::StackId" + }, + "ResourceType": "CFN_STACK" + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/cdk.out b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/cdk.out new file mode 100644 index 0000000000000..d8b441d447f8a --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"29.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/integ-servicecatalogappregistry-application.assets.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/integ-servicecatalogappregistry-application.assets.json new file mode 100644 index 0000000000000..465a46f897a34 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/integ-servicecatalogappregistry-application.assets.json @@ -0,0 +1,19 @@ +{ + "version": "29.0.0", + "files": { + "19dd33f3c17e59cafd22b9459b0a8d9bedbd42252737fedb06b2bcdbcf7809cc": { + "source": { + "path": "integ-servicecatalogappregistry-application.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "19dd33f3c17e59cafd22b9459b0a8d9bedbd42252737fedb06b2bcdbcf7809cc.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/integ-servicecatalogappregistry-application.template.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/integ-servicecatalogappregistry-application.template.json new file mode 100644 index 0000000000000..ecc817b74774a --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/integ-servicecatalogappregistry-application.template.json @@ -0,0 +1,48 @@ +{ + "Resources": { + "AppRegistryAssociation": { + "Type": "AWS::ServiceCatalogAppRegistry::ResourceAssociation", + "Properties": { + "Application": "AppRegistryAssociatedApplication", + "Resource": { + "Ref": "AWS::StackId" + }, + "ResourceType": "CFN_STACK" + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/integ.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/integ.json new file mode 100644 index 0000000000000..6f9dd948020ab --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "29.0.0", + "testCases": { + "ApplicationAssociatorTest/DefaultTest": { + "stacks": [ + "integ-servicecatalogappregistry-application" + ], + "assertionStack": "ApplicationAssociatorTest/DefaultTest/DeployAssert", + "assertionStackName": "ApplicationAssociatorTestDefaultTestDeployAssert2A5F2DB9" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/integservicecatalogappregistryapplicationresourcesStack4399A149.assets.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/integservicecatalogappregistryapplicationresourcesStack4399A149.assets.json new file mode 100644 index 0000000000000..9ca55e04832e1 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/integservicecatalogappregistryapplicationresourcesStack4399A149.assets.json @@ -0,0 +1,19 @@ +{ + "version": "29.0.0", + "files": { + "19dd33f3c17e59cafd22b9459b0a8d9bedbd42252737fedb06b2bcdbcf7809cc": { + "source": { + "path": "integservicecatalogappregistryapplicationresourcesStack4399A149.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "19dd33f3c17e59cafd22b9459b0a8d9bedbd42252737fedb06b2bcdbcf7809cc.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/integservicecatalogappregistryapplicationresourcesStack4399A149.template.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/integservicecatalogappregistryapplicationresourcesStack4399A149.template.json new file mode 100644 index 0000000000000..ecc817b74774a --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/integservicecatalogappregistryapplicationresourcesStack4399A149.template.json @@ -0,0 +1,48 @@ +{ + "Resources": { + "AppRegistryAssociation": { + "Type": "AWS::ServiceCatalogAppRegistry::ResourceAssociation", + "Properties": { + "Application": "AppRegistryAssociatedApplication", + "Resource": { + "Ref": "AWS::StackId" + }, + "ResourceType": "CFN_STACK" + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/manifest.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/manifest.json new file mode 100644 index 0000000000000..60176a0715996 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/manifest.json @@ -0,0 +1,269 @@ +{ + "version": "29.0.0", + "artifacts": { + "integservicecatalogappregistryapplicationresourcesStack4399A149.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "integservicecatalogappregistryapplicationresourcesStack4399A149.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "integservicecatalogappregistryapplicationresourcesStack4399A149": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "integservicecatalogappregistryapplicationresourcesStack4399A149.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/19dd33f3c17e59cafd22b9459b0a8d9bedbd42252737fedb06b2bcdbcf7809cc.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "integservicecatalogappregistryapplicationresourcesStack4399A149.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "ApplicationAssociatorStack", + "integservicecatalogappregistryapplicationresourcesStack4399A149.assets" + ], + "metadata": { + "/integ-servicecatalogappregistry-application/resourcesStack": [ + { + "type": "aws:cdk:warning", + "data": "Environment agnostic stack determined, AppRegistry association might not work as expected in case you deploy cross-region or cross-account stack." + }, + { + "type": "aws:cdk:warning", + "data": "Environment agnostic stack determined, AppRegistry association might not work as expected in case you deploy cross-region or cross-account stack." + } + ], + "/integ-servicecatalogappregistry-application/resourcesStack/AppRegistryAssociation": [ + { + "type": "aws:cdk:logicalId", + "data": "AppRegistryAssociation" + } + ], + "/integ-servicecatalogappregistry-application/resourcesStack/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/integ-servicecatalogappregistry-application/resourcesStack/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "integ-servicecatalogappregistry-application/resourcesStack" + }, + "integ-servicecatalogappregistry-application.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "integ-servicecatalogappregistry-application.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "integ-servicecatalogappregistry-application": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "integ-servicecatalogappregistry-application.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/19dd33f3c17e59cafd22b9459b0a8d9bedbd42252737fedb06b2bcdbcf7809cc.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "integ-servicecatalogappregistry-application.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "ApplicationAssociatorStack", + "integ-servicecatalogappregistry-application.assets" + ], + "metadata": { + "/integ-servicecatalogappregistry-application": [ + { + "type": "aws:cdk:warning", + "data": "Environment agnostic stack determined, AppRegistry association might not work as expected in case you deploy cross-region or cross-account stack." + }, + { + "type": "aws:cdk:warning", + "data": "Environment agnostic stack determined, AppRegistry association might not work as expected in case you deploy cross-region or cross-account stack." + } + ], + "/integ-servicecatalogappregistry-application/AppRegistryAssociation": [ + { + "type": "aws:cdk:logicalId", + "data": "AppRegistryAssociation" + } + ], + "/integ-servicecatalogappregistry-application/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/integ-servicecatalogappregistry-application/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "integ-servicecatalogappregistry-application" + }, + "ApplicationAssociatorStack.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "ApplicationAssociatorStack.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "ApplicationAssociatorStack": { + "type": "aws:cloudformation:stack", + "environment": "aws://416623072619/us-east-1", + "properties": { + "templateFile": "ApplicationAssociatorStack.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::416623072619:role/cdk-hnb659fds-deploy-role-416623072619-us-east-1", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::416623072619:role/cdk-hnb659fds-cfn-exec-role-416623072619-us-east-1", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-416623072619-us-east-1/c4d674e9642d6dbbd0df5c93890473101fc95bbc70de7e28d79ba771b5284de8.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "ApplicationAssociatorStack.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::416623072619:role/cdk-hnb659fds-lookup-role-416623072619-us-east-1", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + }, + "stackName": "Application-AppRegistryAssociatedApplication-Stack" + }, + "dependencies": [ + "ApplicationAssociatorStack.assets" + ], + "metadata": { + "/ApplicationAssociatorStack/DefaultCdkApplication/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "DefaultCdkApplication4573D5A3" + } + ], + "/ApplicationAssociatorStack/DefaultCdkApplication/ApplicationManagerUrl": [ + { + "type": "aws:cdk:logicalId", + "data": "DefaultCdkApplicationApplicationManagerUrl27C138EF" + } + ], + "/ApplicationAssociatorStack/AppRegistryAssociation": [ + { + "type": "aws:cdk:logicalId", + "data": "AppRegistryAssociation" + } + ], + "/ApplicationAssociatorStack/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/ApplicationAssociatorStack/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "ApplicationAssociatorStack" + }, + "ApplicationAssociatorTestDefaultTestDeployAssert2A5F2DB9.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "ApplicationAssociatorTestDefaultTestDeployAssert2A5F2DB9.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "ApplicationAssociatorTestDefaultTestDeployAssert2A5F2DB9": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "ApplicationAssociatorTestDefaultTestDeployAssert2A5F2DB9.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/19dd33f3c17e59cafd22b9459b0a8d9bedbd42252737fedb06b2bcdbcf7809cc.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "ApplicationAssociatorTestDefaultTestDeployAssert2A5F2DB9.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "ApplicationAssociatorStack", + "ApplicationAssociatorTestDefaultTestDeployAssert2A5F2DB9.assets" + ], + "metadata": { + "/ApplicationAssociatorTest/DefaultTest/DeployAssert": [ + { + "type": "aws:cdk:warning", + "data": "Environment agnostic stack determined, AppRegistry association might not work as expected in case you deploy cross-region or cross-account stack." + }, + { + "type": "aws:cdk:warning", + "data": "Environment agnostic stack determined, AppRegistry association might not work as expected in case you deploy cross-region or cross-account stack." + } + ], + "/ApplicationAssociatorTest/DefaultTest/DeployAssert/AppRegistryAssociation": [ + { + "type": "aws:cdk:logicalId", + "data": "AppRegistryAssociation" + } + ], + "/ApplicationAssociatorTest/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/ApplicationAssociatorTest/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "ApplicationAssociatorTest/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/tree.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/tree.json new file mode 100644 index 0000000000000..cdc1e803cbc18 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.js.snapshot/tree.json @@ -0,0 +1,274 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "integ-servicecatalogappregistry-application": { + "id": "integ-servicecatalogappregistry-application", + "path": "integ-servicecatalogappregistry-application", + "children": { + "resourcesStack": { + "id": "resourcesStack", + "path": "integ-servicecatalogappregistry-application/resourcesStack", + "children": { + "AppRegistryAssociation": { + "id": "AppRegistryAssociation", + "path": "integ-servicecatalogappregistry-application/resourcesStack/AppRegistryAssociation", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ServiceCatalogAppRegistry::ResourceAssociation", + "aws:cdk:cloudformation:props": { + "application": "AppRegistryAssociatedApplication", + "resource": { + "Ref": "AWS::StackId" + }, + "resourceType": "CFN_STACK" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-servicecatalogappregistry.CfnResourceAssociation", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "integ-servicecatalogappregistry-application/resourcesStack/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "integ-servicecatalogappregistry-application/resourcesStack/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "AppRegistryAssociation": { + "id": "AppRegistryAssociation", + "path": "integ-servicecatalogappregistry-application/AppRegistryAssociation", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ServiceCatalogAppRegistry::ResourceAssociation", + "aws:cdk:cloudformation:props": { + "application": "AppRegistryAssociatedApplication", + "resource": { + "Ref": "AWS::StackId" + }, + "resourceType": "CFN_STACK" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-servicecatalogappregistry.CfnResourceAssociation", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "integ-servicecatalogappregistry-application/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "integ-servicecatalogappregistry-application/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "RegisterCdkApplication": { + "id": "RegisterCdkApplication", + "path": "RegisterCdkApplication", + "constructInfo": { + "fqn": "@aws-cdk/aws-servicecatalogappregistry.ApplicationAssociator", + "version": "0.0.0" + } + }, + "ApplicationAssociatorStack": { + "id": "ApplicationAssociatorStack", + "path": "ApplicationAssociatorStack", + "children": { + "DefaultCdkApplication": { + "id": "DefaultCdkApplication", + "path": "ApplicationAssociatorStack/DefaultCdkApplication", + "children": { + "Resource": { + "id": "Resource", + "path": "ApplicationAssociatorStack/DefaultCdkApplication/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ServiceCatalogAppRegistry::Application", + "aws:cdk:cloudformation:props": { + "name": "AppRegistryAssociatedApplication", + "description": "Application containing stacks deployed via CDK.", + "tags": { + "managedBy": "CDK_Application_Associator" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-servicecatalogappregistry.CfnApplication", + "version": "0.0.0" + } + }, + "ApplicationManagerUrl": { + "id": "ApplicationManagerUrl", + "path": "ApplicationAssociatorStack/DefaultCdkApplication/ApplicationManagerUrl", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-servicecatalogappregistry.Application", + "version": "0.0.0" + } + }, + "AppRegistryAssociation": { + "id": "AppRegistryAssociation", + "path": "ApplicationAssociatorStack/AppRegistryAssociation", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ServiceCatalogAppRegistry::ResourceAssociation", + "aws:cdk:cloudformation:props": { + "application": { + "Fn::GetAtt": [ + "DefaultCdkApplication4573D5A3", + "Id" + ] + }, + "resource": { + "Ref": "AWS::StackId" + }, + "resourceType": "CFN_STACK" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-servicecatalogappregistry.CfnResourceAssociation", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "ApplicationAssociatorStack/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "ApplicationAssociatorStack/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "ApplicationAssociatorTest": { + "id": "ApplicationAssociatorTest", + "path": "ApplicationAssociatorTest", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "ApplicationAssociatorTest/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "ApplicationAssociatorTest/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.216" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "ApplicationAssociatorTest/DefaultTest/DeployAssert", + "children": { + "AppRegistryAssociation": { + "id": "AppRegistryAssociation", + "path": "ApplicationAssociatorTest/DefaultTest/DeployAssert/AppRegistryAssociation", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ServiceCatalogAppRegistry::ResourceAssociation", + "aws:cdk:cloudformation:props": { + "application": "AppRegistryAssociatedApplication", + "resource": { + "Ref": "AWS::StackId" + }, + "resourceType": "CFN_STACK" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-servicecatalogappregistry.CfnResourceAssociation", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "ApplicationAssociatorTest/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "ApplicationAssociatorTest/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.216" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.ts new file mode 100644 index 0000000000000..b8251b9998b31 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association-no-stack-name.ts @@ -0,0 +1,20 @@ +import * as cdk from '@aws-cdk/core'; +import * as integ from '@aws-cdk/integ-tests'; +import * as appreg from '../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'integ-servicecatalogappregistry-application'); + +new appreg.ApplicationAssociator(app, 'RegisterCdkApplication', { + applications: [appreg.TargetApplication.createApplicationStack({ + applicationName: 'AppRegistryAssociatedApplication', + })], +}); + +new cdk.Stack(stack, 'resourcesStack'); + +new integ.IntegTest(app, 'ApplicationAssociatorTest', { + testCases: [stack], +}); + +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-simspaceweaver/.eslintrc.js b/packages/@aws-cdk/aws-simspaceweaver/.eslintrc.js new file mode 100644 index 0000000000000..2658ee8727166 --- /dev/null +++ b/packages/@aws-cdk/aws-simspaceweaver/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-simspaceweaver/.gitignore b/packages/@aws-cdk/aws-simspaceweaver/.gitignore new file mode 100644 index 0000000000000..62ebc95d75ce6 --- /dev/null +++ b/packages/@aws-cdk/aws-simspaceweaver/.gitignore @@ -0,0 +1,19 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk +nyc.config.js +!.eslintrc.js +!jest.config.js +junit.xml diff --git a/packages/@aws-cdk/aws-simspaceweaver/.npmignore b/packages/@aws-cdk/aws-simspaceweaver/.npmignore new file mode 100644 index 0000000000000..f931fede67c44 --- /dev/null +++ b/packages/@aws-cdk/aws-simspaceweaver/.npmignore @@ -0,0 +1,29 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json + +.eslintrc.js +jest.config.js + +# exclude cdk artifacts +**/cdk.out +junit.xml +test/ +!*.lit.ts diff --git a/packages/@aws-cdk/aws-simspaceweaver/LICENSE b/packages/@aws-cdk/aws-simspaceweaver/LICENSE new file mode 100644 index 0000000000000..9b722c65c5481 --- /dev/null +++ b/packages/@aws-cdk/aws-simspaceweaver/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-simspaceweaver/NOTICE b/packages/@aws-cdk/aws-simspaceweaver/NOTICE new file mode 100644 index 0000000000000..a27b7dd317649 --- /dev/null +++ b/packages/@aws-cdk/aws-simspaceweaver/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-simspaceweaver/README.md b/packages/@aws-cdk/aws-simspaceweaver/README.md new file mode 100644 index 0000000000000..c8d5e511bc634 --- /dev/null +++ b/packages/@aws-cdk/aws-simspaceweaver/README.md @@ -0,0 +1,39 @@ +# AWS::SimSpaceWeaver Construct Library + + +--- + +![cfn-resources: Stable](https://img.shields.io/badge/cfn--resources-stable-success.svg?style=for-the-badge) + +> All classes with the `Cfn` prefix in this module ([CFN Resources]) are always stable and safe to use. +> +> [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib + +--- + + + +This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. + +```ts nofixture +import * as simspaceweaver from '@aws-cdk/aws-simspaceweaver'; +``` + + + +There are no official hand-written ([L2](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) constructs for this service yet. Here are some suggestions on how to proceed: + +- Search [Construct Hub for SimSpaceWeaver construct libraries](https://constructs.dev/search?q=simspaceweaver) +- Use the automatically generated [L1](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_l1_using) constructs, in the same way you would use [the CloudFormation AWS::SimSpaceWeaver resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_SimSpaceWeaver.html) directly. + + + + +There are no hand-written ([L2](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) constructs for this service yet. +However, you can still use the automatically generated [L1](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_l1_using) constructs, and use this service exactly as you would using CloudFormation directly. + +For more information on the resources and properties available for this service, see the [CloudFormation documentation for AWS::SimSpaceWeaver](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_SimSpaceWeaver.html). + +(Read the [CDK Contributing Guide](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and submit an RFC if you are interested in contributing to this construct library.) + + diff --git a/packages/@aws-cdk/aws-simspaceweaver/jest.config.js b/packages/@aws-cdk/aws-simspaceweaver/jest.config.js new file mode 100644 index 0000000000000..3a2fd93a1228a --- /dev/null +++ b/packages/@aws-cdk/aws-simspaceweaver/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-simspaceweaver/lib/index.ts b/packages/@aws-cdk/aws-simspaceweaver/lib/index.ts new file mode 100644 index 0000000000000..dcac65b8d156e --- /dev/null +++ b/packages/@aws-cdk/aws-simspaceweaver/lib/index.ts @@ -0,0 +1,2 @@ +// AWS::SimSpaceWeaver CloudFormation Resources: +export * from './simspaceweaver.generated'; diff --git a/packages/@aws-cdk/aws-simspaceweaver/package.json b/packages/@aws-cdk/aws-simspaceweaver/package.json new file mode 100644 index 0000000000000..46ed41f3b9000 --- /dev/null +++ b/packages/@aws-cdk/aws-simspaceweaver/package.json @@ -0,0 +1,113 @@ +{ + "name": "@aws-cdk/aws-simspaceweaver", + "version": "0.0.0", + "description": "AWS::SimSpaceWeaver Construct Library", + "private": true, + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "projectReferences": true, + "targets": { + "dotnet": { + "namespace": "Amazon.CDK.AWS.SimSpaceWeaver", + "packageId": "Amazon.CDK.AWS.SimSpaceWeaver", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/main/logo/default-256-dark.png" + }, + "java": { + "package": "software.amazon.awscdk.services.simspaceweaver", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "simspaceweaver" + } + }, + "python": { + "classifiers": [ + "Framework :: AWS CDK", + "Framework :: AWS CDK :: 2" + ], + "distName": "aws-cdk.aws-simspaceweaver", + "module": "aws_cdk.aws_simspaceweaver" + } + }, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-simspaceweaver" + }, + "homepage": "https://github.com/aws/aws-cdk", + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "cfn2ts": "cfn2ts", + "build+test": "yarn build && yarn test", + "build+test+package": "yarn build+test && yarn package", + "compat": "cdk-compat", + "gen": "cfn2ts", + "rosetta:extract": "yarn --silent jsii-rosetta extract", + "build+extract": "yarn build && yarn rosetta:extract", + "build+test+extract": "yarn build+test && yarn rosetta:extract" + }, + "cdk-build": { + "cloudformation": "AWS::SimSpaceWeaver", + "jest": true, + "env": { + "AWSLINT_BASE_CONSTRUCT": "true" + } + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "AWS::SimSpaceWeaver", + "aws-simspaceweaver" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assertions": "0.0.0", + "@aws-cdk/cdk-build-tools": "0.0.0", + "@aws-cdk/cfn2ts": "0.0.0", + "@aws-cdk/pkglint": "0.0.0", + "@types/jest": "^27.5.2" + }, + "dependencies": { + "@aws-cdk/core": "0.0.0", + "constructs": "^10.0.0" + }, + "peerDependencies": { + "@aws-cdk/core": "0.0.0", + "constructs": "^10.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "stability": "experimental", + "maturity": "cfn-only", + "awscdkio": { + "announce": false + }, + "publishConfig": { + "tag": "latest" + } +} diff --git a/packages/@aws-cdk/aws-simspaceweaver/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-simspaceweaver/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..e208762bca03c --- /dev/null +++ b/packages/@aws-cdk/aws-simspaceweaver/rosetta/default.ts-fixture @@ -0,0 +1,8 @@ +import { Construct } from 'constructs'; +import { Stack } from '@aws-cdk/core'; + +class MyStack extends Stack { + constructor(scope: Construct, id: string) { + /// here + } +} diff --git a/packages/@aws-cdk/aws-simspaceweaver/test/simspaceweaver.test.ts b/packages/@aws-cdk/aws-simspaceweaver/test/simspaceweaver.test.ts new file mode 100644 index 0000000000000..465c7bdea0693 --- /dev/null +++ b/packages/@aws-cdk/aws-simspaceweaver/test/simspaceweaver.test.ts @@ -0,0 +1,6 @@ +import '@aws-cdk/assertions'; +import {} from '../lib'; + +test('No tests are specified for this package', () => { + expect(true).toBe(true); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions/README.md b/packages/@aws-cdk/aws-stepfunctions/README.md index 5e98aee6775b3..83a7caa3cfd2c 100644 --- a/packages/@aws-cdk/aws-stepfunctions/README.md +++ b/packages/@aws-cdk/aws-stepfunctions/README.md @@ -460,7 +460,7 @@ It's possible that the high-level constructs for the states or `stepfunctions-ta the states or service integrations you are looking for. The primary reasons for this lack of functionality are: -* A [service integration](https://docs.aws.amazon.com/step-functions/latest/dg/concepts-service-integrations.html) is available through Amazon States Langauge, but not available as construct +* A [service integration](https://docs.aws.amazon.com/step-functions/latest/dg/concepts-service-integrations.html) is available through Amazon States Language, but not available as construct classes in the CDK. * The state or state properties are available through Step Functions, but are not configurable through constructs diff --git a/packages/@aws-cdk/aws-synthetics/README.md b/packages/@aws-cdk/aws-synthetics/README.md index d20a24688e8ec..ed4376f45b40c 100644 --- a/packages/@aws-cdk/aws-synthetics/README.md +++ b/packages/@aws-cdk/aws-synthetics/README.md @@ -115,7 +115,7 @@ If you want the canary to run just once upon deployment, you can use `Schedule.o You can specify whether the AWS CloudFormation is to also delete the Lambda functions and layers used by this canary, when the canary is deleted. -This can be provisioned by setting the `DeleteLambdaResourcesOnCanaryDeletion` property to `true` when we define the canary. +This can be provisioned by setting the `enableAutoDeleteLambdas` property to `true` when we define the canary. ```ts const stack = new Stack(); @@ -130,8 +130,8 @@ const canary = new synthetics.Canary(stack, 'Canary', { }); ``` -Even when set to `true` there are resources such as S3 buckets/logs that do NOT get deleted and are to be deleted manually. - +Synthetic Canaries create additional resources under the hood beyond Lambda functions. Setting `enableAutoDeleteLambdas: true` will take care of +cleaning up Lambda functions on deletion, but you still have to manually delete other resources like S3 buckets and CloudWatch logs. ### Configuring the Canary Script diff --git a/packages/@aws-cdk/cfnspec/CHANGELOG.md b/packages/@aws-cdk/cfnspec/CHANGELOG.md index 7d2642e7fe710..595c78553d447 100644 --- a/packages/@aws-cdk/cfnspec/CHANGELOG.md +++ b/packages/@aws-cdk/cfnspec/CHANGELOG.md @@ -1,3 +1,142 @@ +# CloudFormation Resource Specification v109.0.0 + +## New Resource Types + +* AWS::CloudTrail::Channel +* AWS::CloudTrail::ResourcePolicy +* AWS::EC2::IPAMPoolCidr +* AWS::EC2::IPAMResourceDiscovery +* AWS::EC2::IPAMResourceDiscoveryAssociation +* AWS::Omics::AnnotationStore +* AWS::Omics::ReferenceStore +* AWS::Omics::RunGroup +* AWS::Omics::SequenceStore +* AWS::Omics::VariantStore +* AWS::Omics::Workflow +* AWS::SageMaker::ModelCard +* AWS::SimSpaceWeaver::Simulation + +## Attribute Changes + +* AWS::AmplifyUIBuilder::Component AppId (__deleted__) +* AWS::AmplifyUIBuilder::Component EnvironmentName (__deleted__) +* AWS::AmplifyUIBuilder::Theme AppId (__deleted__) +* AWS::AmplifyUIBuilder::Theme CreatedAt (__deleted__) +* AWS::AmplifyUIBuilder::Theme EnvironmentName (__deleted__) +* AWS::AmplifyUIBuilder::Theme ModifiedAt (__deleted__) +* AWS::OpsWorksCM::Server Id (__deleted__) +* AWS::OpsWorksCM::Server ServerName (__added__) +* AWS::RDS::DBProxyEndpoint TargetRole (__added__) + +## Property Changes + +* AWS::AmplifyUIBuilder::Component AppId (__added__) +* AWS::AmplifyUIBuilder::Component EnvironmentName (__added__) +* AWS::AmplifyUIBuilder::Theme AppId (__added__) +* AWS::AmplifyUIBuilder::Theme EnvironmentName (__added__) +* AWS::AppConfig::Deployment KmsKeyIdentifier (__added__) +* AWS::EC2::IPAM DefaultResourceDiscoveryAssociationId (__added__) +* AWS::EC2::IPAM DefaultResourceDiscoveryId (__added__) +* AWS::EC2::IPAM ResourceDiscoveryAssociationCount (__added__) +* AWS::EC2::IPAMPool PublicIpSource (__added__) +* AWS::NetworkManager::ConnectPeer ConnectAttachmentId.Required (__changed__) + * Old: false + * New: true +* AWS::NetworkManager::ConnectPeer InsideCidrBlocks.Required (__changed__) + * Old: false + * New: true +* AWS::NetworkManager::ConnectPeer PeerAddress.Required (__changed__) + * Old: false + * New: true +* AWS::NetworkManager::SiteToSiteVpnAttachment CoreNetworkId.Required (__changed__) + * Old: false + * New: true +* AWS::NetworkManager::SiteToSiteVpnAttachment VpnConnectionArn.Required (__changed__) + * Old: false + * New: true +* AWS::OpsWorksCM::Server ServerName (__deleted__) +* AWS::RDS::DBProxyEndpoint TargetRole (__deleted__) +* AWS::RDS::DBProxyTargetGroup DBProxyName.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::RolesAnywhere::CRL CrlData.Required (__changed__) + * Old: false + * New: true +* AWS::RolesAnywhere::CRL Name.Required (__changed__) + * Old: false + * New: true +* AWS::RolesAnywhere::Profile Name.Required (__changed__) + * Old: false + * New: true +* AWS::RolesAnywhere::Profile RoleArns.Required (__changed__) + * Old: false + * New: true +* AWS::RolesAnywhere::TrustAnchor Name.Required (__changed__) + * Old: false + * New: true +* AWS::RolesAnywhere::TrustAnchor Source.Required (__changed__) + * Old: false + * New: true +* AWS::SNS::Topic TracingConfig (__added__) + +## Property Type Changes + +* AWS::WAFv2::RuleGroup.Allow (__removed__) +* AWS::WAFv2::RuleGroup.Block (__removed__) +* AWS::WAFv2::RuleGroup.Captcha (__removed__) +* AWS::WAFv2::RuleGroup.Challenge (__removed__) +* AWS::WAFv2::RuleGroup.Count (__removed__) +* AWS::ConnectCampaigns::Campaign.AnswerMachineDetectionConfig (__added__) +* AWS::NimbleStudio::LaunchProfile.StreamConfigurationSessionBackup (__added__) +* AWS::WAFv2::RuleGroup.AllowAction (__added__) +* AWS::WAFv2::RuleGroup.BlockAction (__added__) +* AWS::WAFv2::RuleGroup.CaptchaAction (__added__) +* AWS::WAFv2::RuleGroup.ChallengeAction (__added__) +* AWS::WAFv2::RuleGroup.CountAction (__added__) +* AWS::AmplifyUIBuilder::Form.FieldInputConfig IsArray (__added__) +* AWS::AmplifyUIBuilder::Form.SectionalElement Excluded (__added__) +* AWS::ConnectCampaigns::Campaign.OutboundCallConfig AnswerMachineDetectionConfig (__added__) +* AWS::IoT::TopicRule.CloudwatchLogsAction BatchMode (__added__) +* AWS::NetworkFirewall::Firewall.SubnetMapping IPAddressType (__added__) +* AWS::NimbleStudio::LaunchProfile.StreamConfiguration SessionBackup (__added__) +* AWS::RDS::DBProxy.AuthFormat UserName (__deleted__) +* AWS::WAFv2::RuleGroup.RuleAction Allow.Type (__changed__) + * Old: Allow + * New: AllowAction +* AWS::WAFv2::RuleGroup.RuleAction Block.Type (__changed__) + * Old: Block + * New: BlockAction +* AWS::WAFv2::RuleGroup.RuleAction Captcha.Type (__changed__) + * Old: Captcha + * New: CaptchaAction +* AWS::WAFv2::RuleGroup.RuleAction Challenge.Type (__changed__) + * Old: Challenge + * New: ChallengeAction +* AWS::WAFv2::RuleGroup.RuleAction Count.Type (__changed__) + * Old: Count + * New: CountAction + + + + + +# Serverless Application Model (SAM) Resource Specification v2016-10-31 + +## New Resource Types + + +## Attribute Changes + + +## Property Changes + +* AWS::Serverless::Api Models.PrimitiveItemType (__changed__) + * Old: String + * New: Map + +## Property Type Changes + + # CloudFormation Resource Specification (us-west-2) v109.0.0 diff --git a/packages/@aws-cdk/cfnspec/spec-source/cfn-docs/cfn-docs.json b/packages/@aws-cdk/cfnspec/spec-source/cfn-docs/cfn-docs.json index 7ba8fc2df1f28..e1087ced3226e 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/cfn-docs/cfn-docs.json +++ b/packages/@aws-cdk/cfnspec/spec-source/cfn-docs/cfn-docs.json @@ -1931,7 +1931,7 @@ "properties": { "ApplicationId": "The application ID.", "Description": "A description of the configuration profile.", - "LocationUri": "A URI to locate the configuration. You can specify the AWS AppConfig hosted configuration store, Systems Manager (SSM) document, an SSM Parameter Store parameter, or an Amazon S3 object. For the hosted configuration store and for feature flags, specify `hosted` . For an SSM document, specify either the document name in the format `ssm-document://` or the Amazon Resource Name (ARN). For a parameter, specify either the parameter name in the format `ssm-parameter://` or the ARN. For an Amazon S3 object, specify the URI in the following format: `s3:///` . Here is an example: `s3://my-bucket/my-app/us-east-1/my-config.json`", + "LocationUri": "A URI to locate the configuration. You can specify the following:\n\n- For the AWS AppConfig hosted configuration store and for feature flags, specify `hosted` .\n- For an AWS Systems Manager Parameter Store parameter, specify either the parameter name in the format `ssm-parameter://` or the ARN.\n- For an AWS Secrets Manager secret, specify the URI in the following format: `secrets-manager` ://.\n- For an Amazon S3 object, specify the URI in the following format: `s3:///` . Here is an example: `s3://my-bucket/my-app/us-east-1/my-config.json`\n- For an SSM document, specify either the document name in the format `ssm-document://` or the Amazon Resource Name (ARN).", "Name": "A name for the configuration profile.", "RetrievalRoleArn": "The ARN of an IAM role with permission to access the configuration at the specified `LocationUri` .\n\n> A retrieval role ARN is not required for configurations stored in the AWS AppConfig hosted configuration store. It is required for all other sources that store your configuration.", "Tags": "Metadata to assign to the configuration profile. Tags help organize and categorize your AWS AppConfig resources. Each tag consists of a key and an optional value, both of which you define.", @@ -4841,14 +4841,14 @@ "Description": "The description of the data source.", "DynamoDBConfig": "AWS Region and TableName for an Amazon DynamoDB table in your account.", "ElasticsearchConfig": "AWS Region and Endpoints for an Amazon OpenSearch Service domain in your account.\n\nAs of September 2021, Amazon Elasticsearch Service is Amazon OpenSearch Service . This property is deprecated. For new data sources, use *OpenSearchServiceConfig* to specify an OpenSearch Service data source.", - "EventBridgeConfig": "", + "EventBridgeConfig": "An EventBridge configuration that contains a valid ARN of an event bus.", "HttpConfig": "Endpoints for an HTTP data source.", "LambdaConfig": "An ARN of a Lambda function in valid ARN format. This can be the ARN of a Lambda function that exists in the current account or in another account.", "Name": "Friendly name for you to identify your AppSync data source after creation.", "OpenSearchServiceConfig": "AWS Region and Endpoints for an Amazon OpenSearch Service domain in your account.", "RelationalDatabaseConfig": "Relational Database configuration of the relational database data source.", - "ServiceRoleArn": "The AWS Identity and Access Management service role ARN for the data source. The system assumes this role when accessing the data source.\n\nRequired if `Type` is specified as `AWS_LAMBDA` , `AMAZON_DYNAMODB` , `AMAZON_ELASTICSEARCH` , or `AMAZON_OPENSEARCH_SERVICE` .", - "Type": "The type of the data source.\n\n- *AWS_LAMBDA* : The data source is an AWS Lambda function.\n- *AMAZON_DYNAMODB* : The data source is an Amazon DynamoDB table.\n- *AMAZON_ELASTICSEARCH* : The data source is an Amazon OpenSearch Service domain.\n- *AMAZON_OPENSEARCH_SERVICE* : The data source is an Amazon OpenSearch Service domain.\n- *NONE* : There is no data source. This type is used when you wish to invoke a GraphQL operation without connecting to a data source, such as performing data transformation with resolvers or triggering a subscription to be invoked from a mutation.\n- *HTTP* : The data source is an HTTP endpoint.\n- *RELATIONAL_DATABASE* : The data source is a relational database." + "ServiceRoleArn": "The AWS Identity and Access Management service role ARN for the data source. The system assumes this role when accessing the data source.\n\nRequired if `Type` is specified as `AWS_LAMBDA` , `AMAZON_DYNAMODB` , `AMAZON_ELASTICSEARCH` , `AMAZON_EVENTBRIDGE` , or `AMAZON_OPENSEARCH_SERVICE` .", + "Type": "The type of the data source.\n\n- *AWS_LAMBDA* : The data source is an AWS Lambda function.\n- *AMAZON_DYNAMODB* : The data source is an Amazon DynamoDB table.\n- *AMAZON_ELASTICSEARCH* : The data source is an Amazon OpenSearch Service domain.\n- *AMAZON_EVENTBRIDGE* : The data source is an Amazon EventBridge event bus.\n- *AMAZON_OPENSEARCH_SERVICE* : The data source is an Amazon OpenSearch Service domain.\n- *NONE* : There is no data source. This type is used when you wish to invoke a GraphQL operation without connecting to a data source, such as performing data transformation with resolvers or triggering a subscription to be invoked from a mutation.\n- *HTTP* : The data source is an HTTP endpoint.\n- *RELATIONAL_DATABASE* : The data source is a relational database." } }, "AWS::AppSync::DataSource.AuthorizationConfig": { @@ -4897,9 +4897,9 @@ }, "AWS::AppSync::DataSource.EventBridgeConfig": { "attributes": {}, - "description": "", + "description": "The data source. This can be an API destination, resource, or AWS service.", "properties": { - "EventBusArn": "" + "EventBusArn": "The event bus pipeline's ARN. For more information about event buses, see [EventBridge event buses](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-bus.html) ." } }, "AWS::AppSync::DataSource.HttpConfig": { @@ -5591,7 +5591,7 @@ "AssessmentName": "The name of the assessment that's associated with the delegation.", "Comment": "The comment that's related to the delegation.", "ControlSetId": "The identifier for the control set that's associated with the delegation.", - "CreatedBy": "The IAM user or role that created the delegation.\n\n*Minimum* : `1`\n\n*Maximum* : `100`\n\n*Pattern* : `^[a-zA-Z0-9-_()\\\\[\\\\]\\\\s]+$`", + "CreatedBy": "The user or role that created the delegation.\n\n*Minimum* : `1`\n\n*Maximum* : `100`\n\n*Pattern* : `^[a-zA-Z0-9-_()\\\\[\\\\]\\\\s]+$`", "CreationTime": "Specifies when the delegation was created.", "Id": "The unique identifier for the delegation.", "LastUpdated": "Specifies when the delegation was last updated.", @@ -8558,6 +8558,37 @@ "Enabled": "This field is `true` if any of the AWS accounts have public keys that CloudFront can use to verify the signatures of signed URLs and signed cookies. If not, this field is `false` ." } }, + "AWS::CloudTrail::Channel": { + "attributes": { + "ChannelArn": "`Ref` returns the ARN of the CloudTrail channel, such as `arn:aws:cloudtrail:us-east-2:123456789012:channel/01234567890` .", + "Channels": "`Ref` returns the names of CloudTrail channels.", + "Ref": "When the logical ID of this resource is provided to the Ref intrinsic function, `Ref` returns the resource name." + }, + "description": "Contains information about a returned CloudTrail channel.", + "properties": { + "Channel": "Contains information about a returned CloudTrail channel.", + "Destinations": "One or more event data stores to which events arriving through a channel will be logged.", + "Name": "The name of the channel.", + "Source": "The name of the partner or external event source. You cannot change this name after you create the channel. A maximum of one channel is allowed per source.\n\nA source can be either `Custom` for all valid non- AWS events, or the name of a partner event source. For information about the source names for available partners, see [Additional information about integration partners](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/query-event-data-store-integration.html#cloudtrail-lake-partner-information) in the CloudTrail User Guide.", + "Tags": "A list of tags." + } + }, + "AWS::CloudTrail::Channel.Channel": { + "attributes": {}, + "description": "Contains information about a returned CloudTrail channel.", + "properties": { + "ChannelArn": "The Amazon Resource Name (ARN) of a channel.", + "Name": "The name of the CloudTrail channel. For service-linked channels, the name is `aws-service-channel/service-name/custom-suffix` where `service-name` represents the name of the AWS service that created the channel and `custom-suffix` represents the suffix created by the AWS service." + } + }, + "AWS::CloudTrail::Channel.Destination": { + "attributes": {}, + "description": "Contains information about the destination receiving events.", + "properties": { + "Location": "For channels used for a CloudTrail Lake integration, the location is the ARN of an event data store that receives events from a channel. For service-linked channels, the location is the name of the AWS service.", + "Type": "The type of destination for events arriving from a channel. For channels used for a CloudTrail Lake integration, the value is `EventDataStore` . For service-linked channels, the value is `AWS_SERVICE` ." + } + }, "AWS::CloudTrail::EventDataStore": { "attributes": { "CreatedTimestamp": "`Ref` returns the time stamp of the creation of the event data store, such as `1248496624` .", @@ -8568,7 +8599,7 @@ }, "description": "Creates a new event data store.", "properties": { - "AdvancedEventSelectors": "The advanced event selectors to use to select the events for the data store. You can configure up to five advanced event selectors for each event data store.\n\nFor more information about how to use advanced event selectors to log CloudTrail events, see [Log events by using advanced event selectors](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/logging-data-events-with-cloudtrail.html#creating-data-event-selectors-advanced) in the CloudTrail User Guide.\n\nFor more information about how to use advanced event selectors to include AWS Config configuration items in your event data store, see [Create an event data store for AWS Config configuration items](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/lake-cli-create-eds-config.html) in the CloudTrail User Guide.", + "AdvancedEventSelectors": "The advanced event selectors to use to select the events for the data store. You can configure up to five advanced event selectors for each event data store.\n\nFor more information about how to use advanced event selectors to log CloudTrail events, see [Log events by using advanced event selectors](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/logging-data-events-with-cloudtrail.html#creating-data-event-selectors-advanced) in the CloudTrail User Guide.\n\nFor more information about how to use advanced event selectors to include AWS Config configuration items in your event data store, see [Create an event data store for AWS Config configuration items](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/query-lake-cli.html#lake-cli-create-eds-config) in the CloudTrail User Guide.\n\nFor more information about how to use advanced event selectors to include non- AWS events in your event data store, see [Create an integration to log events from outside AWS](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/query-lake-cli.html#lake-cli-create-integration) in the CloudTrail User Guide.", "KmsKeyId": "Specifies the AWS KMS key ID to use to encrypt the events delivered by CloudTrail. The value can be an alias name prefixed by `alias/` , a fully specified ARN to an alias, a fully specified ARN to a key, or a globally unique identifier.\n\n> Disabling or deleting the KMS key, or removing CloudTrail permissions on the key, prevents CloudTrail from logging events to the event data store, and prevents users from querying the data in the event data store that was encrypted with the key. After you associate an event data store with a KMS key, the KMS key cannot be removed or changed. Before you disable or delete a KMS key that you are using with an event data store, delete or back up your event data store. \n\nCloudTrail also supports AWS KMS multi-Region keys. For more information about multi-Region keys, see [Using multi-Region keys](https://docs.aws.amazon.com/kms/latest/developerguide/multi-region-keys-overview.html) in the *AWS Key Management Service Developer Guide* .\n\nExamples:\n\n- `alias/MyAliasName`\n- `arn:aws:kms:us-east-2:123456789012:alias/MyAliasName`\n- `arn:aws:kms:us-east-2:123456789012:key/12345678-1234-1234-1234-123456789012`\n- `12345678-1234-1234-1234-123456789012`", "MultiRegionEnabled": "Specifies whether the event data store includes events from all regions, or only from the region in which the event data store is created.", "Name": "The name of the event data store.", @@ -8592,13 +8623,23 @@ "properties": { "EndsWith": "An operator that includes events that match the last few characters of the event record field specified as the value of `Field` .", "Equals": "An operator that includes events that match the exact value of the event record field specified as the value of `Field` . This is the only valid operator that you can use with the `readOnly` , `eventCategory` , and `resources.type` fields.", - "Field": "A field in a CloudTrail event record on which to filter events to be logged. For event data stores for AWS Config configuration items, the field is used only for selecting configuration items as filtering is not supported.\n\nFor CloudTrail event records, supported fields include `readOnly` , `eventCategory` , `eventSource` (for management events), `eventName` , `resources.type` , and `resources.ARN` .\n\nFor AWS Config configuration item records, the only supported field is `eventCategory` .\n\n- *`readOnly`* - Optional. Can be set to `Equals` a value of `true` or `false` . If you do not add this field, CloudTrail logs both `read` and `write` events. A value of `true` logs only `read` events. A value of `false` logs only `write` events.\n- *`eventSource`* - For filtering management events only. This can be set only to `NotEquals` `kms.amazonaws.com` .\n- *`eventName`* - Can use any operator. You can use it to \ufb01lter in or \ufb01lter out any data event logged to CloudTrail, such as `PutBucket` or `GetSnapshotBlock` . You can have multiple values for this \ufb01eld, separated by commas.\n- *`eventCategory`* - This is required and must be set to `Equals` . For CloudTrail event records, the value must be `Management` or `Data` . For AWS Config configuration item records, the value must be `ConfigurationItem` .\n- *`resources.type`* - This \ufb01eld is required for CloudTrail data events. `resources.type` can only use the `Equals` operator, and the value can be one of the following:\n\n- `AWS::S3::Object`\n- `AWS::Lambda::Function`\n- `AWS::DynamoDB::Table`\n- `AWS::S3Outposts::Object`\n- `AWS::ManagedBlockchain::Node`\n- `AWS::S3ObjectLambda::AccessPoint`\n- `AWS::EC2::Snapshot`\n- `AWS::S3::AccessPoint`\n- `AWS::DynamoDB::Stream`\n- `AWS::Glue::Table`\n- `AWS::FinSpace::Environment`\n- `AWS::SageMaker::ExperimentTrialComponent`\n- `AWS::SageMaker::FeatureGroup`\n\nYou can have only one `resources.type` \ufb01eld per selector. To log data events on more than one resource type, add another selector.\n- *`resources.ARN`* - You can use any operator with `resources.ARN` , but if you use `Equals` or `NotEquals` , the value must exactly match the ARN of a valid resource of the type you've speci\ufb01ed in the template as the value of resources.type. For example, if resources.type equals `AWS::S3::Object` , the ARN must be in one of the following formats. To log all data events for all objects in a specific S3 bucket, use the `StartsWith` operator, and include only the bucket ARN as the matching value.\n\nThe trailing slash is intentional; do not exclude it. Replace the text between less than and greater than symbols (<>) with resource-specific information.\n\n- `arn::s3:::/`\n- `arn::s3::://`\n\nWhen `resources.type` equals `AWS::S3::AccessPoint` , and the operator is set to `Equals` or `NotEquals` , the ARN must be in one of the following formats. To log events on all objects in an S3 access point, we recommend that you use only the access point ARN, don\u2019t include the object path, and use the `StartsWith` or `NotStartsWith` operators.\n\n- `arn::s3:::accesspoint/`\n- `arn::s3:::accesspoint//object/`\n\nWhen resources.type equals `AWS::Lambda::Function` , and the operator is set to `Equals` or `NotEquals` , the ARN must be in the following format:\n\n- `arn::lambda:::function:`\n\nWhen resources.type equals `AWS::DynamoDB::Table` , and the operator is set to `Equals` or `NotEquals` , the ARN must be in the following format:\n\n- `arn::dynamodb:::table/`\n\nWhen `resources.type` equals `AWS::S3Outposts::Object` , and the operator is set to `Equals` or `NotEquals` , the ARN must be in the following format:\n\n- `arn::s3-outposts:::`\n\nWhen `resources.type` equals `AWS::ManagedBlockchain::Node` , and the operator is set to `Equals` or `NotEquals` , the ARN must be in the following format:\n\n- `arn::managedblockchain:::nodes/`\n\nWhen `resources.type` equals `AWS::S3ObjectLambda::AccessPoint` , and the operator is set to `Equals` or `NotEquals` , the ARN must be in the following format:\n\n- `arn::s3-object-lambda:::accesspoint/`\n\nWhen `resources.type` equals `AWS::EC2::Snapshot` , and the operator is set to `Equals` or `NotEquals` , the ARN must be in the following format:\n\n- `arn::ec2:::snapshot/`\n\nWhen `resources.type` equals `AWS::DynamoDB::Stream` , and the operator is set to `Equals` or `NotEquals` , the ARN must be in the following format:\n\n- `arn::dynamodb:::table//stream/`\n\nWhen `resources.type` equals `AWS::Glue::Table` , and the operator is set to `Equals` or `NotEquals` , the ARN must be in the following format:\n\n- `arn::glue:::table//`\n\nWhen `resources.type` equals `AWS::FinSpace::Environment` , and the operator is set to `Equals` or `NotEquals` , the ARN must be in the following format:\n\n- `arn::finspace:::environment/`\n\nWhen `resources.type` equals `AWS::SageMaker::ExperimentTrialComponent` , and the operator is set to `Equals` or `NotEquals` , the ARN must be in the following format:\n\n- `arn::sagemaker:::experiment-trial-component/`\n\nWhen `resources.type` equals `AWS::SageMaker::FeatureGroup` , and the operator is set to `Equals` or `NotEquals` , the ARN must be in the following format:\n\n- `arn::sagemaker:::feature-group/`", + "Field": "A field in a CloudTrail event record on which to filter events to be logged. For event data stores for AWS Config configuration items, Audit Manager evidence, or non- AWS events, the field is used only for selecting events as filtering is not supported.\n\nFor CloudTrail event records, supported fields include `readOnly` , `eventCategory` , `eventSource` (for management events), `eventName` , `resources.type` , and `resources.ARN` .\n\nFor event data stores for AWS Config configuration items, Audit Manager evidence, or non- AWS events, the only supported field is `eventCategory` .\n\n- *`readOnly`* - Optional. Can be set to `Equals` a value of `true` or `false` . If you do not add this field, CloudTrail logs both `read` and `write` events. A value of `true` logs only `read` events. A value of `false` logs only `write` events.\n- *`eventSource`* - For filtering management events only. This can be set only to `NotEquals` `kms.amazonaws.com` .\n- *`eventName`* - Can use any operator. You can use it to \ufb01lter in or \ufb01lter out any data event logged to CloudTrail, such as `PutBucket` or `GetSnapshotBlock` . You can have multiple values for this \ufb01eld, separated by commas.\n- *`eventCategory`* - This is required and must be set to `Equals` .\n\n- For CloudTrail event records, the value must be `Management` or `Data` .\n- For AWS Config configuration items, the value must be `ConfigurationItem` .\n- For Audit Manager evidence, the value must be `Evidence` .\n- For non- AWS events, the value must be `ActivityAuditLog` .\n- *`resources.type`* - This \ufb01eld is required for CloudTrail data events. `resources.type` can only use the `Equals` operator, and the value can be one of the following:\n\n- `AWS::CloudTrail::Channel`\n- `AWS::S3::Object`\n- `AWS::Lambda::Function`\n- `AWS::DynamoDB::Table`\n- `AWS::S3Outposts::Object`\n- `AWS::ManagedBlockchain::Node`\n- `AWS::S3ObjectLambda::AccessPoint`\n- `AWS::EC2::Snapshot`\n- `AWS::S3::AccessPoint`\n- `AWS::DynamoDB::Stream`\n- `AWS::Glue::Table`\n- `AWS::FinSpace::Environment`\n- `AWS::SageMaker::ExperimentTrialComponent`\n- `AWS::SageMaker::FeatureGroup`\n\nYou can have only one `resources.type` \ufb01eld per selector. To log data events on more than one resource type, add another selector.\n- *`resources.ARN`* - You can use any operator with `resources.ARN` , but if you use `Equals` or `NotEquals` , the value must exactly match the ARN of a valid resource of the type you've speci\ufb01ed in the template as the value of resources.type. For example, if resources.type equals `AWS::S3::Object` , the ARN must be in one of the following formats. To log all data events for all objects in a specific S3 bucket, use the `StartsWith` operator, and include only the bucket ARN as the matching value.\n\nThe trailing slash is intentional; do not exclude it. Replace the text between less than and greater than symbols (<>) with resource-specific information.\n\n- `arn::s3:::/`\n- `arn::s3::://`\n\nWhen `resources.type` equals `AWS::S3::AccessPoint` , and the operator is set to `Equals` or `NotEquals` , the ARN must be in one of the following formats. To log events on all objects in an S3 access point, we recommend that you use only the access point ARN, don\u2019t include the object path, and use the `StartsWith` or `NotStartsWith` operators.\n\n- `arn::s3:::accesspoint/`\n- `arn::s3:::accesspoint//object/`\n\nWhen resources.type equals `AWS::Lambda::Function` , and the operator is set to `Equals` or `NotEquals` , the ARN must be in the following format:\n\n- `arn::lambda:::function:`\n\nWhen resources.type equals `AWS::DynamoDB::Table` , and the operator is set to `Equals` or `NotEquals` , the ARN must be in the following format:\n\n- `arn::dynamodb:::table/`\n\nWhen resources.type equals `AWS::CloudTrail::Channel` , and the operator is set to `Equals` or `NotEquals` , the ARN must be in the following format:\n\n- `arn::cloudtrail:::channel/`\n\nWhen `resources.type` equals `AWS::S3Outposts::Object` , and the operator is set to `Equals` or `NotEquals` , the ARN must be in the following format:\n\n- `arn::s3-outposts:::`\n\nWhen `resources.type` equals `AWS::ManagedBlockchain::Node` , and the operator is set to `Equals` or `NotEquals` , the ARN must be in the following format:\n\n- `arn::managedblockchain:::nodes/`\n\nWhen `resources.type` equals `AWS::S3ObjectLambda::AccessPoint` , and the operator is set to `Equals` or `NotEquals` , the ARN must be in the following format:\n\n- `arn::s3-object-lambda:::accesspoint/`\n\nWhen `resources.type` equals `AWS::EC2::Snapshot` , and the operator is set to `Equals` or `NotEquals` , the ARN must be in the following format:\n\n- `arn::ec2:::snapshot/`\n\nWhen `resources.type` equals `AWS::DynamoDB::Stream` , and the operator is set to `Equals` or `NotEquals` , the ARN must be in the following format:\n\n- `arn::dynamodb:::table//stream/`\n\nWhen `resources.type` equals `AWS::Glue::Table` , and the operator is set to `Equals` or `NotEquals` , the ARN must be in the following format:\n\n- `arn::glue:::table//`\n\nWhen `resources.type` equals `AWS::FinSpace::Environment` , and the operator is set to `Equals` or `NotEquals` , the ARN must be in the following format:\n\n- `arn::finspace:::environment/`\n\nWhen `resources.type` equals `AWS::SageMaker::ExperimentTrialComponent` , and the operator is set to `Equals` or `NotEquals` , the ARN must be in the following format:\n\n- `arn::sagemaker:::experiment-trial-component/`\n\nWhen `resources.type` equals `AWS::SageMaker::FeatureGroup` , and the operator is set to `Equals` or `NotEquals` , the ARN must be in the following format:\n\n- `arn::sagemaker:::feature-group/`", "NotEndsWith": "An operator that excludes events that match the last few characters of the event record field specified as the value of `Field` .", "NotEquals": "An operator that excludes events that match the exact value of the event record field specified as the value of `Field` .", "NotStartsWith": "An operator that excludes events that match the first few characters of the event record field specified as the value of `Field` .", "StartsWith": "An operator that includes events that match the first few characters of the event record field specified as the value of `Field` ." } }, + "AWS::CloudTrail::ResourcePolicy": { + "attributes": { + "Ref": "When the logical ID of this resource is provided to the Ref intrinsic function, `Ref` returns the resource. The resource is a combination of the resource-based policy document and the channel ARN." + }, + "description": "Attaches a resource-based permission policy to a CloudTrail channel that is used for an integration with an event source outside of AWS . For more information about resource-based policies, see [CloudTrail resource-based policy examples](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/security_iam_resource-based-policy-examples.html) in the *CloudTrail User Guide* .", + "properties": { + "ResourceArn": "The Amazon Resource Name (ARN) of the CloudTrail channel attached to the resource-based policy. The following is the format of a resource ARN: `arn:aws:cloudtrail:us-east-2:123456789012:channel/MyChannel` .", + "ResourcePolicy": "A JSON-formatted string for an AWS resource-based policy.\n\nThe following are requirements for the resource policy:\n\n- Contains only one action: cloudtrail-data:PutAuditEvents\n- Contains at least one statement. The policy can have a maximum of 20 statements.\n- Each statement contains at least one principal. A statement can have a maximum of 50 principals." + } + }, "AWS::CloudTrail::Trail": { "attributes": { "Arn": "`Ref` returns the ARN of the CloudTrail trail, such as `arn:aws:cloudtrail:us-east-2:123456789012:trail/myCloudTrail` .", @@ -10706,10 +10747,10 @@ "attributes": { "Ref": "" }, - "description": "", + "description": "The approved origin for the instance.", "properties": { - "InstanceId": "", - "Origin": "" + "InstanceId": "The Amazon Resource Name (ARN) of the instance.\n\n*Minimum* : `1`\n\n*Maximum* : `100`", + "Origin": "Domain name to be added to the allow-list of the instance.\n\n*Maximum* : `267`" } }, "AWS::Connect::ContactFlow": { @@ -10864,14 +10905,14 @@ }, "AWS::Connect::IntegrationAssociation": { "attributes": { - "IntegrationAssociationId": "", + "IntegrationAssociationId": "Identifier of the association with an Amazon Connect instance.", "Ref": "" }, - "description": "Creates an AWS resource association with an Amazon Connect instance.", + "description": "Specifies the association of an AWS resource such as Lex bot (both v1 and v2) and Lambda function with an Amazon Connect instance.", "properties": { - "InstanceId": "The identifier of the Amazon Connect instance. You can find the instanceId in the ARN of the instance.", - "IntegrationArn": "The Amazon Resource Name (ARN) for the AppIntegration.", - "IntegrationType": "The integration type." + "InstanceId": "The Amazon Resource Name (ARN) of the instance.\n\n*Minimum* : `1`\n\n*Maximum* : `100`", + "IntegrationArn": "ARN of the integration being associated with the instance.\n\n*Minimum* : `1`\n\n*Maximum* : `140`", + "IntegrationType": "Specifies the integration type to be associated with the instance.\n\n*Allowed Values* : `LEX_BOT` | `LAMBDA_FUNCTION`" } }, "AWS::Connect::PhoneNumber": { @@ -11017,13 +11058,13 @@ }, "AWS::Connect::SecurityKey": { "attributes": { - "AssociationId": "", + "AssociationId": "An `AssociationId` is automatically generated when a storage config is associated with an instance.", "Ref": "" }, - "description": "Configuration information of the security key.", + "description": "The security key for the instance.\n\n> Only two security keys are allowed per Amazon Connect instance.", "properties": { - "InstanceId": "", - "Key": "The key of the security key." + "InstanceId": "The Amazon Resource Name (ARN) of the instance.\n\n*Minimum* : `1`\n\n*Maximum* : `100`", + "Key": "A valid security key in PEM format.\n\n*Minimum* : `1`\n\n*Maximum* : `1024`" } }, "AWS::Connect::TaskTemplate": { @@ -13229,12 +13270,12 @@ "description": "Information about a filter used to specify which AWS resources are analyzed for anomalous behavior by DevOps Guru.", "properties": { "CloudFormation": "Information about AWS CloudFormation stacks. You can use up to 500 stacks to specify which AWS resources in your account to analyze. For more information, see [Stacks](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacks.html) in the *AWS CloudFormation User Guide* .", - "Tags": "The AWS tags used to filter the resources in the resource collection.\n\nTags help you identify and organize your AWS resources. Many AWS services support tagging, so you can assign the same tag to resources from different services to indicate that the resources are related. For example, you can assign the same tag to an Amazon DynamoDB table resource that you assign to an AWS Lambda function. For more information about using tags, see the [Tagging best practices](https://docs.aws.amazon.com/https://d1.awsstatic.com/whitepapers/aws-tagging-best-practices.pdf) whitepaper.\n\nEach AWS tag has two parts.\n\n- A tag *key* (for example, `CostCenter` , `Environment` , `Project` , or `Secret` ). Tag *keys* are case-sensitive.\n- An optional field known as a tag *value* (for example, `111122223333` , `Production` , or a team name). Omitting the tag *value* is the same as using an empty string. Like tag *keys* , tag *values* are case-sensitive.\n\nTogether these are known as *key* - *value* pairs.\n\n> The string used for a *key* in a tag that you use to define your resource coverage must begin with the prefix `Devops-guru-` . The tag *key* might be `DevOps-Guru-deployment-application` or `devops-guru-rds-application` . When you create a *key* , the case of characters in the *key* can be whatever you choose. After you create a *key* , it is case-sensitive. For example, DevOps Guru works with a *key* named `devops-guru-rds` and a *key* named `DevOps-Guru-RDS` , and these act as two different *keys* . Possible *key* / *value* pairs in your application might be `Devops-Guru-production-application/RDS` or `Devops-Guru-production-application/containers` ." + "Tags": "The AWS tags used to filter the resources in the resource collection.\n\nTags help you identify and organize your AWS resources. Many AWS services support tagging, so you can assign the same tag to resources from different services to indicate that the resources are related. For example, you can assign the same tag to an Amazon DynamoDB table resource that you assign to an AWS Lambda function. For more information about using tags, see the [Tagging best practices](https://docs.aws.amazon.com/https://docs.aws.amazon.com/whitepapers/latest/tagging-best-practices/tagging-best-practices.html) whitepaper.\n\nEach AWS tag has two parts.\n\n- A tag *key* (for example, `CostCenter` , `Environment` , `Project` , or `Secret` ). Tag *keys* are case-sensitive.\n- An optional field known as a tag *value* (for example, `111122223333` , `Production` , or a team name). Omitting the tag *value* is the same as using an empty string. Like tag *keys* , tag *values* are case-sensitive.\n\nTogether these are known as *key* - *value* pairs.\n\n> The string used for a *key* in a tag that you use to define your resource coverage must begin with the prefix `Devops-guru-` . The tag *key* might be `DevOps-Guru-deployment-application` or `devops-guru-rds-application` . When you create a *key* , the case of characters in the *key* can be whatever you choose. After you create a *key* , it is case-sensitive. For example, DevOps Guru works with a *key* named `devops-guru-rds` and a *key* named `DevOps-Guru-RDS` , and these act as two different *keys* . Possible *key* / *value* pairs in your application might be `Devops-Guru-production-application/RDS` or `Devops-Guru-production-application/containers` ." } }, "AWS::DevOpsGuru::ResourceCollection.TagCollection": { "attributes": {}, - "description": "A collection of AWS tags.\n\nTags help you identify and organize your AWS resources. Many AWS services support tagging, so you can assign the same tag to resources from different services to indicate that the resources are related. For example, you can assign the same tag to an Amazon DynamoDB table resource that you assign to an AWS Lambda function. For more information about using tags, see the [Tagging best practices](https://docs.aws.amazon.com/https://d1.awsstatic.com/whitepapers/aws-tagging-best-practices.pdf) whitepaper.\n\nEach AWS tag has two parts.\n\n- A tag *key* (for example, `CostCenter` , `Environment` , `Project` , or `Secret` ). Tag *keys* are case-sensitive.\n- An optional field known as a tag *value* (for example, `111122223333` , `Production` , or a team name). Omitting the tag *value* is the same as using an empty string. Like tag *keys* , tag *values* are case-sensitive.\n\nTogether these are known as *key* - *value* pairs.\n\n> The string used for a *key* in a tag that you use to define your resource coverage must begin with the prefix `Devops-guru-` . The tag *key* might be `DevOps-Guru-deployment-application` or `devops-guru-rds-application` . When you create a *key* , the case of characters in the *key* can be whatever you choose. After you create a *key* , it is case-sensitive. For example, DevOps Guru works with a *key* named `devops-guru-rds` and a *key* named `DevOps-Guru-RDS` , and these act as two different *keys* . Possible *key* / *value* pairs in your application might be `Devops-Guru-production-application/RDS` or `Devops-Guru-production-application/containers` .", + "description": "A collection of AWS tags.\n\nTags help you identify and organize your AWS resources. Many AWS services support tagging, so you can assign the same tag to resources from different services to indicate that the resources are related. For example, you can assign the same tag to an Amazon DynamoDB table resource that you assign to an AWS Lambda function. For more information about using tags, see the [Tagging best practices](https://docs.aws.amazon.com/https://docs.aws.amazon.com/whitepapers/latest/tagging-best-practices/tagging-best-practices.html) whitepaper.\n\nEach AWS tag has two parts.\n\n- A tag *key* (for example, `CostCenter` , `Environment` , `Project` , or `Secret` ). Tag *keys* are case-sensitive.\n- An optional field known as a tag *value* (for example, `111122223333` , `Production` , or a team name). Omitting the tag *value* is the same as using an empty string. Like tag *keys* , tag *values* are case-sensitive.\n\nTogether these are known as *key* - *value* pairs.\n\n> The string used for a *key* in a tag that you use to define your resource coverage must begin with the prefix `Devops-guru-` . The tag *key* might be `DevOps-Guru-deployment-application` or `devops-guru-rds-application` . When you create a *key* , the case of characters in the *key* can be whatever you choose. After you create a *key* , it is case-sensitive. For example, DevOps Guru works with a *key* named `devops-guru-rds` and a *key* named `DevOps-Guru-RDS` , and these act as two different *keys* . Possible *key* / *value* pairs in your application might be `Devops-Guru-production-application/RDS` or `Devops-Guru-production-application/containers` .", "properties": { "AppBoundaryKey": "An AWS tag *key* that is used to identify the AWS resources that DevOps Guru analyzes. All AWS resources in your account and Region tagged with this *key* make up your DevOps Guru application and analysis boundary.\n\n> The string used for a *key* in a tag that you use to define your resource coverage must begin with the prefix `Devops-guru-` . The tag *key* might be `DevOps-Guru-deployment-application` or `devops-guru-rds-application` . When you create a *key* , the case of characters in the *key* can be whatever you choose. After you create a *key* , it is case-sensitive. For example, DevOps Guru works with a *key* named `devops-guru-rds` and a *key* named `DevOps-Guru-RDS` , and these act as two different *keys* . Possible *key* / *value* pairs in your application might be `Devops-Guru-production-application/RDS` or `Devops-Guru-production-application/containers` .", "TagValues": "The values in an AWS tag collection.\n\nThe tag's *value* is an optional field used to associate a string with the tag *key* (for example, `111122223333` , `Production` , or a team name). The *key* and *value* are the tag's *key* pair. Omitting the tag *value* is the same as using an empty string. Like tag *keys* , tag *values* are case-sensitive. You can specify a maximum of 256 characters for a tag value." @@ -13890,7 +13931,7 @@ }, "AWS::EC2::CapacityReservationFleet.TagSpecification": { "attributes": {}, - "description": "The tags to apply to a resource when the resource is being created.\n\n> The `Valid Values` lists all the resource types that can be tagged. However, the action you're using might not support tagging all of these resource types. If you try to tag a resource type that is unsupported for the action you're using, you'll get an error.", + "description": "The tags to apply to a resource when the resource is being created. When you specify a tag, you must specify the resource type to tag, otherwise the request will fail.\n\n> The `Valid Values` lists all the resource types that can be tagged. However, the action you're using might not support tagging all of these resource types. If you try to tag a resource type that is unsupported for the action you're using, you'll get an error.", "properties": { "ResourceType": "The type of resource to tag on creation. Specify `capacity-reservation-fleet` .\n\nTo tag a resource after it has been created, see [CreateTags](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CreateTags.html) .", "Tags": "The tags to apply to the resource." @@ -14003,7 +14044,7 @@ }, "AWS::EC2::ClientVpnEndpoint.TagSpecification": { "attributes": {}, - "description": "The tags to apply to a resource when the resource is being created.\n\n> The `Valid Values` lists all the resource types that can be tagged. However, the action you're using might not support tagging all of these resource types. If you try to tag a resource type that is unsupported for the action you're using, you'll get an error.", + "description": "The tags to apply to a resource when the resource is being created. When you specify a tag, you must specify the resource type to tag, otherwise the request will fail.\n\n> The `Valid Values` lists all the resource types that can be tagged. However, the action you're using might not support tagging all of these resource types. If you try to tag a resource type that is unsupported for the action you're using, you'll get an error.", "properties": { "ResourceType": "The type of resource to tag.", "Tags": "The tags to apply to the resource." @@ -14305,7 +14346,7 @@ "NetworkBorderGroup": "A unique set of Availability Zones, Local Zones, or Wavelength Zones from which AWS advertises IP addresses. Use this parameter to limit the IP address to this location. IP addresses cannot move between network border groups.\n\nUse [DescribeAvailabilityZones](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeAvailabilityZones.html) to view the network border groups.\n\nYou cannot use a network border group with EC2 Classic. If you attempt this operation on EC2 Classic, you receive an `InvalidParameterCombination` error.", "PublicIpv4Pool": "The ID of an address pool that you own. Use this parameter to let Amazon EC2 select an address from the address pool.\n\n> Updates to the `PublicIpv4Pool` property may require *some interruptions* . Updates on an EIP reassociates the address on its associated resource.", "Tags": "Any tags assigned to the Elastic IP address.\n\n> Updates to the `Tags` property may require *some interruptions* . Updates on an EIP reassociates the address on its associated resource.", - "TransferAddress": "The Elastic IP address you are accepting for transfer." + "TransferAddress": "The Elastic IP address you are accepting for transfer. You can only accept one transferred address. For more information on Elastic IP address transfers, see [Transfer Elastic IP addresses](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-eips.html#transfer-EIPs-intro) in the *Amazon Virtual Private Cloud User Guide* ." } }, "AWS::EC2::EIPAssociation": { @@ -14460,6 +14501,7 @@ "IpamScopeId": "The ID of the scope in which you would like to create the IPAM pool.", "Locale": "The locale of the IPAM pool. In IPAM, the locale is the AWS Region where you want to make an IPAM pool available for allocations. Only resources in the same Region as the locale of the pool can get IP address allocations from the pool. You can only allocate a CIDR for a VPC, for example, from an IPAM pool that shares a locale with the VPC\u2019s Region. Note that once you choose a Locale for a pool, you cannot modify it. If you choose an AWS Region for locale that has not been configured as an operating Region for the IPAM, you'll get an error.", "ProvisionedCidrs": "Information about the CIDRs provisioned to an IPAM pool.", + "PublicIpSource": "The IP address source for pools in the public scope. Only used for provisioning IP address CIDRs to pools in the public scope. Default is `BYOIP` . For more information, see [Create IPv6 pools](https://docs.aws.amazon.com//vpc/latest/ipam/intro-create-ipv6-pools.html) in the *Amazon VPC IPAM User Guide* . By default, you can add only one Amazon-provided IPv6 CIDR block to a top-level IPv6 pool. For information on increasing the default limit, see [Quotas for your IPAM](https://docs.aws.amazon.com//vpc/latest/ipam/quotas-ipam.html) in the *Amazon VPC IPAM User Guide* .", "PubliclyAdvertisable": "Determines if a pool is publicly advertisable. This option is not available for pools with AddressFamily set to `ipv4` .", "SourceIpamPoolId": "The ID of the source IPAM pool. You can use this option to create an IPAM pool within an existing source pool.", "Tags": "The key/value combination of a tag assigned to the resource. Use the tag key in the filter name and the tag value as the filter value. For example, to find all resources that have a tag with the key `Owner` and the value `TeamA` , specify `tag:Owner` for the filter name and `TeamA` for the filter value." @@ -14467,11 +14509,65 @@ }, "AWS::EC2::IPAMPool.ProvisionedCidr": { "attributes": {}, - "description": "The CIDR provisioned to the IPAM pool. A CIDR is a representation of an IP address and its associated network mask (or netmask) and refers to a range of IP addresses. An IPv4 CIDR example is `10.24.34.0/23` . An IPv6 CIDR example is `2001:DB8::/32` .", + "description": "The CIDR provisioned to the IPAM pool. A CIDR is a representation of an IP address and its associated network mask (or netmask) and refers to a range of IP addresses. An IPv4 CIDR example is `10.24.34.0/23` . An IPv6 CIDR example is `2001:DB8::/32` .\n\n> This resource type does not allow you to provision a CIDR using the netmask length. To provision a CIDR using netmask length, use [AWS::EC2::IPAMPoolCidr](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ipampoolcidr.html) .", "properties": { "Cidr": "The CIDR provisioned to the IPAM pool. A CIDR is a representation of an IP address and its associated network mask (or netmask) and refers to a range of IP addresses. An IPv4 CIDR example is `10.24.34.0/23` . An IPv6 CIDR example is `2001:DB8::/32` ." } }, + "AWS::EC2::IPAMPoolCidr": { + "attributes": { + "IpamPoolCidrId": "The IPAM pool CIDR ID.", + "Ref": "`Ref` returns the IPAM pool ID and IPAM pool CIDR ID in the following format: `ipam-pool-01123456|ipam-pool-cidr-0123456` .", + "State": "The state of the CIDR." + }, + "description": "A CIDR provisioned to an IPAM pool.", + "properties": { + "Cidr": "The CIDR provisioned to the IPAM pool. A CIDR is a representation of an IP address and its associated network mask (or netmask) and refers to a range of IP addresses. An IPv4 CIDR example is `10.24.34.0/23` . An IPv6 CIDR example is `2001:DB8::/32` .", + "IpamPoolId": "The ID of the IPAM pool.", + "NetmaskLength": "The netmask length of the CIDR you'd like to provision to a pool. Can be used for provisioning Amazon-provided IPv6 CIDRs to top-level pools and for provisioning CIDRs to pools with source pools. Cannot be used to provision BYOIP CIDRs to top-level pools. \"NetmaskLength\" or \"Cidr\" is required." + } + }, + "AWS::EC2::IPAMResourceDiscovery": { + "attributes": { + "IpamResourceDiscoveryArn": "The resource discovery ARN.", + "IpamResourceDiscoveryId": "The resource discovery ID.", + "IpamResourceDiscoveryRegion": "The resource discovery Region.", + "IsDefault": "Defines if the resource discovery is the default. The default resource discovery is the resource discovery automatically created when you create an IPAM.", + "OwnerId": "The owner ID.", + "Ref": "`Ref` returns the resource discovery ID. For example: `ipam-res-disco-111122223333` .", + "State": "The resource discovery's state.\n\n- `create-in-progress` - Resource discovery is being created.\n- `create-complete` - Resource discovery creation is complete.\n- `create-failed` - Resource discovery creation has failed.\n- `modify-in-progress` - Resource discovery is being modified.\n- `modify-complete` - Resource discovery modification is complete.\n- `modify-failed` - Resource discovery modification has failed.\n- `delete-in-progress` - Resource discovery is being deleted.\n- `delete-complete` - Resource discovery deletion is complete.\n- `delete-failed` - Resource discovery deletion has failed.\n- `isolate-in-progress` - AWS account that created the resource discovery has been removed and the resource discovery is being isolated.\n- `isolate-complete` - Resource discovery isolation is complete.\n- `restore-in-progress` - AWS account that created the resource discovery and was isolated has been restored." + }, + "description": "A resource discovery is an IPAM component that enables IPAM to manage and monitor resources that belong to the owning account.", + "properties": { + "Description": "The resource discovery description.", + "OperatingRegions": "The operating Regions for the resource discovery. Operating Regions are AWS Regions where the IPAM is allowed to manage IP address CIDRs. IPAM only discovers and monitors resources in the AWS Regions you select as operating Regions.", + "Tags": "A tag is a label that you assign to an AWS resource. Each tag consists of a key and an optional value. You can use tags to search and filter your resources or track your AWS costs." + } + }, + "AWS::EC2::IPAMResourceDiscovery.IpamOperatingRegion": { + "attributes": {}, + "description": "The operating Regions for an IPAM. Operating Regions are AWS Regions where the IPAM is allowed to manage IP address CIDRs. IPAM only discovers and monitors resources in the AWS Regions you select as operating Regions.\n\nFor more information about operating Regions, see [Create an IPAM](https://docs.aws.amazon.com//vpc/latest/ipam/create-ipam.html) in the *Amazon VPC IPAM User Guide* .", + "properties": { + "RegionName": "The name of the operating Region." + } + }, + "AWS::EC2::IPAMResourceDiscoveryAssociation": { + "attributes": { + "IpamResourceDiscoveryAssociationArn": "The resource discovery association ARN.", + "IpamResourceDiscoveryAssociationId": "The resource discovery association ID.", + "IsDefault": "Defines if the resource discovery is the default. When you create an IPAM, a default resource discovery is created for your IPAM and it's associated with your IPAM.", + "OwnerId": "The owner ID.", + "Ref": "`Ref` returns the resource discovery ID. For example: `ipam-res-disco-111122223333` .", + "State": "The lifecycle state of the association when you associate or disassociate a resource discovery.\n\n- `associate-in-progress` - Resource discovery is being associated.\n- `associate-complete` - Resource discovery association is complete.\n- `associate-failed` - Resource discovery association has failed.\n- `disassociate-in-progress` - Resource discovery is being disassociated.\n- `disassociate-complete` - Resource discovery disassociation is complete.\n- `disassociate-failed` - Resource discovery disassociation has failed.\n- `isolate-in-progress` - AWS account that created the resource discovery association has been removed and the resource discovery associatation is being isolated.\n- `isolate-complete` - Resource discovery isolation is complete..\n- `restore-in-progress` - Resource discovery is being restored.", + "Status": "The resource discovery status.\n\n- `active` : The connection or permissions required to read the results of the resource discovery are intact.\n- `not-found` : The connection or permissions required to read the results of the resource discovery are broken. This may happen if the owner of the resource discovery stopped sharing it or deleted the resource discovery. Verify the resource discovery still exists and the AWS RAM resource share is still in tact." + }, + "description": "An IPAM resource discovery association. An associated resource discovery is a resource discovery that has been associated with an IPAM. IPAM aggregates the resource CIDRs discovered by the associated resource discovery.", + "properties": { + "IpamId": "The IPAM ID.", + "IpamResourceDiscoveryId": "The resource discovery ID.", + "Tags": "A tag is a label that you assign to an AWS resource. Each tag consists of a key and an optional value. You can use tags to search and filter your resources or track your AWS costs." + } + }, "AWS::EC2::IPAMScope": { "attributes": { "Arn": "The ARN of the scope.", @@ -15147,11 +15243,11 @@ "properties": { "AllocationId": "[Public NAT gateway only] The allocation ID of the Elastic IP address that's associated with the NAT gateway. This property is required for a public NAT gateway and cannot be specified with a private NAT gateway.", "ConnectivityType": "Indicates whether the NAT gateway supports public or private connectivity. The default is public connectivity.", - "MaxDrainDurationSeconds": "", + "MaxDrainDurationSeconds": "The maximum amount of time to wait (in seconds) before forcibly releasing the IP addresses if connections are still in progress. Default value is 350 seconds.", "PrivateIpAddress": "The private IPv4 address to assign to the NAT gateway. If you don't provide an address, a private IPv4 address will be automatically assigned.", - "SecondaryAllocationIds": "", - "SecondaryPrivateIpAddressCount": "", - "SecondaryPrivateIpAddresses": "", + "SecondaryAllocationIds": "Secondary EIP allocation IDs. For more information about secondary addresses, see [Create a NAT gateway](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html#nat-gateway-creating) in the *Amazon Virtual Private Cloud User Guide* .", + "SecondaryPrivateIpAddressCount": "[Private NAT gateway only] The number of secondary private IPv4 addresses you want to assign to the NAT gateway. For more information about secondary addresses, see [Create a NAT gateway](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html#nat-gateway-creating) in the *Amazon Virtual Private Cloud User Guide* .\n\n> `SecondaryPrivateIpAddressCount` and `SecondaryPrivateIpAddresses` cannot be set at the same time.", + "SecondaryPrivateIpAddresses": "Secondary private IPv4 addresses. For more information about secondary addresses, see [Create a NAT gateway](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html#nat-gateway-creating) in the *Amazon Virtual Private Cloud User Guide* .\n\n> `SecondaryPrivateIpAddressCount` and `SecondaryPrivateIpAddresses` cannot be set at the same time.", "SubnetId": "The ID of the subnet in which the NAT gateway is located.", "Tags": "The tags for the NAT gateway." } @@ -16649,7 +16745,7 @@ "SnapshotId": "The snapshot from which to create the volume. You must specify either a snapshot ID or a volume size.", "Tags": "The tags to apply to the volume during creation.", "Throughput": "The throughput to provision for a volume, with a maximum of 1,000 MiB/s.\n\nThis parameter is valid only for `gp3` volumes. The default value is 125.\n\nValid Range: Minimum value of 125. Maximum value of 1000.", - "VolumeType": "The volume type. This parameter can be one of the following values:\n\n- General Purpose SSD: `gp2` | `gp3`\n- Provisioned IOPS SSD: `io1` | `io2`\n- Throughput Optimized HDD: `st1`\n- Cold HDD: `sc1`\n- Magnetic: `standard`\n\n> Throughput Optimized HDD ( `st1` ) and Cold HDD ( `sc1` ) volumes can't be used as boot volumes. \n\nFor more information, see [Amazon EBS volume types](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html) in the *Amazon Elastic Compute Cloud User Guide* .\n\nDefault: `gp2`" + "VolumeType": "The volume type. This parameter can be one of the following values:\n\n- General Purpose SSD: `gp2` | `gp3`\n- Provisioned IOPS SSD: `io1` | `io2`\n- Throughput Optimized HDD: `st1`\n- Cold HDD: `sc1`\n- Magnetic: `standard`\n\nFor more information, see [Amazon EBS volume types](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html) in the *Amazon Elastic Compute Cloud User Guide* .\n\nDefault: `gp2`" } }, "AWS::EC2::VolumeAttachment": { @@ -19450,13 +19546,13 @@ }, "AWS::ElasticLoadBalancingV2::TargetGroup": { "attributes": { - "LoadBalancerArns": "The Amazon Resource Names (ARNs) of the load balancers that route traffic to this target group.", + "LoadBalancerArns": "The Amazon Resource Name (ARN) of the load balancer that routes traffic to this target group.", "Ref": "`Ref` returns the Amazon Resource Name (ARN) of the target group.", - "TargetGroupArn": "", + "TargetGroupArn": "The Amazon Resource Name (ARN) of the target group.", "TargetGroupFullName": "The full name of the target group. For example, `targetgroup/my-target-group/cbf133c568e0d028` .", "TargetGroupName": "The name of the target group. For example, `my-target-group` ." }, - "description": "Specifies a target group for an Application Load Balancer, a Network Load Balancer, or a Gateway Load Balancer.\n\nIf the protocol of the target group is TCP, TLS, UDP, or TCP_UDP, you can't modify the health check protocol, interval, timeout, or success codes.\n\nBefore you register a Lambda function as a target, you must create a `AWS::Lambda::Permission` resource that grants the Elastic Load Balancing service principal permission to invoke the Lambda function.", + "description": "Specifies a target group for an Application Load Balancer, a Network Load Balancer, or a Gateway Load Balancer.\n\nBefore you register a Lambda function as a target, you must create a `AWS::Lambda::Permission` resource that grants the Elastic Load Balancing service principal permission to invoke the Lambda function.", "properties": { "HealthCheckEnabled": "Indicates whether health checks are enabled. If the target type is `lambda` , health checks are disabled by default but can be enabled. If the target type is `instance` , `ip` , or `alb` , health checks are always enabled and cannot be disabled.", "HealthCheckIntervalSeconds": "The approximate amount of time, in seconds, between health checks of an individual target. The range is 5-300. If the target group protocol is TCP, TLS, UDP, TCP_UDP, HTTP or HTTPS, the default is 30 seconds. If the target group protocol is GENEVE, the default is 10 seconds. If the target type is `lambda` , the default is 35 seconds.", @@ -19464,7 +19560,7 @@ "HealthCheckPort": "The port the load balancer uses when performing health checks on targets. If the protocol is HTTP, HTTPS, TCP, TLS, UDP, or TCP_UDP, the default is `traffic-port` , which is the port on which each target receives traffic from the load balancer. If the protocol is GENEVE, the default is port 80.", "HealthCheckProtocol": "The protocol the load balancer uses when performing health checks on targets. For Application Load Balancers, the default is HTTP. For Network Load Balancers and Gateway Load Balancers, the default is TCP. The TCP protocol is not supported for health checks if the protocol of the target group is HTTP or HTTPS. The GENEVE, TLS, UDP, and TCP_UDP protocols are not supported for health checks.", "HealthCheckTimeoutSeconds": "The amount of time, in seconds, during which no response from a target means a failed health check. The range is 2\u2013120 seconds. For target groups with a protocol of HTTP, the default is 6 seconds. For target groups with a protocol of TCP, TLS or HTTPS, the default is 10 seconds. For target groups with a protocol of GENEVE, the default is 5 seconds. If the target type is `lambda` , the default is 30 seconds.", - "HealthyThresholdCount": "The number of consecutive health check successes required before considering a target healthy. The range is 2-10. If the target group protocol is TCP, TCP_UDP, UDP, TLS, HTTP or HTTPS, the default is 5. For target groups with a protocol of GENEVE, the default is 3. If the target type is `lambda` , the default is 5.", + "HealthyThresholdCount": "The number of consecutive health check successes required before considering a target healthy. The range is 2-10. If the target group protocol is TCP, TCP_UDP, UDP, TLS, HTTP or HTTPS, the default is 5. For target groups with a protocol of GENEVE, the default is 5. If the target type is `lambda` , the default is 5.", "IpAddressType": "The type of IP address used for this target group. The possible values are `ipv4` and `ipv6` . This is an optional parameter. If not specified, the IP address type defaults to `ipv4` .", "Matcher": "[HTTP/HTTPS health checks] The HTTP or gRPC codes to use when checking for a successful response from a target. For target groups with a protocol of TCP, TCP_UDP, UDP or TLS the range is 200-599. For target groups with a protocol of HTTP or HTTPS, the range is 200-499. For target groups with a protocol of GENEVE, the range is 200-399.", "Name": "The name of the target group.\n\nThis name must be unique per region per account, can have a maximum of 32 characters, must contain only alphanumeric characters or hyphens, and must not begin or end with a hyphen.", @@ -19475,7 +19571,7 @@ "TargetGroupAttributes": "The attributes.", "TargetType": "The type of target that you must specify when registering targets with this target group. You can't specify targets for a target group using more than one target type.\n\n- `instance` - Register targets by instance ID. This is the default value.\n- `ip` - Register targets by IP address. You can specify IP addresses from the subnets of the virtual private cloud (VPC) for the target group, the RFC 1918 range (10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16), and the RFC 6598 range (100.64.0.0/10). You can't specify publicly routable IP addresses.\n- `lambda` - Register a single Lambda function as a target.\n- `alb` - Register a single Application Load Balancer as a target.", "Targets": "The targets.", - "UnhealthyThresholdCount": "The number of consecutive health check failures required before considering a target unhealthy. The range is 2-10. If the target group protocol is TCP, TCP_UDP, UDP, TLS, HTTP or HTTPS, the default is 2. For target groups with a protocol of GENEVE, the default is 3. If the target type is `lambda` , the default is 5.", + "UnhealthyThresholdCount": "The number of consecutive health check failures required before considering a target unhealthy. The range is 2-10. If the target group protocol is TCP, TCP_UDP, UDP, TLS, HTTP or HTTPS, the default is 2. For target groups with a protocol of GENEVE, the default is 2. If the target type is `lambda` , the default is 5.", "VpcId": "The identifier of the virtual private cloud (VPC). If the target is a Lambda function, this parameter does not apply. Otherwise, this parameter is required." } }, @@ -30352,7 +30448,7 @@ }, "AWS::LakeFormation::Resource": { "attributes": {}, - "description": "The `AWS::LakeFormation::Resource` represents the data ( buckets and folders) that is being registered with AWS Lake Formation . During a stack operation, AWS CloudFormation calls the AWS Lake Formation [`RegisterResource`](https://docs.aws.amazon.com/lake-formation/latest/dg/aws-lake-formation-api-credential-vending.html#aws-lake-formation-api-credential-vending-RegisterResource) API operation to register the resource. To remove a `Resource` type, AWS CloudFormation calls the AWS Lake Formation [`DeregisterResource`](https://docs.aws.amazon.com/lake-formation/latest/dg/aws-lake-formation-api-credential-vending.html#aws-lake-formation-api-credential-vending-DeregisterResource) API operation.", + "description": "The `AWS::LakeFormation::Resource` represents the data ( buckets and folders) that is being registered with AWS Lake Formation . During a stack operation, AWS CloudFormation calls the AWS Lake Formation [`RegisterResource`](https://docs.aws.amazon.com/lake-formation/latest/dg/aws-lake-formation-api-credential-vending.html#aws-lake-formation-api-credential-vending-RegisterResource) API operation to register the resource. To remove a `Resource` type, AWS CloudFormation calls the AWS Lake Formation [`DeregisterResource`](https://docs.aws.amazon.com/lake-formation/latest/dg/aws-lake-formation-api-credential-vending.html#aws-lake-formation-api-credential-vending-DeregisterResource) API operation.\n\n> `AWS::LakeFormation::Resource` is a legacy resource that doesn't support the `UPDATE` operation. Changes to the resource will require an explicit deletion and recreation to apply new properties.", "properties": { "ResourceArn": "The Amazon Resource Name (ARN) of the resource.", "RoleArn": "The IAM role that registered a resource.", @@ -30794,7 +30890,7 @@ "Action": "The action that the principal can use on the function. For example, `lambda:InvokeFunction` or `lambda:GetFunction` .", "EventSourceToken": "For Alexa Smart Home functions, a token that the invoker must supply.", "FunctionName": "The name of the Lambda function, version, or alias.\n\n**Name formats** - *Function name* \u2013 `my-function` (name-only), `my-function:v1` (with alias).\n- *Function ARN* \u2013 `arn:aws:lambda:us-west-2:123456789012:function:my-function` .\n- *Partial ARN* \u2013 `123456789012:function:my-function` .\n\nYou can append a version number or alias to any of the formats. The length constraint applies only to the full ARN. If you specify only the function name, it is limited to 64 characters in length.", - "FunctionUrlAuthType": "The type of authentication that your function URL uses. Set to `AWS_IAM` if you want to restrict access to authenticated IAM users only. Set to `NONE` if you want to bypass IAM authentication to create a public endpoint. For more information, see [Security and auth model for Lambda function URLs](https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html) .", + "FunctionUrlAuthType": "The type of authentication that your function URL uses. Set to `AWS_IAM` if you want to restrict access to authenticated users only. Set to `NONE` if you want to bypass IAM authentication to create a public endpoint. For more information, see [Security and auth model for Lambda function URLs](https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html) .", "Principal": "The AWS service or AWS account that invokes the function. If you specify a service, use `SourceArn` or `SourceAccount` to limit who can invoke the function through that service.", "PrincipalOrgID": "The identifier for your organization in AWS Organizations . Use this to grant permissions to all the AWS accounts under this organization.", "SourceAccount": "For AWS service , the ID of the AWS account that owns the resource. Use this together with `SourceArn` to ensure that the specified account owns the resource. It is possible for an Amazon S3 bucket to be deleted by its owner and recreated by another account.", @@ -30809,7 +30905,7 @@ }, "description": "The `AWS::Lambda::Url` resource creates a function URL with the specified configuration parameters. A [function URL](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html) is a dedicated HTTP(S) endpoint that you can use to invoke your function.", "properties": { - "AuthType": "The type of authentication that your function URL uses. Set to `AWS_IAM` if you want to restrict access to authenticated IAM users only. Set to `NONE` if you want to bypass IAM authentication to create a public endpoint. For more information, see [Security and auth model for Lambda function URLs](https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html) .", + "AuthType": "The type of authentication that your function URL uses. Set to `AWS_IAM` if you want to restrict access to authenticated users only. Set to `NONE` if you want to bypass IAM authentication to create a public endpoint. For more information, see [Security and auth model for Lambda function URLs](https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html) .", "Cors": "The [Cross-Origin Resource Sharing (CORS)](https://docs.aws.amazon.com/https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) settings for your function URL.", "InvokeMode": "", "Qualifier": "The alias name.", @@ -36740,26 +36836,6 @@ "Tags": "The list of key-value tags that changed for the segment." } }, - "AWS::NetworkManager::TransitGatewayPeering": { - "attributes": { - "CoreNetworkArn": "The ARN of the core network.", - "CreatedAt": "The timestamp when the core network peering was created.", - "EdgeLocation": "The edge location for the peer.", - "OwnerAccountId": "The ID of the account owner.", - "PeeringId": "The ID of the peering.", - "PeeringType": "The peering type. This will be `TRANSIT_GATEWAY` .", - "Ref": "`Ref` returns the `peeringId` . For example: `peering-01234ab1234a12a12` .", - "ResourceArn": "The ARN of the resource peered to a core network.", - "State": "The current state of the peer. This can be `CREATING` | `FAILED` | `AVAILABLE` | `DELETING` .", - "TransitGatewayPeeringAttachmentId": "The ID of the peering attachment." - }, - "description": "Creates a transit gateway peering connection.", - "properties": { - "CoreNetworkId": "The ID of the core network.", - "Tags": "The list of key-value tags associated with the peering.", - "TransitGatewayArn": "The ARN of the transit gateway." - } - }, "AWS::NetworkManager::TransitGatewayRegistration": { "attributes": { "Ref": "`Ref` returns the ID of the global network and the ARN of the transit gateway. For example: `global-network-01231231231231231|arn:aws:ec2:us-west-2:123456789012:transit-gateway/tgw-123abc05e04123abc` ." @@ -37038,6 +37114,185 @@ "Tags": "An array of key-value pairs to apply to the sink.\n\nFor more information, see [Tag](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html) ." } }, + "AWS::Omics::AnnotationStore": { + "attributes": { + "CreationTime": "When the store was created.", + "Id": "The store's ID.", + "Ref": "`Ref` returns the details of this resource. For example:\n\n`{ \"Ref\": \"AnnotationStore.Id\" }` `Ref` returns the id for the annotation store.", + "Status": "The store's status.", + "StatusMessage": "The store's status message.", + "StoreArn": "The store's ARN.", + "StoreSizeBytes": "The store's size in bytes.", + "UpdateTime": "When the store was updated." + }, + "description": "Creates an annotation store.", + "properties": { + "Description": "A description for the store.", + "Name": "The name of the Annotation Store.", + "Reference": "The genome reference for the store's annotations.", + "SseConfig": "The store's server-side encryption (SSE) settings.", + "StoreFormat": "The annotation file format of the store.", + "StoreOptions": "File parsing options for the annotation store.", + "Tags": "Tags for the store." + } + }, + "AWS::Omics::AnnotationStore.ReferenceItem": { + "attributes": {}, + "description": "A genome reference.", + "properties": { + "ReferenceArn": "The reference's ARN." + } + }, + "AWS::Omics::AnnotationStore.SseConfig": { + "attributes": {}, + "description": "Server-side encryption (SSE) settings for a store.", + "properties": { + "KeyArn": "An encryption key ARN.", + "Type": "The encryption type." + } + }, + "AWS::Omics::AnnotationStore.StoreOptions": { + "attributes": {}, + "description": "The store's file parsing options.", + "properties": { + "TsvStoreOptions": "Formatting options for a TSV file." + } + }, + "AWS::Omics::AnnotationStore.TsvStoreOptions": { + "attributes": {}, + "description": "The store's parsing options.", + "properties": { + "AnnotationType": "The store's annotation type.", + "FormatToHeader": "The store's header key to column name mapping.", + "Schema": "The schema of an annotation store." + } + }, + "AWS::Omics::ReferenceStore": { + "attributes": { + "Arn": "", + "CreationTime": "When the store was created.", + "Ref": "`Ref` returns the details of this resource. For example:\n\n`{ \"Ref\": \"ReferenceStore.Arn\" }` `Ref` returns the arn for the reference store.", + "ReferenceStoreId": "The store's ID." + }, + "description": "Creates a reference store.", + "properties": { + "Description": "A description for the store.", + "Name": "A name for the store.", + "SseConfig": "Server-side encryption (SSE) settings for the store.", + "Tags": "Tags for the store." + } + }, + "AWS::Omics::ReferenceStore.SseConfig": { + "attributes": {}, + "description": "Server-side encryption (SSE) settings for a store.", + "properties": { + "KeyArn": "An encryption key ARN.", + "Type": "The encryption type." + } + }, + "AWS::Omics::RunGroup": { + "attributes": { + "Arn": "The run group's ARN.", + "CreationTime": "When the run group was created.", + "Id": "The run group's ID.", + "Ref": "`Ref` returns the details of this resource. For example:\n\n`{ \"Ref\": \"RunGroup.CreationTime\" }` `Ref` returns the timestamp for a run group." + }, + "description": "Creates a run group.", + "properties": { + "MaxCpus": "The group's maximum CPU count setting.", + "MaxDuration": "The group's maximum duration setting in minutes.", + "MaxRuns": "The group's maximum concurrent run setting.", + "Name": "The group's name.", + "Tags": "Tags for the group." + } + }, + "AWS::Omics::SequenceStore": { + "attributes": { + "Arn": "The store's ARN.", + "CreationTime": "When the store was created.", + "Ref": "`Ref` returns the details of this resource. For example:\n\n`{ \"Ref\": \"SequenceStore.CreationTime\" }` `Ref` returns the timestamp for when the sequence store was created.", + "SequenceStoreId": "The store's ID." + }, + "description": "Creates a sequence store.", + "properties": { + "Description": "A description for the store.", + "Name": "A name for the store.", + "SseConfig": "Server-side encryption (SSE) settings for the store.", + "Tags": "Tags for the store." + } + }, + "AWS::Omics::SequenceStore.SseConfig": { + "attributes": {}, + "description": "Server-side encryption (SSE) settings for a store.", + "properties": { + "KeyArn": "An encryption key ARN.", + "Type": "The encryption type." + } + }, + "AWS::Omics::VariantStore": { + "attributes": { + "CreationTime": "When the store was created.", + "Id": "The store's ID.", + "Ref": "`Ref` returns the details of this resource. For example:\n\n`{ \"Ref\": \"VariantStore.Status\" }`\n\nFor the Amazon Omics resource `VariantStore.Status` , `Ref` returns the status of the variant store.", + "Status": "The store's status.", + "StatusMessage": "The store's status message.", + "StoreArn": "The store's ARN.", + "StoreSizeBytes": "The store's size in bytes.", + "UpdateTime": "When the store was updated." + }, + "description": "Create a store for variant data.", + "properties": { + "Description": "A description for the store.", + "Name": "A name for the store.", + "Reference": "The genome reference for the store's variants.", + "SseConfig": "Server-side encryption (SSE) settings for the store.", + "Tags": "Tags for the store." + } + }, + "AWS::Omics::VariantStore.ReferenceItem": { + "attributes": {}, + "description": "The read set's genome reference ARN.", + "properties": { + "ReferenceArn": "The reference's ARN." + } + }, + "AWS::Omics::VariantStore.SseConfig": { + "attributes": {}, + "description": "Server-side encryption (SSE) settings for a store.", + "properties": { + "KeyArn": "An encryption key ARN.", + "Type": "The encryption type." + } + }, + "AWS::Omics::Workflow": { + "attributes": { + "Arn": "The ARN for the workflow.", + "CreationTime": "When the workflow was created.", + "Id": "The workflow's ID.", + "Ref": "`Ref` returns the details of this resource. For example:\n\n`{ \"Ref\": \"Workflow.Type\" }` `Ref` returns the type of workflow.", + "Status": "The workflow's status.", + "Type": "The workflow's type." + }, + "description": "Creates a workflow.", + "properties": { + "DefinitionUri": "The URI of a definition for the workflow.", + "Description": "The parameter's description.", + "Engine": "An engine for the workflow.", + "Main": "The path of the main definition file for the workflow.", + "Name": "The workflow's name.", + "ParameterTemplate": "The workflow's parameter template.", + "StorageCapacity": "A storage capacity for the workflow in gigabytes.", + "Tags": "Tags for the workflow." + } + }, + "AWS::Omics::Workflow.WorkflowParameter": { + "attributes": {}, + "description": "A workflow parameter.", + "properties": { + "Description": "The parameter's description.", + "Optional": "Whether the parameter is optional." + } + }, "AWS::OpenSearchServerless::AccessPolicy": { "attributes": { "Ref": "When you pass the logical ID of this resource to the intrinsic `Ref` function, `Ref` returns the name of the access policy. For more information about using the `Ref` function, see [Ref](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) ." @@ -37681,7 +37936,7 @@ "Email": "The email address associated with the AWS account.\n\nThe [regex pattern](https://docs.aws.amazon.com/http://wikipedia.org/wiki/regex) for this parameter is a string of characters that represents a standard internet email address.", "ParentIds": "The unique identifier (ID) of the root or organizational unit (OU) that you want to create the new account in. If you don't specify this parameter, the `ParentId` defaults to the root ID.\n\nThis parameter only accepts a string array with one string value.\n\nThe [regex pattern](https://docs.aws.amazon.com/http://wikipedia.org/wiki/regex) for a parent ID string requires one of the following:\n\n- *Root* - A string that begins with \"r-\" followed by from 4 to 32 lowercase letters or digits.\n- *Organizational unit (OU)* - A string that begins with \"ou-\" followed by from 4 to 32 lowercase letters or digits (the ID of the root that the OU is in). This string is followed by a second \"-\" dash and from 8 to 32 additional lowercase letters or digits.", "RoleName": "The name of an IAM role that AWS Organizations automatically preconfigures in the new member account. This role trusts the management account, allowing users in the management account to assume the role, as permitted by the management account administrator. The role has administrator permissions in the new member account.\n\nIf you don't specify this parameter, the role name defaults to `OrganizationAccountAccessRole` .\n\nFor more information about how to use this role to access the member account, see the following links:\n\n- [Accessing and Administering the Member Accounts in Your Organization](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_accounts_access.html#orgs_manage_accounts_create-cross-account-role) in the *AWS Organizations User Guide*\n- Steps 2 and 3 in [Tutorial: Delegate Access Across AWS accounts Using IAM Roles](https://docs.aws.amazon.com/IAM/latest/UserGuide/tutorial_cross-account-with-roles.html) in the *IAM User Guide*\n\nThe [regex pattern](https://docs.aws.amazon.com/http://wikipedia.org/wiki/regex) that is used to validate this parameter. The pattern can include uppercase letters, lowercase letters, digits with no spaces, and any of the following characters: =,.@-", - "Tags": "A list of tags that you want to attach to the newly created account. For each tag in the list, you must specify both a tag key and a value. You can set the value to an empty string, but you can't set it to `null` . For more information about tagging, see [Tagging AWS Organizations resources](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_tagging.html) in the AWS Organizations User Guide.\n\n> If any one of the tags is invalid or if you exceed the maximum allowed number of tags for an account, then the entire request fails and the account is not created." + "Tags": "A list of tags that you want to attach to the newly created account. For each tag in the list, you must specify both a tag key and a value. You can set the value to an empty string, but you can't set it to `null` . For more information about tagging, see [Tagging AWS Organizations resources](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_tagging.html) in the AWS Organizations User Guide.\n\n> If any one of the tags is not valid or if you exceed the maximum allowed number of tags for an account, then the entire request fails and the account is not created." } }, "AWS::Organizations::OrganizationalUnit": { @@ -37694,7 +37949,7 @@ "properties": { "Name": "The friendly name of this OU.\n\nThe [regex pattern](https://docs.aws.amazon.com/http://wikipedia.org/wiki/regex) that is used to validate this parameter is a string of any of the characters in the ASCII character range.", "ParentId": "The unique identifier (ID) of the parent root or OU that you want to create the new OU in.\n\n> To update the `ParentId` parameter value, you must first remove all accounts attached to the organizational unit (OU). OUs can't be moved within the organization with accounts still attached. \n\nThe [regex pattern](https://docs.aws.amazon.com/http://wikipedia.org/wiki/regex) for a parent ID string requires one of the following:\n\n- *Root* - A string that begins with \"r-\" followed by from 4 to 32 lowercase letters or digits.\n- *Organizational unit (OU)* - A string that begins with \"ou-\" followed by from 4 to 32 lowercase letters or digits (the ID of the root that the OU is in). This string is followed by a second \"-\" dash and from 8 to 32 additional lowercase letters or digits.", - "Tags": "A list of tags that you want to attach to the newly created OU. For each tag in the list, you must specify both a tag key and a value. You can set the value to an empty string, but you can't set it to `null` . For more information about tagging, see [Tagging AWS Organizations resources](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_tagging.html) in the AWS Organizations User Guide.\n\n> If any one of the tags is invalid or if you exceed the allowed number of tags for an OU, then the entire request fails and the OU is not created." + "Tags": "A list of tags that you want to attach to the newly created OU. For each tag in the list, you must specify both a tag key and a value. You can set the value to an empty string, but you can't set it to `null` . For more information about tagging, see [Tagging AWS Organizations resources](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_tagging.html) in the AWS Organizations User Guide.\n\n> If any one of the tags is not valid or if you exceed the allowed number of tags for an OU, then the entire request fails and the OU is not created." } }, "AWS::Organizations::Policy": { @@ -37709,7 +37964,7 @@ "Content": "The policy text content.", "Description": "Human readable description of the policy.", "Name": "Name of the policy.\n\nThe [regex pattern](https://docs.aws.amazon.com/http://wikipedia.org/wiki/regex) that is used to validate this parameter is a string of any of the characters in the ASCII character range.", - "Tags": "A list of tags that you want to attach to the newly created policy. For each tag in the list, you must specify both a tag key and a value. You can set the value to an empty string, but you can't set it to `null` . For more information about tagging, see [Tagging AWS Organizations resources](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_tagging.html) in the AWS Organizations User Guide.\n\n> If any one of the tags is invalid or if you exceed the allowed number of tags for a policy, then the entire request fails and the policy is not created.", + "Tags": "A list of tags that you want to attach to the newly created policy. For each tag in the list, you must specify both a tag key and a value. You can set the value to an empty string, but you can't set it to `null` . For more information about tagging, see [Tagging AWS Organizations resources](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_tagging.html) in the AWS Organizations User Guide.\n\n> If any one of the tags is not valid or if you exceed the allowed number of tags for a policy, then the entire request fails and the policy is not created.", "TargetIds": "List of unique identifiers (IDs) of the root, OU, or account that you want to attach the policy to. You can get the ID by calling the [ListRoots](https://docs.aws.amazon.com/organizations/latest/APIReference/API_ListRoots.html) , [ListOrganizationalUnitsForParent](https://docs.aws.amazon.com/organizations/latest/APIReference/API_ListOrganizationalUnitsForParent.html) , or [ListAccounts](https://docs.aws.amazon.com/organizations/latest/APIReference/API_ListAccounts.html) operations. If you don't specify this parameter, the policy is created but not attached to any organization resource.\n\nThe [regex pattern](https://docs.aws.amazon.com/http://wikipedia.org/wiki/regex) for a target ID string requires one of the following:\n\n- *Root* - A string that begins with \"r-\" followed by from 4 to 32 lowercase letters or digits.\n- *Account* - A string that consists of exactly 12 digits.\n- *Organizational unit (OU)* - A string that begins with \"ou-\" followed by from 4 to 32 lowercase letters or digits (the ID of the root that the OU is in). This string is followed by a second \"-\" dash and from 8 to 32 additional lowercase letters or digits.", "Type": "The type of policy to create." } @@ -39243,7 +39498,7 @@ "description": "The parameters for using an EventBridge event bus as a target.", "properties": { "DetailType": "A free-form string, with a maximum of 128 characters, used to decide what fields to expect in the event detail.", - "EndpointId": "The URL subdomain of the endpoint. For example, if the URL for Endpoint is https://abcde.veo.endpoints.event.amazonaws.com, then the EndpointId is `abcde.veo` .\n\n> When using Java, you must include `auth-crt` on the class path.", + "EndpointId": "The URL subdomain of the endpoint. For example, if the URL for Endpoint is https://abcde.veo.endpoints.event.amazonaws.com, then the EndpointId is `abcde.veo` .", "Resources": "AWS resources, identified by Amazon Resource Name (ARN), which the event primarily concerns. Any number, including zero, may be present.", "Source": "The source of the event.", "Time": "The time stamp of the event, per [RFC3339](https://docs.aws.amazon.com/https://www.rfc-editor.org/rfc/rfc3339.txt) . If no time stamp is provided, the time stamp of the [PutEvents](https://docs.aws.amazon.com/eventbridge/latest/APIReference/API_PutEvents.html) call is used." @@ -39411,7 +39666,7 @@ "DataSetArns": "The ARNs of the datasets of the analysis.", "LastUpdatedTime": "The time that the analysis was last updated." }, - "description": "Creates an analysis in Amazon QuickSight.", + "description": "Creates an analysis in Amazon QuickSight. Analyses can be created either from a template or from an `AnalysisDefinition` .", "properties": { "AnalysisId": "The ID for the analysis that you're creating. This ID displays in the URL of the analysis.", "AwsAccountId": "The ID of the AWS account where you are creating an analysis.", @@ -39419,7 +39674,7 @@ "Name": "A descriptive name for the analysis that you're creating. This name displays for the analysis in the Amazon QuickSight console.", "Parameters": "The parameter names and override values that you want to use. An analysis can have any parameter type, and some parameters might accept multiple values.", "Permissions": "A structure that describes the principals and the resource-level permissions on an analysis. You can use the `Permissions` structure to grant permissions by providing a list of AWS Identity and Access Management (IAM) action information for each principal listed by Amazon Resource Name (ARN).\n\nTo specify no permissions, omit `Permissions` .", - "SourceEntity": "A source entity to use for the analysis that you're creating. This metadata structure contains details that describe a source template and one or more datasets.", + "SourceEntity": "A source entity to use for the analysis that you're creating. This metadata structure contains details that describe a source template and one or more datasets.\n\nEither a `SourceEntity` or a `Definition` must be provided in order for the request to be valid.", "Tags": "Contains a map of the key-value pairs for the resource tag or tags assigned to the analysis.", "ThemeArn": "The ARN for the theme to apply to the analysis that you're creating. To see the theme in the Amazon QuickSight console, make sure that you have access to it." } @@ -40230,12 +40485,12 @@ "Version.ThemeArn": "", "Version.VersionNumber": "" }, - "description": "Creates a template from an existing Amazon QuickSight analysis or template. You can use the resulting template to create a dashboard.\n\nA *template* is an entity in Amazon QuickSight that encapsulates the metadata required to create an analysis and that you can use to create s dashboard. A template adds a layer of abstraction by using placeholders to replace the dataset associated with the analysis. You can use templates to create dashboards by replacing dataset placeholders with datasets that follow the same schema that was used to create the source analysis and template.", + "description": "Creates a template either from a `TemplateDefinition` or from an existing Amazon QuickSight analysis or template. You can use the resulting template to create additional dashboards, templates, or analyses.\n\nA *template* is an entity in Amazon QuickSight that encapsulates the metadata required to create an analysis and that you can use to create s dashboard. A template adds a layer of abstraction by using placeholders to replace the dataset associated with the analysis. You can use templates to create dashboards by replacing dataset placeholders with datasets that follow the same schema that was used to create the source analysis and template.", "properties": { "AwsAccountId": "The ID for the AWS account that the group is in. You use the ID for the AWS account that contains your Amazon QuickSight account.", "Name": "A display name for the template.", "Permissions": "A list of resource permissions to be set on the template.", - "SourceEntity": "The entity that you are using as a source when you create the template. In `SourceEntity` , you specify the type of object you're using as source: `SourceTemplate` for a template or `SourceAnalysis` for an analysis. Both of these require an Amazon Resource Name (ARN). For `SourceTemplate` , specify the ARN of the source template. For `SourceAnalysis` , specify the ARN of the source analysis. The `SourceTemplate` ARN can contain any AWS account and any Amazon QuickSight-supported AWS Region .\n\nUse the `DataSetReferences` entity within `SourceTemplate` or `SourceAnalysis` to list the replacement datasets for the placeholders listed in the original. The schema in each dataset must match its placeholder.", + "SourceEntity": "The entity that you are using as a source when you create the template. In `SourceEntity` , you specify the type of object you're using as source: `SourceTemplate` for a template or `SourceAnalysis` for an analysis. Both of these require an Amazon Resource Name (ARN). For `SourceTemplate` , specify the ARN of the source template. For `SourceAnalysis` , specify the ARN of the source analysis. The `SourceTemplate` ARN can contain any AWS account and any Amazon QuickSight-supported AWS Region .\n\nUse the `DataSetReferences` entity within `SourceTemplate` or `SourceAnalysis` to list the replacement datasets for the placeholders listed in the original. The schema in each dataset must match its placeholder.\n\nEither a `SourceEntity` or a `Definition` must be provided in order for the request to be valid.", "Tags": "Contains a map of the key-value pairs for the resource tag or tags assigned to the resource.", "TemplateId": "An ID for the template that you want to create. This template is unique per AWS Region ; in each AWS account.", "VersionDescription": "A description of the current template version being created. This API operation creates the first version of the template. Every time `UpdateTemplate` is called, a new version is created. Each version of the template maintains a description of the version in the `VersionDescription` field." @@ -40686,7 +40941,7 @@ "CopyTagsToSnapshot": "A value that indicates whether to copy tags from the DB instance to snapshots of the DB instance. By default, tags are not copied.\n\n*Amazon Aurora*\n\nNot applicable. Copying tags to snapshots is managed by the DB cluster. Setting this value for an Aurora DB instance has no effect on the DB cluster setting.", "CustomIAMInstanceProfile": "The instance profile associated with the underlying Amazon EC2 instance of an RDS Custom DB instance. The instance profile must meet the following requirements:\n\n- The profile must exist in your account.\n- The profile must have an IAM role that Amazon EC2 has permissions to assume.\n- The instance profile name and the associated IAM role name must start with the prefix `AWSRDSCustom` .\n\nFor the list of permissions required for the IAM role, see [Configure IAM and your VPC](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/custom-setup-orcl.html#custom-setup-orcl.iam-vpc) in the *Amazon RDS User Guide* .\n\nThis setting is required for RDS Custom.", "DBClusterIdentifier": "The identifier of the DB cluster that the instance will belong to.", - "DBClusterSnapshotIdentifier": "The identifier for the RDS for MySQL Multi-AZ DB cluster snapshot to restore from.\n\nFor more information on Multi-AZ DB clusters, see [Multi-AZ deployments with two readable standby DB instances](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/multi-az-db-clusters-concepts.html) in the *Amazon RDS User Guide* .\n\nConstraints:\n\n- Must match the identifier of an existing Multi-AZ DB cluster snapshot.\n- Can't be specified when `DBSnapshotIdentifier` is specified.\n- Must be specified when `DBSnapshotIdentifier` isn't specified.\n- If you are restoring from a shared manual Multi-AZ DB cluster snapshot, the `DBClusterSnapshotIdentifier` must be the ARN of the shared snapshot.\n- Can't be the identifier of an Aurora DB cluster snapshot.\n- Can't be the identifier of an RDS for PostgreSQL Multi-AZ DB cluster snapshot.", + "DBClusterSnapshotIdentifier": "The identifier for the RDS for MySQL Multi-AZ DB cluster snapshot to restore from.\n\nFor more information on Multi-AZ DB clusters, see [Multi-AZ DB cluster deployments](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/multi-az-db-clusters-concepts.html) in the *Amazon RDS User Guide* .\n\nConstraints:\n\n- Must match the identifier of an existing Multi-AZ DB cluster snapshot.\n- Can't be specified when `DBSnapshotIdentifier` is specified.\n- Must be specified when `DBSnapshotIdentifier` isn't specified.\n- If you are restoring from a shared manual Multi-AZ DB cluster snapshot, the `DBClusterSnapshotIdentifier` must be the ARN of the shared snapshot.\n- Can't be the identifier of an Aurora DB cluster snapshot.\n- Can't be the identifier of an RDS for PostgreSQL Multi-AZ DB cluster snapshot.", "DBInstanceClass": "The compute and memory capacity of the DB instance, for example, `db.m4.large` . Not all DB instance classes are available in all AWS Regions, or for all database engines.\n\nFor the full list of DB instance classes, and availability for your engine, see [DB Instance Class](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.DBInstanceClass.html) in the *Amazon RDS User Guide.* For more information about DB instance class pricing and AWS Region support for DB instance classes, see [Amazon RDS Pricing](https://docs.aws.amazon.com/rds/pricing/) .", "DBInstanceIdentifier": "A name for the DB instance. If you specify a name, AWS CloudFormation converts it to lowercase. If you don't specify a name, AWS CloudFormation generates a unique physical ID and uses that ID for the DB instance. For more information, see [Name Type](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-name.html) .\n\nFor information about constraints that apply to DB instance identifiers, see [Naming constraints in Amazon RDS](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Limits.html#RDS_Limits.Constraints) in the *Amazon RDS User Guide* .\n\n> If you specify a name, you can't perform updates that require replacement of this resource. You can perform updates that require no or some interruption. If you must replace the resource, specify a new name.", "DBName": "The meaning of this parameter differs according to the database engine you use.\n\n> If you specify the `[DBSnapshotIdentifier](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-database-instance.html#cfn-rds-dbinstance-dbsnapshotidentifier)` property, this property only applies to RDS for Oracle. \n\n*Amazon Aurora*\n\nNot applicable. The database name is managed by the DB cluster.\n\n*MySQL*\n\nThe name of the database to create when the DB instance is created. If this parameter is not specified, no database is created in the DB instance.\n\nConstraints:\n\n- Must contain 1 to 64 letters or numbers.\n- Can't be a word reserved by the specified database engine\n\n*MariaDB*\n\nThe name of the database to create when the DB instance is created. If this parameter is not specified, no database is created in the DB instance.\n\nConstraints:\n\n- Must contain 1 to 64 letters or numbers.\n- Can't be a word reserved by the specified database engine\n\n*PostgreSQL*\n\nThe name of the database to create when the DB instance is created. If this parameter is not specified, the default `postgres` database is created in the DB instance.\n\nConstraints:\n\n- Must begin with a letter. Subsequent characters can be letters, underscores, or digits (0-9).\n- Must contain 1 to 63 characters.\n- Can't be a word reserved by the specified database engine\n\n*Oracle*\n\nThe Oracle System ID (SID) of the created DB instance. If you specify `null` , the default value `ORCL` is used. You can't specify the string NULL, or any other reserved word, for `DBName` .\n\nDefault: `ORCL`\n\nConstraints:\n\n- Can't be longer than 8 characters\n\n*SQL Server*\n\nNot applicable. Must be null.", @@ -41098,7 +41353,7 @@ "MaintenanceTrackName": "An optional parameter for the name of the maintenance track for the cluster. If you don't provide a maintenance track name, the cluster is assigned to the `current` track.", "ManualSnapshotRetentionPeriod": "The default number of days to retain a manual snapshot. If the value is -1, the snapshot is retained indefinitely. This setting doesn't change the retention period of existing snapshots.\n\nThe value must be either -1 or an integer between 1 and 3,653.", "MasterUserPassword": "The password associated with the admin user account for the cluster that is being created.\n\nConstraints:\n\n- Must be between 8 and 64 characters in length.\n- Must contain at least one uppercase letter.\n- Must contain at least one lowercase letter.\n- Must contain one number.\n- Can be any printable ASCII character (ASCII code 33-126) except `'` (single quote), `\"` (double quote), `\\` , `/` , or `@` .", - "MasterUsername": "The user name associated with the admin user account for the cluster that is being created.\n\nConstraints:\n\n- Must be 1 - 128 alphanumeric characters. The user name can't be `PUBLIC` .\n- First character must be a letter.\n- Cannot be a reserved word. A list of reserved words can be found in [Reserved Words](https://docs.aws.amazon.com/redshift/latest/dg/r_pg_keywords.html) in the Amazon Redshift Database Developer Guide.", + "MasterUsername": "The user name associated with the admin user account for the cluster that is being created.\n\nConstraints:\n\n- Must be 1 - 128 alphanumeric characters or hyphens. The user name can't be `PUBLIC` .\n- Must contain only lowercase letters, numbers, underscore, plus sign, period (dot), at symbol (@), or hyphen.\n- The first character must be a letter.\n- Must not contain a colon (:) or a slash (/).\n- Cannot be a reserved word. A list of reserved words can be found in [Reserved Words](https://docs.aws.amazon.com/redshift/latest/dg/r_pg_keywords.html) in the Amazon Redshift Database Developer Guide.", "NodeType": "The node type to be provisioned for the cluster. For information about node types, go to [Working with Clusters](https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-clusters.html#how-many-nodes) in the *Amazon Redshift Cluster Management Guide* .\n\nValid Values: `ds2.xlarge` | `ds2.8xlarge` | `dc1.large` | `dc1.8xlarge` | `dc2.large` | `dc2.8xlarge` | `ra3.xlplus` | `ra3.4xlarge` | `ra3.16xlarge`", "NumberOfNodes": "The number of compute nodes in the cluster. This parameter is required when the *ClusterType* parameter is specified as `multi-node` .\n\nFor information about determining how many nodes you need, go to [Working with Clusters](https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-clusters.html#how-many-nodes) in the *Amazon Redshift Cluster Management Guide* .\n\nIf you don't specify this parameter, you get a single-node cluster. When requesting a multi-node cluster, you must specify the number of nodes that you want in the cluster.\n\nDefault: `1`\n\nConstraints: Value must be at least 1 and no more than 100.", "OwnerAccount": "The AWS account used to create or copy the snapshot. Required if you are restoring a snapshot you do not own, optional if you own the snapshot.", @@ -41112,7 +41367,7 @@ "SnapshotCopyGrantName": "The name of the snapshot copy grant.", "SnapshotCopyManual": "Indicates whether to apply the snapshot retention period to newly copied manual snapshots instead of automated snapshots.", "SnapshotCopyRetentionPeriod": "The number of days to retain automated snapshots in the destination AWS Region after they are copied from the source AWS Region .\n\nBy default, this only changes the retention period of copied automated snapshots.\n\nIf you decrease the retention period for automated snapshots that are copied to a destination AWS Region , Amazon Redshift deletes any existing automated snapshots that were copied to the destination AWS Region and that fall outside of the new retention period.\n\nConstraints: Must be at least 1 and no more than 35 for automated snapshots.\n\nIf you specify the `manual` option, only newly copied manual snapshots will have the new retention period.\n\nIf you specify the value of -1 newly copied manual snapshots are retained indefinitely.\n\nConstraints: The number of days must be either -1 or an integer between 1 and 3,653 for manual snapshots.", - "SnapshotIdentifier": "The name of the snapshot from which to create the new cluster. This parameter isn't case sensitive. You can specify this parameter or `snapshotArn` , but not both.\n\nExample: `my-snapshot-id`", + "SnapshotIdentifier": "The name of the snapshot from which to create the new cluster. This parameter isn't case sensitive. You must specify this parameter or `snapshotArn` , but not both.\n\nExample: `my-snapshot-id`", "Tags": "A list of tag instances.", "VpcSecurityGroupIds": "A list of Virtual Private Cloud (VPC) security groups to be associated with the cluster.\n\nDefault: The default VPC security group is associated with the cluster." } @@ -41247,7 +41502,7 @@ }, "description": "Describes an endpoint authorization for authorizing Redshift-managed VPC endpoint access to a cluster across AWS accounts .", "properties": { - "Account": "The A AWS account ID of either the cluster owner (grantor) or grantee. If `Grantee` parameter is true, then the `Account` value is of the grantor.", + "Account": "The AWS account ID of either the cluster owner (grantor) or grantee. If `Grantee` parameter is true, then the `Account` value is of the grantor.", "ClusterIdentifier": "The cluster identifier.", "Force": "Indicates whether to force the revoke action. If true, the Redshift-managed VPC endpoints associated with the endpoint authorization are also deleted.", "VpcIds": "The virtual private cloud (VPC) identifiers to grant access to." @@ -41983,42 +42238,42 @@ "AWS::RolesAnywhere::CRL": { "attributes": { "CrlId": "The unique primary identifier of the Crl", - "Ref": "The name of the CRL." + "Ref": "`Ref` returns `CrlId` ." }, - "description": "Creates a Crl.", + "description": "The state of the certificate revocation list (CRL) after a read or write operation.", "properties": { - "CrlData": "x509 v3 Certificate Revocation List to revoke auth for corresponding certificates presented in CreateSession operations", - "Enabled": "The enabled status of the resource.", - "Name": "The customer specified name of the resource.", - "Tags": "A list of Tags.", + "CrlData": "The revocation record for a certificate, following the x509 v3 standard.", + "Enabled": "Indicates whether the certificate revocation list (CRL) is enabled.", + "Name": "The name of the certificate revocation list (CRL).", + "Tags": "A list of tags to attach to the CRL.", "TrustAnchorArn": "The ARN of the TrustAnchor the certificate revocation list (CRL) will provide revocation for." } }, "AWS::RolesAnywhere::Profile": { "attributes": { - "ProfileArn": "", + "ProfileArn": "The ARN of the profile.", "ProfileId": "The unique primary identifier of the Profile", - "Ref": "The name of the Profile" + "Ref": "`Ref` returns `ProfileId` ." }, - "description": "Creates a Profile.", + "description": "Creates a *profile* , a list of the roles that Roles Anywhere service is trusted to assume. You use profiles to intersect permissions with IAM managed policies.\n\n*Required permissions:* `rolesanywhere:CreateProfile` .", "properties": { - "DurationSeconds": "The number of seconds vended session credentials will be valid for", - "Enabled": "The enabled status of the resource.", - "ManagedPolicyArns": "A list of managed policy ARNs. Managed policies identified by this list will be applied to the vended session credentials.", - "Name": "The customer specified name of the resource.", - "RequireInstanceProperties": "Specifies whether instance properties are required in CreateSession requests with this profile.", - "RoleArns": "A list of IAM role ARNs that can be assumed when this profile is specified in a CreateSession request.", - "SessionPolicy": "A session policy that will applied to the trust boundary of the vended session credentials.", - "Tags": "A list of Tags." + "DurationSeconds": "Sets the maximum number of seconds that vended temporary credentials through [CreateSession](https://docs.aws.amazon.com/rolesanywhere/latest/userguide/authentication-create-session.html) will be valid for, between 900 and 3600.", + "Enabled": "Indicates whether the profile is enabled.", + "ManagedPolicyArns": "A list of managed policy ARNs that apply to the vended session credentials.", + "Name": "The name of the profile.", + "RequireInstanceProperties": "Specifies whether instance properties are required in temporary credential requests with this profile.", + "RoleArns": "A list of IAM role ARNs. During `CreateSession` , if a matching role ARN is provided, the properties in this profile will be applied to the intersection session policy.", + "SessionPolicy": "A session policy that applies to the trust boundary of the vended session credentials.", + "Tags": "A list of tags to attach to the profile." } }, "AWS::RolesAnywhere::TrustAnchor": { "attributes": { "Ref": "`Ref` returns `TrustAnchorId` .", "TrustAnchorArn": "The ARN of the trust anchor.", - "TrustAnchorId": "" + "TrustAnchorId": "The unique primary identifier of the TrustAnchor" }, - "description": "Creates a TrustAnchor.", + "description": "The state of the trust anchor after a read or write operation.", "properties": { "Enabled": "Indicates whether the trust anchor is enabled.", "Name": "The name of the trust anchor.", @@ -42028,15 +42283,15 @@ }, "AWS::RolesAnywhere::TrustAnchor.Source": { "attributes": {}, - "description": "Object representing the TrustAnchor type and its related certificate data.", + "description": "The trust anchor type and its related certificate data.", "properties": { - "SourceData": "A union object representing the data field of the TrustAnchor depending on its type", - "SourceType": "The type of the TrustAnchor." + "SourceData": "The data field of the trust anchor depending on its type.", + "SourceType": "The type of the trust anchor." } }, "AWS::RolesAnywhere::TrustAnchor.SourceData": { "attributes": {}, - "description": "A union object representing the data field of the TrustAnchor depending on its type", + "description": "The data field of the trust anchor depending on its type.", "properties": { "AcmPcaArn": "The root certificate of the AWS Private Certificate Authority specified by this ARN is used in trust validation for temporary credential requests. Included for trust anchors of type `AWS_ACM_PCA` .", "X509CertificateData": "The PEM-encoded data for the certificate anchor. Included for trust anchors of type `CERTIFICATE_BUNDLE` ." @@ -45090,7 +45345,7 @@ "SingleSignOnManagedApplicationInstanceId": "The IAM Identity Center managed application instance ID.", "Url": "The URL for the Domain." }, - "description": "Creates a `Domain` used by Amazon SageMaker Studio. A domain consists of an associated Amazon Elastic File System (EFS) volume, a list of authorized users, and a variety of security, application, policy, and Amazon Virtual Private Cloud (VPC) configurations. An AWS account is limited to one domain per region. Users within a domain can share notebook files and other artifacts with each other.\n\n*EFS storage*\n\nWhen a domain is created, an EFS volume is created for use by all of the users within the domain. Each user receives a private home directory within the EFS volume for notebooks, Git repositories, and data files.\n\nSageMaker uses the AWS Key Management Service ( AWS KMS) to encrypt the EFS volume attached to the domain with an AWS managed key by default. For more control, you can specify a customer managed key. For more information, see [Protect Data at Rest Using Encryption](https://docs.aws.amazon.com/sagemaker/latest/dg/encryption-at-rest.html) .\n\n*VPC configuration*\n\nAll SageMaker Studio traffic between the domain and the EFS volume is through the specified VPC and subnets. For other Studio traffic, you can specify the `AppNetworkAccessType` parameter. `AppNetworkAccessType` corresponds to the network access type that you choose when you onboard to Studio. The following options are available:\n\n- `PublicInternetOnly` - Non-EFS traffic goes through a VPC managed by Amazon SageMaker, which allows internet access. This is the default value.\n- `VpcOnly` - All Studio traffic is through the specified VPC and subnets. Internet access is disabled by default. To allow internet access, you must specify a NAT gateway.\n\nWhen internet access is disabled, you won't be able to run a Studio notebook or to train or host models unless your VPC has an interface endpoint to the SageMaker API and runtime or a NAT gateway and your security groups allow outbound connections.\n\n> NFS traffic over TCP on port 2049 needs to be allowed in both inbound and outbound rules in order to launch a SageMaker Studio app successfully. \n\nFor more information, see [Connect SageMaker Studio Notebooks to Resources in a VPC](https://docs.aws.amazon.com/sagemaker/latest/dg/studio-notebooks-and-internet-access.html) .", + "description": "Creates a `Domain` used by Amazon SageMaker Studio. A domain consists of an associated Amazon Elastic File System (EFS) volume, a list of authorized users, and a variety of security, application, policy, and Amazon Virtual Private Cloud (VPC) configurations. Users within a domain can share notebook files and other artifacts with each other.\n\n*EFS storage*\n\nWhen a domain is created, an EFS volume is created for use by all of the users within the domain. Each user receives a private home directory within the EFS volume for notebooks, Git repositories, and data files.\n\nSageMaker uses the AWS Key Management Service ( AWS KMS) to encrypt the EFS volume attached to the domain with an AWS managed key by default. For more control, you can specify a customer managed key. For more information, see [Protect Data at Rest Using Encryption](https://docs.aws.amazon.com/sagemaker/latest/dg/encryption-at-rest.html) .\n\n*VPC configuration*\n\nAll SageMaker Studio traffic between the domain and the EFS volume is through the specified VPC and subnets. For other Studio traffic, you can specify the `AppNetworkAccessType` parameter. `AppNetworkAccessType` corresponds to the network access type that you choose when you onboard to Studio. The following options are available:\n\n- `PublicInternetOnly` - Non-EFS traffic goes through a VPC managed by Amazon SageMaker, which allows internet access. This is the default value.\n- `VpcOnly` - All Studio traffic is through the specified VPC and subnets. Internet access is disabled by default. To allow internet access, you must specify a NAT gateway.\n\nWhen internet access is disabled, you won't be able to run a Studio notebook or to train or host models unless your VPC has an interface endpoint to the SageMaker API and runtime or a NAT gateway and your security groups allow outbound connections.\n\n> NFS traffic over TCP on port 2049 needs to be allowed in both inbound and outbound rules in order to launch a SageMaker Studio app successfully. \n\nFor more information, see [Connect SageMaker Studio Notebooks to Resources in a VPC](https://docs.aws.amazon.com/sagemaker/latest/dg/studio-notebooks-and-internet-access.html) .", "properties": { "AppNetworkAccessType": "Specifies the VPC used for non-EFS traffic. The default value is `PublicInternetOnly` .\n\n- `PublicInternetOnly` - Non-EFS traffic is through a VPC managed by Amazon SageMaker , which allows direct internet access\n- `VpcOnly` - All Studio traffic is through the specified VPC and subnets\n\n*Valid Values* : `PublicInternetOnly | VpcOnly`", "AppSecurityGroupManagement": "The entity that creates and manages the required security groups for inter-app communication in `VpcOnly` mode. Required when `CreateDomain.AppNetworkAccessType` is `VpcOnly` and `DomainSettings.RStudioServerProDomainSettings.DomainExecutionRoleArn` is provided.", @@ -45489,7 +45744,7 @@ "DataCatalogConfig": "The meta data of the Glue table that is autogenerated when an `OfflineStore` is created.", "DisableGlueTableCreation": "Set to `True` to disable the automatic creation of an AWS Glue table when configuring an `OfflineStore` .", "S3StorageConfig": "The Amazon Simple Storage (Amazon S3) location of `OfflineStore` .", - "TableFormat": "Format for the offline store table. Supported formats are Glue (Default) and [Apache Iceberg](https://docs.aws.amazon.com/https://iceberg.apache.org/) ." + "TableFormat": "" } }, "AWS::SageMaker::FeatureGroup.OnlineStoreConfig": { @@ -45504,7 +45759,7 @@ "attributes": {}, "description": "The security configuration for `OnlineStore` .", "properties": { - "KmsKeyId": "The ID of the AWS Key Management Service ( AWS KMS) key that SageMaker Feature Store uses to encrypt the Amazon S3 objects at rest using Amazon S3 server-side encryption.\n\nThe caller (either IAM user or IAM role) of `CreateFeatureGroup` must have below permissions to the `OnlineStore` `KmsKeyId` :\n\n- `\"kms:Encrypt\"`\n- `\"kms:Decrypt\"`\n- `\"kms:DescribeKey\"`\n- `\"kms:CreateGrant\"`\n- `\"kms:RetireGrant\"`\n- `\"kms:ReEncryptFrom\"`\n- `\"kms:ReEncryptTo\"`\n- `\"kms:GenerateDataKey\"`\n- `\"kms:ListAliases\"`\n- `\"kms:ListGrants\"`\n- `\"kms:RevokeGrant\"`\n\nThe caller (either IAM user or IAM role) to all DataPlane operations ( `PutRecord` , `GetRecord` , `DeleteRecord` ) must have the following permissions to the `KmsKeyId` :\n\n- `\"kms:Decrypt\"`" + "KmsKeyId": "The ID of the AWS Key Management Service ( AWS KMS) key that SageMaker Feature Store uses to encrypt the Amazon S3 objects at rest using Amazon S3 server-side encryption.\n\nThe caller (either IAM user or IAM role) of `CreateFeatureGroup` must have below permissions to the `OnlineStore` `KmsKeyId` :\n\n- `\"kms:Encrypt\"`\n- `\"kms:Decrypt\"`\n- `\"kms:DescribeKey\"`\n- `\"kms:CreateGrant\"`\n- `\"kms:RetireGrant\"`\n- `\"kms:ReEncryptFrom\"`\n- `\"kms:ReEncryptTo\"`\n- `\"kms:GenerateDataKey\"`\n- `\"kms:ListAliases\"`\n- `\"kms:ListGrants\"`\n- `\"kms:RevokeGrant\"`\n\nThe caller (either user or IAM role) to all DataPlane operations ( `PutRecord` , `GetRecord` , `DeleteRecord` ) must have the following permissions to the `KmsKeyId` :\n\n- `\"kms:Decrypt\"`" } }, "AWS::SageMaker::FeatureGroup.S3StorageConfig": { @@ -45570,7 +45825,7 @@ "ImageConfig": "Specifies whether the model container is in Amazon ECR or a private Docker registry accessible from your Amazon Virtual Private Cloud (VPC). For information about storing containers in a private Docker registry, see [Use a Private Docker Registry for Real-Time Inference Containers](https://docs.aws.amazon.com/sagemaker/latest/dg/your-algorithms-containers-inference-private.html)", "InferenceSpecificationName": "The inference specification name in the model package version.", "Mode": "Whether the container hosts a single model or multiple models.", - "ModelDataUrl": "The S3 path where the model artifacts, which result from model training, are stored. This path must point to a single gzip compressed tar archive (.tar.gz suffix). The S3 path is required for SageMaker built-in algorithms, but not if you use your own algorithms. For more information on built-in algorithms, see [Common Parameters](https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-algo-docker-registry-paths.html) .\n\n> The model artifacts must be in an S3 bucket that is in the same region as the model or endpoint you are creating. \n\nIf you provide a value for this parameter, SageMaker uses AWS Security Token Service to download model artifacts from the S3 path you provide. AWS STS is activated in your IAM user account by default. If you previously deactivated AWS STS for a region, you need to reactivate AWS STS for that region. For more information, see [Activating and Deactivating AWS STS in an AWS Region](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_enable-regions.html) in the *AWS Identity and Access Management User Guide* .\n\n> If you use a built-in algorithm to create a model, SageMaker requires that you provide a S3 path to the model artifacts in `ModelDataUrl` .", + "ModelDataUrl": "The S3 path where the model artifacts, which result from model training, are stored. This path must point to a single gzip compressed tar archive (.tar.gz suffix). The S3 path is required for SageMaker built-in algorithms, but not if you use your own algorithms. For more information on built-in algorithms, see [Common Parameters](https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-algo-docker-registry-paths.html) .\n\n> The model artifacts must be in an S3 bucket that is in the same region as the model or endpoint you are creating. \n\nIf you provide a value for this parameter, SageMaker uses AWS Security Token Service to download model artifacts from the S3 path you provide. AWS STS is activated in your AWS account by default. If you previously deactivated AWS STS for a region, you need to reactivate AWS STS for that region. For more information, see [Activating and Deactivating AWS STS in an AWS Region](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_enable-regions.html) in the *AWS Identity and Access Management User Guide* .\n\n> If you use a built-in algorithm to create a model, SageMaker requires that you provide a S3 path to the model artifacts in `ModelDataUrl` .", "ModelPackageName": "The name or Amazon Resource Name (ARN) of the model package to use to create the model.", "MultiModelConfig": "Specifies additional configuration for multi-model endpoints." } @@ -45794,6 +46049,174 @@ "Subnets": "The ID of the subnets in the VPC to which you want to connect your training job or model. For information about the availability of specific instance types, see [Supported Instance Types and Availability Zones](https://docs.aws.amazon.com/sagemaker/latest/dg/instance-types-az.html) ." } }, + "AWS::SageMaker::ModelCard": { + "attributes": { + "ModelCardArn": "The Amazon Resource Number (ARN) of the model card. For example, `arn:aws:sagemaker:us-west-2:012345678901:modelcard/examplemodelcard` .", + "Ref": "`Ref` returns the model card name.\n\nFor more information about using the Ref function, see [Ref](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) ." + }, + "description": "Creates an Amazon SageMaker Model Card.\n\nFor information about how to use model cards, see [Amazon SageMaker Model Card](https://docs.aws.amazon.com/sagemaker/latest/dg/model-cards.html) .", + "properties": { + "Content": "The content of the model card. Content uses the [model card JSON schema](https://docs.aws.amazon.com/sagemaker/latest/dg/model-cards.html#model-cards-json-schema) .", + "ModelCardName": "The unique name of the model card.", + "ModelCardStatus": "The approval status of the model card within your organization. Different organizations might have different criteria for model card review and approval.\n\n- `Draft` : The model card is a work in progress.\n- `PendingReview` : The model card is pending review.\n- `Approved` : The model card is approved.\n- `Archived` : The model card is archived. No more updates should be made to the model card, but it can still be exported.", + "SecurityConfig": "The security configuration used to protect model card data.", + "Tags": "Key-value pairs used to manage metadata for the model card." + } + }, + "AWS::SageMaker::ModelCard.AdditionalInformation": { + "attributes": {}, + "description": "Additional information about the model.", + "properties": { + "CaveatsAndRecommendations": "Caveats and recommendations for those who might use this model in their applications.", + "CustomDetails": "Any additional information to document about the model.", + "EthicalConsiderations": "Any ethical considerations documented by the model card author." + } + }, + "AWS::SageMaker::ModelCard.BusinessDetails": { + "attributes": {}, + "description": "Information about how the model supports business goals.", + "properties": { + "BusinessProblem": "The specific business problem that the model is trying to solve.", + "BusinessStakeholders": "The relevant stakeholders for the model.", + "LineOfBusiness": "The broader business need that the model is serving." + } + }, + "AWS::SageMaker::ModelCard.Content": { + "attributes": {}, + "description": "The content of the model card. It follows the [model card json schema](https://docs.aws.amazon.com/sagemaker/latest/dg/model-cards.html#model-cards-json-schema) .", + "properties": { + "AdditionalInformation": "Additional information about the model.", + "BusinessDetails": "Information about how the model supports business goals.", + "EvaluationDetails": "An overview about the model's evaluation.", + "IntendedUses": "The intended usage of the model.", + "ModelOverview": "An overview about the model", + "TrainingDetails": "An overview about model training." + } + }, + "AWS::SageMaker::ModelCard.EvaluationDetail": { + "attributes": {}, + "description": "The evaluation details of the model.", + "properties": { + "Datasets": "The location of the datasets used to evaluate the model.", + "EvaluationJobArn": "The Amazon Resource Name (ARN) of the evaluation job.", + "EvaluationObservation": "Any observations made during the model evaluation.", + "Metadata": "Additional attributes associated with the evaluation results.", + "MetricGroups": "An evaluation Metric Group object.", + "Name": "The evaluation job name." + } + }, + "AWS::SageMaker::ModelCard.Function": { + "attributes": {}, + "description": "Function details.", + "properties": { + "Condition": "An optional description of any conditions of your objective function metric.", + "Facet": "The metric of the model's objective function. For example, *loss* or *rmse* . The following list shows examples of the values that you can specify for the metric:\n\n- `ACCURACY`\n- `AUC`\n- `LOSS`\n- `MAE`\n- `RMSE`", + "Function": "The optimization direction of the model's objective function. You must specify one of the following values:\n\n- `Maximize`\n- `Minimize`" + } + }, + "AWS::SageMaker::ModelCard.InferenceEnvironment": { + "attributes": {}, + "description": "An overview of a model's inference environment.", + "properties": { + "ContainerImage": "The container used to run the inference environment." + } + }, + "AWS::SageMaker::ModelCard.IntendedUses": { + "attributes": {}, + "description": "The intended uses of a model.", + "properties": { + "ExplanationsForRiskRating": "An explanation of why your organization categorizes the model with its risk rating.", + "FactorsAffectingModelEfficiency": "Factors affecting model efficacy.", + "IntendedUses": "The intended use cases for the model.", + "PurposeOfModel": "The general purpose of the model.", + "RiskRating": "Your organization's risk rating. You can specify one the following values as the risk rating:\n\n- High\n- Medium\n- Low\n- Unknown" + } + }, + "AWS::SageMaker::ModelCard.MetricGroup": { + "attributes": {}, + "description": "A group of metric data that you use to initialize a metric group object.", + "properties": { + "MetricData": "A list of metric objects. The `MetricDataItems` list can have one of the following values:\n\n- `bar_chart_metric`\n- `matrix_metric`\n- `simple_metric`\n- `linear_graph_metric`\n\nFor more information about the metric schema, see the definition section of the [model card JSON schema](https://docs.aws.amazon.com/sagemaker/latest/dg/model-cards.html#model-cards-json-schema) .", + "Name": "The metric group name." + } + }, + "AWS::SageMaker::ModelCard.ModelOverview": { + "attributes": {}, + "description": "An overview about the model.", + "properties": { + "AlgorithmType": "The algorithm used to solve the problem.", + "InferenceEnvironment": "An overview about model inference.", + "ModelArtifact": "The location of the model artifact.", + "ModelCreator": "The creator of the model.", + "ModelDescription": "A description of the model.", + "ModelId": "The SageMaker Model ARN or non- SageMaker Model ID.", + "ModelName": "The name of the model.", + "ModelOwner": "The owner of the model.", + "ModelVersion": "The version of the model.", + "ProblemType": "The problem being solved with the model." + } + }, + "AWS::SageMaker::ModelCard.ObjectiveFunction": { + "attributes": {}, + "description": "The function that is optimized during model training.", + "properties": { + "Function": "A function object that details optimization direction, metric, and additional descriptions.", + "Notes": "Notes about the object function, including other considerations for possible objective functions." + } + }, + "AWS::SageMaker::ModelCard.SecurityConfig": { + "attributes": {}, + "description": "The security configuration used to protect model card data.", + "properties": { + "KmsKeyId": "A AWS Key Management Service [key ID](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#key-id-key-id) used to encrypt a model card." + } + }, + "AWS::SageMaker::ModelCard.TrainingDetails": { + "attributes": {}, + "description": "The training details of the model", + "properties": { + "ObjectiveFunction": "The function that is optimized during model training.", + "TrainingJobDetails": "Details about any associated training jobs.", + "TrainingObservations": "Any observations about training." + } + }, + "AWS::SageMaker::ModelCard.TrainingEnvironment": { + "attributes": {}, + "description": "SageMaker training image.", + "properties": { + "ContainerImage": "SageMaker inference image URI." + } + }, + "AWS::SageMaker::ModelCard.TrainingHyperParameter": { + "attributes": {}, + "description": "A hyper parameter that was configured in training the model.", + "properties": { + "Name": "The name of the hyper parameter.", + "Value": "The value specified for the hyper parameter." + } + }, + "AWS::SageMaker::ModelCard.TrainingJobDetails": { + "attributes": {}, + "description": "The overview of a training job.", + "properties": { + "HyperParameters": "The hyper parameters used in the training job.", + "TrainingArn": "The SageMaker training job Amazon Resource Name (ARN)", + "TrainingDatasets": "The location of the datasets used to train the model.", + "TrainingEnvironment": "The SageMaker training job image URI.", + "TrainingMetrics": "The SageMaker training job results.", + "UserProvidedHyperParameters": "Additional hyper parameters that you've specified when training the model.", + "UserProvidedTrainingMetrics": "Custom training job results." + } + }, + "AWS::SageMaker::ModelCard.TrainingMetric": { + "attributes": {}, + "description": "A result from a SageMaker training job.", + "properties": { + "Name": "The name of the result from the SageMaker training job.", + "Notes": "Any additional notes describing the result of the training job.", + "Value": "The value of a result from the SageMaker training job." + } + }, "AWS::SageMaker::ModelExplainabilityJobDefinition": { "attributes": { "CreationTime": "The time when the job definition was created.", @@ -47133,18 +47556,18 @@ "attributes": { "Ref": "When you pass the logical ID of an `AWS::SecretsManager::RotationSchedule` resource to the intrinsic `Ref` function, the function returns the ARN of the secret being configured, such as:\n\n*arn:aws:secretsmanager: us-west-2* : *123456789012* :secret: *my-path/my-secret-name* - *1a2b3c*\n\nYou can use the ARN to reference a secret you create in one part of the stack template from within the definition of another resource later, in the same template. You typically do this when you define the [AWS::SecretsManager::SecretTargetAttachment](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-secrettargetattachment.html) resource type.\n\nFor more information about using the `Ref` function, see [Ref](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) ." }, - "description": "Sets the rotation schedule and Lambda rotation function for a secret. For more information, see [How rotation works](https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotate-secrets_how.html) . For the rotation function, you have two options:\n\n- You can create a new rotation function based on one of the [Secrets Manager rotation function templates](https://docs.aws.amazon.com/secretsmanager/latest/userguide/reference_available-rotation-templates.html) by using `HostedRotationLambda` .\n- You can choose an existing rotation function by using `RotationLambdaARN` .\n\nFor Amazon RDS , Amazon Redshift , Amazon DocumentDB secrets, if you define both the secret and the database or service in the AWS CloudFormation template, then you need to define the [AWS::SecretsManager::SecretTargetAttachment](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-secrettargetattachment.html) resource to populate the secret with the connection details of the database or service before you attempt to configure rotation.", + "description": "Sets the rotation schedule and Lambda rotation function for a secret. For more information, see [How rotation works](https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotate-secrets_how.html) .\n\nFor Amazon RDS master user credentials, see [AWS::RDS::DBCluster MasterUserSecret](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-dbcluster-masterusersecret.html) .\n\nFor the rotation function, you have two options:\n\n- You can create a new rotation function based on one of the [Secrets Manager rotation function templates](https://docs.aws.amazon.com/secretsmanager/latest/userguide/reference_available-rotation-templates.html) by using `HostedRotationLambda` .\n- You can choose an existing rotation function by using `RotationLambdaARN` .\n\nFor database secrets, if you define both the secret and the database or service in the AWS CloudFormation template, then you need to define the [AWS::SecretsManager::SecretTargetAttachment](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-secrettargetattachment.html) resource to populate the secret with the connection details of the database or service before you attempt to configure rotation.", "properties": { - "HostedRotationLambda": "Creates a new Lambda rotation function based on one of the [Secrets Manager rotation function templates](https://docs.aws.amazon.com/secretsmanager/latest/userguide/reference_available-rotation-templates.html) . To use a rotation function that already exists, specify `RotationLambdaARN` instead.", + "HostedRotationLambda": "Creates a new Lambda rotation function based on one of the [Secrets Manager rotation function templates](https://docs.aws.amazon.com/secretsmanager/latest/userguide/reference_available-rotation-templates.html) . To use a rotation function that already exists, specify `RotationLambdaARN` instead.\n\nFor Amazon RDS master user credentials, see [AWS::RDS::DBCluster MasterUserSecret](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-dbcluster-masterusersecret.html) .", "RotateImmediatelyOnUpdate": "Specifies whether to rotate the secret immediately or wait until the next scheduled rotation window. The rotation schedule is defined in `RotationRules` .\n\nIf you don't immediately rotate the secret, Secrets Manager tests the rotation configuration by running the [`testSecret` step](https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotate-secrets_how.html) of the Lambda rotation function. The test creates an `AWSPENDING` version of the secret and then removes it.\n\nIf you don't specify this value, then by default, Secrets Manager rotates the secret immediately.\n\nRotation is an asynchronous process. For more information, see [How rotation works](https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotate-secrets_how.html) .", - "RotationLambdaARN": "The ARN of an existing Lambda rotation function. To specify a rotation function that is also defined in this template, use the [Ref](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) function.\n\nTo create a new rotation function based on one of the [Secrets Manager rotation function templates](https://docs.aws.amazon.com/secretsmanager/latest/userguide/reference_available-rotation-templates.html) , specify `HostedRotationLambda` instead.", + "RotationLambdaARN": "The ARN of an existing Lambda rotation function. To specify a rotation function that is also defined in this template, use the [Ref](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) function.\n\nFor Amazon RDS master user credentials, see [AWS::RDS::DBCluster MasterUserSecret](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-dbcluster-masterusersecret.html) .\n\nTo create a new rotation function based on one of the [Secrets Manager rotation function templates](https://docs.aws.amazon.com/secretsmanager/latest/userguide/reference_available-rotation-templates.html) , specify `HostedRotationLambda` instead.", "RotationRules": "A structure that defines the rotation configuration for this secret.", "SecretId": "The ARN or name of the secret to rotate.\n\nTo reference a secret also created in this template, use the [Ref](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) function with the secret's logical ID." } }, "AWS::SecretsManager::RotationSchedule.HostedRotationLambda": { "attributes": {}, - "description": "Creates a new Lambda rotation function based on one of the [Secrets Manager rotation function templates](https://docs.aws.amazon.com/secretsmanager/latest/userguide/reference_available-rotation-templates.html) .\n\nYou must specify `Transform: AWS::SecretsManager-2020-07-23` at the beginning of the CloudFormation template.", + "description": "Creates a new Lambda rotation function based on one of the [Secrets Manager rotation function templates](https://docs.aws.amazon.com/secretsmanager/latest/userguide/reference_available-rotation-templates.html) .\n\nYou must specify `Transform: AWS::SecretsManager-2020-07-23` at the beginning of the CloudFormation template.\n\nFor Amazon RDS master user credentials, see [AWS::RDS::DBCluster MasterUserSecret](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-dbcluster-masterusersecret.html) .", "properties": { "ExcludeCharacters": "A string of the characters that you don't want in the password.", "KmsKeyArn": "The ARN of the KMS key that Secrets Manager uses to encrypt the secret. If you don't specify this value, then Secrets Manager uses the key `aws/secretsmanager` . If `aws/secretsmanager` doesn't yet exist, then Secrets Manager creates it for you automatically the first time it encrypts the secret value.", @@ -47171,14 +47594,14 @@ "attributes": { "Ref": "When you pass the logical ID of an `AWS::SecretsManager::Secret` resource to the intrinsic `Ref` function, the function returns the ARN of the secret configured such as:\n\n`arn:aws:secretsmanager:us-west-2:123456789012:secret:my-path/my-secret-name-1a2b3c`\n\nIf you know the ARN of a secret, you can reference a secret you created in one part of the stack template from within the definition of another resource in the same template. You typically use the `Ref` function with the [AWS::SecretsManager::SecretTargetAttachment](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-secrettargetattachment.html) resource type to get references to both the secret and its associated database.\n\nFor more information about using the `Ref` function, see [Ref](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) ." }, - "description": "Creates a new secret. A *secret* can be a password, a set of credentials such as a user name and password, an OAuth token, or other secret information that you store in an encrypted form in Secrets Manager.\n\nTo retrieve a secret in a CloudFormation template, use a *dynamic reference* . For more information, see [Retrieve a secret in an AWS CloudFormation resource](https://docs.aws.amazon.com/secretsmanager/latest/userguide/cfn-example_reference-secret.html) .\n\nA common scenario is to first create a secret with `GenerateSecretString` , which generates a password, and then use a dynamic reference to retrieve the username and password from the secret to use as credentials for a new database. Follow these steps, as shown in the examples below:\n\n- Define the secret without referencing the service or database. You can't reference the service or database because it doesn't exist yet. The secret must contain a username and password.\n- Next, define the service or database. Include the reference to the secret to use stored credentials to define the database admin user and password.\n- Finally, define a `SecretTargetAttachment` resource type to finish configuring the secret with the required database engine type and the connection details of the service or database. The rotation function requires the details, if you attach one later by defining a [AWS::SecretsManager::RotationSchedule](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-rotationschedule.html) resource type.\n\nFor information about creating a secret in the console, see [Create a secret](https://docs.aws.amazon.com/secretsmanager/latest/userguide/manage_create-basic-secret.html) . For information about creating a secret using the CLI or SDK, see [CreateSecret](https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_CreateSecret.html) .\n\nFor information about retrieving a secret in code, see [Retrieve secrets from Secrets Manager](https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets.html) .\n\n> Do not create a dynamic reference using a backslash `(\\)` as the final value. AWS CloudFormation cannot resolve those references, which causes a resource failure.", + "description": "Creates a new secret. A *secret* can be a password, a set of credentials such as a user name and password, an OAuth token, or other secret information that you store in an encrypted form in Secrets Manager.\n\nFor Amazon RDS master user credentials, see [AWS::RDS::DBCluster MasterUserSecret](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-dbcluster-masterusersecret.html) .\n\nTo retrieve a secret in a CloudFormation template, use a *dynamic reference* . For more information, see [Retrieve a secret in an AWS CloudFormation resource](https://docs.aws.amazon.com/secretsmanager/latest/userguide/cfn-example_reference-secret.html) .\n\nA common scenario is to first create a secret with `GenerateSecretString` , which generates a password, and then use a dynamic reference to retrieve the username and password from the secret to use as credentials for a new database. Follow these steps, as shown in the examples below:\n\n- Define the secret without referencing the service or database. You can't reference the service or database because it doesn't exist yet. The secret must contain a username and password.\n- Next, define the service or database. Include the reference to the secret to use stored credentials to define the database admin user and password.\n- Finally, define a `SecretTargetAttachment` resource type to finish configuring the secret with the required database engine type and the connection details of the service or database. The rotation function requires the details, if you attach one later by defining a [AWS::SecretsManager::RotationSchedule](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-rotationschedule.html) resource type.\n\nFor information about creating a secret in the console, see [Create a secret](https://docs.aws.amazon.com/secretsmanager/latest/userguide/manage_create-basic-secret.html) . For information about creating a secret using the CLI or SDK, see [CreateSecret](https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_CreateSecret.html) .\n\nFor information about retrieving a secret in code, see [Retrieve secrets from Secrets Manager](https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets.html) .\n\n> Do not create a dynamic reference using a backslash `(\\)` as the final value. AWS CloudFormation cannot resolve those references, which causes a resource failure.", "properties": { "Description": "The description of the secret.", - "GenerateSecretString": "A structure that specifies how to generate a password to encrypt and store in the secret. To include a specific string in the secret, use `SecretString` instead. If you omit both `GenerateSecretString` and `SecretString` , you create an empty secret.\n\nWe recommend that you specify the maximum length and include every character type that the system you are generating a password for can support.", + "GenerateSecretString": "A structure that specifies how to generate a password to encrypt and store in the secret. To include a specific string in the secret, use `SecretString` instead. If you omit both `GenerateSecretString` and `SecretString` , you create an empty secret. When you make a change to this property, a new secret version is created.\n\nWe recommend that you specify the maximum length and include every character type that the system you are generating a password for can support.", "KmsKeyId": "The ARN, key ID, or alias of the AWS KMS key that Secrets Manager uses to encrypt the secret value in the secret. An alias is always prefixed by `alias/` , for example `alias/aws/secretsmanager` . For more information, see [About aliases](https://docs.aws.amazon.com/kms/latest/developerguide/alias-about.html) .\n\nTo use a AWS KMS key in a different account, use the key ARN or the alias ARN.\n\nIf you don't specify this value, then Secrets Manager uses the key `aws/secretsmanager` . If that key doesn't yet exist, then Secrets Manager creates it for you automatically the first time it encrypts the secret value.\n\nIf the secret is in a different AWS account from the credentials calling the API, then you can't use `aws/secretsmanager` to encrypt the secret, and you must create and use a customer managed AWS KMS key.", "Name": "The name of the new secret.\n\nThe secret name can contain ASCII letters, numbers, and the following characters: /_+=.@-\n\nDo not end your secret name with a hyphen followed by six characters. If you do so, you risk confusion and unexpected results when searching for a secret by partial ARN. Secrets Manager automatically adds a hyphen and six random characters after the secret name at the end of the ARN.", "ReplicaRegions": "A custom type that specifies a `Region` and the `KmsKeyId` for a replica secret.", - "SecretString": "The text to encrypt and store in the secret. We recommend you use a JSON structure of key/value pairs for your secret value. To generate a random password, use `GenerateSecretString` instead. If you omit both `GenerateSecretString` and `SecretString` , you create an empty secret.", + "SecretString": "The text to encrypt and store in the secret. We recommend you use a JSON structure of key/value pairs for your secret value. To generate a random password, use `GenerateSecretString` instead. If you omit both `GenerateSecretString` and `SecretString` , you create an empty secret. When you make a change to this property, a new secret version is created.", "Tags": "A list of tags to attach to the secret. Each tag is a key and value pair of strings in a JSON text string, for example:\n\n`[{\"Key\":\"CostCenter\",\"Value\":\"12345\"},{\"Key\":\"environment\",\"Value\":\"production\"}]`\n\nSecrets Manager tag key names are case sensitive. A tag with the key \"ABC\" is a different tag from one with key \"abc\".\n\nIf you check tags in permissions policies as part of your security strategy, then adding or removing a tag can change permissions. If the completion of this operation would result in you losing your permissions for this secret, then Secrets Manager blocks the operation and returns an `Access Denied` error. For more information, see [Control access to secrets using tags](https://docs.aws.amazon.com/secretsmanager/latest/userguide/auth-and-access_examples.html#tag-secrets-abac) and [Limit access to identities with tags that match secrets' tags](https://docs.aws.amazon.com/secretsmanager/latest/userguide/auth-and-access_examples.html#auth-and-access_tags2) .\n\nFor information about how to format a JSON parameter for the various command line tool environments, see [Using JSON for Parameters](https://docs.aws.amazon.com/cli/latest/userguide/cli-using-param.html#cli-using-param-json) . If your command-line tool or SDK requires quotation marks around the parameter, you should use single quotes to avoid confusion with the double quotes required in the JSON text.\n\nThe following restrictions apply to tags:\n\n- Maximum number of tags per secret: 50\n- Maximum key length: 127 Unicode characters in UTF-8\n- Maximum value length: 255 Unicode characters in UTF-8\n- Tag keys and values are case sensitive.\n- Do not use the `aws:` prefix in your tag names or values because AWS reserves it for AWS use. You can't edit or delete tag names or values with this prefix. Tags with this prefix do not count against your tags per secret limit.\n- If you use your tagging schema across multiple services and resources, other services might have restrictions on allowed characters. Generally allowed characters: letters, spaces, and numbers representable in UTF-8, plus the following special characters: + - = . _ : / @." } }, @@ -47210,7 +47633,7 @@ "attributes": { "Ref": "When you pass the logical ID of an `AWS::SecretsManager::SecretTargetAttachment` resource to the intrinsic `Ref` function, the function returns the ARN of the secret, such as:\n\n`arn:aws:secretsmanager:us-west-2:123456789012:secret:my-path/my-secret-name-1a2b3c`\n\nYou can use the ARN to reference a secret you created in one part of the stack template from within the definition of another resource from a different part of the same template.\n\nFor more information about using the `Ref` function, see [Ref](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) ." }, - "description": "The `AWS::SecretsManager::SecretTargetAttachment` resource completes the final link between a Secrets Manager secret and the associated database by adding the database connection information to the secret JSON. If you want to turn on automatic rotation for a database credential secret, the secret must contain the database connection information. For more information, see [JSON structure of Secrets Manager database credential secrets](https://docs.aws.amazon.com/secretsmanager/latest/userguide/reference_secret_json_structure.html) .", + "description": "The `AWS::SecretsManager::SecretTargetAttachment` resource completes the final link between a Secrets Manager secret and the associated database by adding the database connection information to the secret JSON. If you want to turn on automatic rotation for a database credential secret, the secret must contain the database connection information. For more information, see [JSON structure of Secrets Manager database credential secrets](https://docs.aws.amazon.com/secretsmanager/latest/userguide/reference_secret_json_structure.html) .\n\nFor Amazon RDS master user credentials, see [AWS::RDS::DBCluster MasterUserSecret](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-dbcluster-masterusersecret.html) .", "properties": { "SecretId": "The ARN or name of the secret. To reference a secret also created in this template, use the see [Ref](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) function with the secret's logical ID.", "TargetId": "The ID of the database or cluster.", @@ -47264,7 +47687,7 @@ "properties": { "Description": "The description of the provisioning artifact, including how it differs from the previous provisioning artifact.", "DisableTemplateValidation": "If set to true, AWS Service Catalog stops validating the specified provisioning artifact even if it is invalid.", - "Info": "Specify the template source with one of the following options, but not both. Keys accepted: [ `LoadTemplateFromURL` , `ImportFromPhysicalId` ]\n\nThe URL of the AWS CloudFormation template in Amazon S3 or GitHub in JSON format. Specify the URL in JSON format as follows:\n\n`\"LoadTemplateFromURL\": \"https://s3.amazonaws.com/cf-templates-ozkq9d3hgiq2-us-east-1/...\"`\n\n`ImportFromPhysicalId` : The physical id of the resource that contains the template. Currently only supports AWS CloudFormation stack arn. Specify the physical id in JSON format as follows: `ImportFromPhysicalId: \u201carn:aws:cloudformation:[us-east-1]:[accountId]:stack/[StackName]/[resourceId]`", + "Info": "Specify the template source with one of the following options, but not both. Keys accepted: [ `LoadTemplateFromURL` , `ImportFromPhysicalId` ]\n\nThe URL of the AWS CloudFormation template in Amazon S3 in JSON format. Specify the URL in JSON format as follows:\n\n`\"LoadTemplateFromURL\": \"https://s3.amazonaws.com/cf-templates-ozkq9d3hgiq2-us-east-1/...\"`\n\n`ImportFromPhysicalId` : The physical id of the resource that contains the template. Currently only supports AWS CloudFormation stack arn. Specify the physical id in JSON format as follows: `ImportFromPhysicalId: \u201carn:aws:cloudformation:[us-east-1]:[accountId]:stack/[StackName]/[resourceId]`", "Name": "The name of the provisioning artifact (for example, v1 v2beta). No spaces are allowed." } }, @@ -47283,7 +47706,7 @@ "PathId": "The path identifier of the product. This value is optional if the product has a default path, and required if the product has more than one path. To list the paths for a product, use [ListLaunchPaths](https://docs.aws.amazon.com/servicecatalog/latest/dg/API_ListLaunchPaths.html) .\n\n> You must provide the name or ID, but not both.", "PathName": "The name of the path. This value is optional if the product has a default path, and required if the product has more than one path. To list the paths for a product, use [ListLaunchPaths](https://docs.aws.amazon.com/servicecatalog/latest/dg/API_ListLaunchPaths.html) .\n\n> You must provide the name or ID, but not both.", "ProductId": "The product identifier.\n\n> You must specify either the ID or the name of the product, but not both.", - "ProductName": "A user-friendly name for the provisioned product. This value must be unique for the AWS account and cannot be updated after the product is provisioned.\n\nEach time a stack is created or updated, if `ProductName` is provided it will successfully resolve to `ProductId` as long as only one product exists in the account or Region with that `ProductName` .\n\n> You must specify either the name or the ID of the product, but not both.", + "ProductName": "The name of the Service Catalog product.\n\nEach time a stack is created or updated, if `ProductName` is provided it will successfully resolve to `ProductId` as long as only one product exists in the account or Region with that `ProductName` .\n\n> You must specify either the name or the ID of the product, but not both.", "ProvisionedProductName": "A user-friendly name for the provisioned product. This value must be unique for the AWS account and cannot be updated after the product is provisioned.", "ProvisioningArtifactId": "The identifier of the provisioning artifact (also known as a version).\n\n> You must specify either the ID or the name of the provisioning artifact, but not both.", "ProvisioningArtifactName": "The name of the provisioning artifact (also known as a version) for the product. This name must be unique for the product.\n\n> You must specify either the name or the ID of the provisioning artifact, but not both. You must also specify either the name or the ID of the product, but not both.", @@ -47718,6 +48141,26 @@ "Value": "The numerical value of the time unit for signature validity." } }, + "AWS::SimSpaceWeaver::Simulation": { + "attributes": { + "DescribePayload": "The JSON blob that the [DescribeSimulation](https://docs.aws.amazon.com/simspaceweaver/latest/APIReference/API_DescribeSimulation.html) action returns.", + "Ref": "`Ref` returns the name of the `Simulation` . For example, `MyTestSimulation_22-12-15_12_00_00` ." + }, + "description": "Use the `AWS::SimSpaceWeaver::Simulation` resource to specify a simulation that AWS CloudFormation starts in the AWS Cloud , in your AWS account . In the resource properties section of your template, provide the name of an existing IAM role configured with the proper permissions, and the name of an existing Amazon S3 bucket. Your account must have permissions to read the Amazon S3 bucket. The Amazon S3 bucket must contain a valid schema. The schema must refer to simulation assets that are already uploaded to the AWS Cloud . For more information, see the [detailed tutorial](https://docs.aws.amazon.com/simspaceweaver/latest/userguide/getting-started_detailed.html) in the *AWS SimSpace Weaver User Guide* .", + "properties": { + "Name": "The name of the simulation.", + "RoleArn": "The Amazon Resource Name (ARN) of the AWS Identity and Access Management ( IAM ) role that the simulation assumes to perform actions. For more information about ARNs, see [Amazon Resource Names (ARNs)](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) in the *AWS General Reference* . For more information about IAM roles, see [IAM roles](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html) in the *AWS Identity and Access Management User Guide* .", + "SchemaS3Location": "The location of the simulation schema in Amazon Simple Storage Service ( Amazon S3 ). For more information about Amazon S3 , see the [*Amazon Simple Storage Service User Guide*](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html) ." + } + }, + "AWS::SimSpaceWeaver::Simulation.S3Location": { + "attributes": {}, + "description": "A location in Amazon Simple Storage Service ( Amazon S3 ) where SimSpace Weaver stores simulation data, such as your app .zip files and schema file. For more information about Amazon S3 , see the [*Amazon Simple Storage Service User Guide*](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html) .", + "properties": { + "BucketName": "The name of an Amazon S3 bucket. For more information about buckets, see [Creating, configuring, and working with Amazon S3 buckets](https://docs.aws.amazon.com/AmazonS3/latest/userguide/creating-buckets-s3.html) in the *Amazon Simple Storage Service User Guide* .", + "ObjectKey": "The key name of an object in Amazon S3 . For more information about Amazon S3 objects and object keys, see [Uploading, downloading, and working with objects in Amazon S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/uploading-downloading-objects.html) in the *Amazon Simple Storage Service User Guide* ." + } + }, "AWS::StepFunctions::Activity": { "attributes": { "Arn": "", @@ -48294,7 +48737,7 @@ "Arn": "The Amazon Resource Name associated with the user, in the form `arn:aws:transfer:region: *account-id* :user/ *server-id* / *username*` .\n\nAn example of a user ARN is: `arn:aws:transfer:us-east-1:123456789012:user/user1` .", "Ref": "`Ref` returns the username, such as `transfer_user` .", "ServerId": "The ID of the server to which the user is attached.\n\nAn example `ServerId` is `s-01234567890abcdef` .", - "UserName": "A unique string that identifies a user account associated with a server.\n\nAn example `UserName` is `transfer-user-1` ." + "UserName": "A unique string that identifies a Transfer Family user account associated with a server.\n\nAn example `UserName` is `transfer-user-1` ." }, "description": "The `AWS::Transfer::User` resource creates a user and associates them with an existing server. You can only create and associate users with servers that have the `IdentityProviderType` set to `SERVICE_MANAGED` . Using parameters for `CreateUser` , you can specify the user name, set the home directory, store the user's public key, and assign the user's AWS Identity and Access Management (IAM) role. You can also optionally add a session policy, and assign metadata with tags that can be used to group and search for users.", "properties": { @@ -48329,7 +48772,7 @@ }, "AWS::Transfer::User.SshPublicKey": { "attributes": {}, - "description": "Provides information about the public Secure Shell (SSH) key that is associated with a user account for the specific file transfer protocol-enabled server (as identified by `ServerId` ). The information returned includes the date the key was imported, the public key contents, and the public key ID. A user can store more than one SSH public key associated with their user name on a specific server.\n\n*SshPublicKeyBody*\n\nSpecifies the content of the SSH public key as specified by the `PublicKeyId` .\n\nAWS Transfer Family accepts RSA, ECDSA, and ED25519 keys.\n\nType: String\n\nLength Constraints: Maximum length of 2048.\n\nRequired: Yes", + "description": "Provides information about the public Secure Shell (SSH) key that is associated with a Transfer Family user account for the specific file transfer protocol-enabled server (as identified by `ServerId` ). The information returned includes the date the key was imported, the public key contents, and the public key ID. A user can store more than one SSH public key associated with their user name on a specific server.\n\n*SshPublicKeyBody*\n\nSpecifies the content of the SSH public key as specified by the `PublicKeyId` .\n\nAWS Transfer Family accepts RSA, ECDSA, and ED25519 keys.\n\nType: String\n\nLength Constraints: Maximum length of 2048.\n\nRequired: Yes", "properties": {} }, "AWS::Transfer::Workflow": { @@ -49265,7 +49708,7 @@ }, "AWS::WAFv2::RuleGroup.RateBasedStatement": { "attributes": {}, - "description": "A rate-based rule tracks the rate of requests for each originating IP address, and triggers the rule action when the rate exceeds a limit that you specify on the number of requests in any 5-minute time span. You can use this to put a temporary block on requests from an IP address that is sending excessive requests.\n\nAWS WAF tracks and manages web requests separately for each instance of a rate-based rule that you use. For example, if you provide the same rate-based rule settings in two web ACLs, each of the two rule statements represents a separate instance of the rate-based rule and gets its own tracking and management by AWS WAF . If you define a rate-based rule inside a rule group, and then use that rule group in multiple places, each use creates a separate instance of the rate-based rule that gets its own tracking and management by AWS WAF .\n\nWhen the rule action triggers, AWS WAF blocks additional requests from the IP address until the request rate falls below the limit.\n\nYou can optionally nest another statement inside the rate-based statement, to narrow the scope of the rule so that it only counts requests that match the nested statement. For example, based on recent requests that you have seen from an attacker, you might create a rate-based rule with a nested AND rule statement that contains the following nested statements:\n\n- An IP match statement with an IP set that specified the address 192.0.2.44.\n- A string match statement that searches in the User-Agent header for the string BadBot.\n\nIn this rate-based rule, you also define a rate limit. For this example, the rate limit is 1,000. Requests that meet the criteria of both of the nested statements are counted. If the count exceeds 1,000 requests per five minutes, the rule action triggers. Requests that do not meet the criteria of both of the nested statements are not counted towards the rate limit and are not affected by this rule.\n\nYou cannot nest a `RateBasedStatement` inside another statement, for example inside a `NotStatement` or `OrStatement` . You can define a `RateBasedStatement` inside a web ACL and inside a rule group.", + "description": "A rate-based rule tracks the rate of requests for each originating IP address, and triggers the rule action when the rate exceeds a limit that you specify on the number of requests in any 5-minute time span. You can use this to put a temporary block on requests from an IP address that is sending excessive requests.\n\nAWS WAF tracks and manages web requests separately for each instance of a rate-based rule that you use. For example, if you provide the same rate-based rule settings in two web ACLs, each of the two rule statements represents a separate instance of the rate-based rule and gets its own tracking and management by AWS WAF . If you define a rate-based rule inside a rule group, and then use that rule group in multiple places, each use creates a separate instance of the rate-based rule that gets its own tracking and management by AWS WAF .\n\nWhen the rule action triggers, AWS WAF blocks additional requests from the IP address until the request rate falls below the limit.\n\nYou can optionally nest another statement inside the rate-based statement, to narrow the scope of the rule so that it only counts requests that match the nested statement. For example, based on recent requests that you have seen from an attacker, you might create a rate-based rule with a nested AND rule statement that contains the following nested statements:\n\n- An IP match statement with an IP set that specifies the address 192.0.2.44.\n- A string match statement that searches in the User-Agent header for the string BadBot.\n\nIn this rate-based rule, you also define a rate limit. For this example, the rate limit is 1,000. Requests that meet the criteria of both of the nested statements are counted. If the count exceeds 1,000 requests per five minutes, the rule action triggers. Requests that do not meet the criteria of both of the nested statements are not counted towards the rate limit and are not affected by this rule.\n\nYou cannot nest a `RateBasedStatement` inside another statement, for example inside a `NotStatement` or `OrStatement` . You can define a `RateBasedStatement` inside a web ACL and inside a rule group.", "properties": { "AggregateKeyType": "Setting that indicates how to aggregate the request counts. The options are the following:\n\n- IP - Aggregate the request counts on the IP address from the web request origin.\n- FORWARDED_IP - Aggregate the request counts on the first IP address in an HTTP header. If you use this, configure the `ForwardedIPConfig` , to specify the header to use.", "ForwardedIPConfig": "The configuration for inspecting IP addresses in an HTTP header that you specify, instead of using the IP address that's reported by the web request origin. Commonly, this is the X-Forwarded-For (XFF) header, but you can specify any header name.\n\n> If the specified header isn't present in the request, AWS WAF doesn't apply the rule to the web request at all. \n\nThis is required if `AggregateKeyType` is set to `FORWARDED_IP` .", @@ -49360,7 +49803,7 @@ "LabelMatchStatement": "A rule statement to match against labels that have been added to the web request by rules that have already run in the web ACL.\n\nThe label match statement provides the label or namespace string to search for. The label string can represent a part or all of the fully qualified label name that had been added to the web request. Fully qualified labels have a prefix, optional namespaces, and label name. The prefix identifies the rule group or web ACL context of the rule that added the label. If you do not provide the fully qualified name in your label match string, AWS WAF performs the search for labels that were added in the same context as the label match statement.", "NotStatement": "A logical rule statement used to negate the results of another rule statement. You provide one `Statement` within the `NotStatement` .", "OrStatement": "A logical rule statement used to combine other rule statements with OR logic. You provide more than one `Statement` within the `OrStatement` .", - "RateBasedStatement": "A rate-based rule tracks the rate of requests for each originating IP address, and triggers the rule action when the rate exceeds a limit that you specify on the number of requests in any 5-minute time span. You can use this to put a temporary block on requests from an IP address that is sending excessive requests.\n\nAWS WAF tracks and manages web requests separately for each instance of a rate-based rule that you use. For example, if you provide the same rate-based rule settings in two web ACLs, each of the two rule statements represents a separate instance of the rate-based rule and gets its own tracking and management by AWS WAF . If you define a rate-based rule inside a rule group, and then use that rule group in multiple places, each use creates a separate instance of the rate-based rule that gets its own tracking and management by AWS WAF .\n\nWhen the rule action triggers, AWS WAF blocks additional requests from the IP address until the request rate falls below the limit.\n\nYou can optionally nest another statement inside the rate-based statement, to narrow the scope of the rule so that it only counts requests that match the nested statement. For example, based on recent requests that you have seen from an attacker, you might create a rate-based rule with a nested AND rule statement that contains the following nested statements:\n\n- An IP match statement with an IP set that specified the address 192.0.2.44.\n- A string match statement that searches in the User-Agent header for the string BadBot.\n\nIn this rate-based rule, you also define a rate limit. For this example, the rate limit is 1,000. Requests that meet the criteria of both of the nested statements are counted. If the count exceeds 1,000 requests per five minutes, the rule action triggers. Requests that do not meet the criteria of both of the nested statements are not counted towards the rate limit and are not affected by this rule.\n\nYou cannot nest a `RateBasedStatement` inside another statement, for example inside a `NotStatement` or `OrStatement` . You can define a `RateBasedStatement` inside a web ACL and inside a rule group.", + "RateBasedStatement": "A rate-based rule tracks the rate of requests for each originating IP address, and triggers the rule action when the rate exceeds a limit that you specify on the number of requests in any 5-minute time span. You can use this to put a temporary block on requests from an IP address that is sending excessive requests.\n\nAWS WAF tracks and manages web requests separately for each instance of a rate-based rule that you use. For example, if you provide the same rate-based rule settings in two web ACLs, each of the two rule statements represents a separate instance of the rate-based rule and gets its own tracking and management by AWS WAF . If you define a rate-based rule inside a rule group, and then use that rule group in multiple places, each use creates a separate instance of the rate-based rule that gets its own tracking and management by AWS WAF .\n\nWhen the rule action triggers, AWS WAF blocks additional requests from the IP address until the request rate falls below the limit.\n\nYou can optionally nest another statement inside the rate-based statement, to narrow the scope of the rule so that it only counts requests that match the nested statement. For example, based on recent requests that you have seen from an attacker, you might create a rate-based rule with a nested AND rule statement that contains the following nested statements:\n\n- An IP match statement with an IP set that specifies the address 192.0.2.44.\n- A string match statement that searches in the User-Agent header for the string BadBot.\n\nIn this rate-based rule, you also define a rate limit. For this example, the rate limit is 1,000. Requests that meet the criteria of both of the nested statements are counted. If the count exceeds 1,000 requests per five minutes, the rule action triggers. Requests that do not meet the criteria of both of the nested statements are not counted towards the rate limit and are not affected by this rule.\n\nYou cannot nest a `RateBasedStatement` inside another statement, for example inside a `NotStatement` or `OrStatement` . You can define a `RateBasedStatement` inside a web ACL and inside a rule group.", "RegexMatchStatement": "A rule statement used to search web request components for a match against a single regular expression.", "RegexPatternSetReferenceStatement": "A rule statement used to search web request components for matches with regular expressions. To use this, create a `RegexPatternSet` that specifies the expressions that you want to detect, then use the ARN of that set in this statement. A web request matches the pattern set rule statement if the request component matches any of the patterns in the set.\n\nEach regex pattern set rule statement references a regex pattern set. You create and maintain the set independent of your rules. This allows you to use the single set in multiple rules. When you update the referenced set, AWS WAF automatically updates all rules that reference it.", "SizeConstraintStatement": "A rule statement that compares a number of bytes against the size of a request component, using a comparison operator, such as greater than (>) or less than (<). For example, you can use a size constraint statement to look for query strings that are longer than 100 bytes.\n\nIf you configure AWS WAF to inspect the request body, AWS WAF inspects only the first 8192 bytes (8 KB). If the request body for your web requests never exceeds 8192 bytes, you could use a size constraint statement to block requests that have a request body greater than 8192 bytes.\n\nIf you choose URI for the value of Part of the request to filter on, the slash (/) in the URI counts as one character. For example, the URI `/logo.jpg` is nine characters long.", @@ -49724,7 +50167,7 @@ }, "AWS::WAFv2::WebACL.RateBasedStatement": { "attributes": {}, - "description": "A rate-based rule tracks the rate of requests for each originating IP address, and triggers the rule action when the rate exceeds a limit that you specify on the number of requests in any 5-minute time span. You can use this to put a temporary block on requests from an IP address that is sending excessive requests.\n\nAWS WAF tracks and manages web requests separately for each instance of a rate-based rule that you use. For example, if you provide the same rate-based rule settings in two web ACLs, each of the two rule statements represents a separate instance of the rate-based rule and gets its own tracking and management by AWS WAF . If you define a rate-based rule inside a rule group, and then use that rule group in multiple places, each use creates a separate instance of the rate-based rule that gets its own tracking and management by AWS WAF .\n\nWhen the rule action triggers, AWS WAF blocks additional requests from the IP address until the request rate falls below the limit.\n\nYou can optionally nest another statement inside the rate-based statement, to narrow the scope of the rule so that it only counts requests that match the nested statement. For example, based on recent requests that you have seen from an attacker, you might create a rate-based rule with a nested AND rule statement that contains the following nested statements:\n\n- An IP match statement with an IP set that specified the address 192.0.2.44.\n- A string match statement that searches in the User-Agent header for the string BadBot.\n\nIn this rate-based rule, you also define a rate limit. For this example, the rate limit is 1,000. Requests that meet the criteria of both of the nested statements are counted. If the count exceeds 1,000 requests per five minutes, the rule action triggers. Requests that do not meet the criteria of both of the nested statements are not counted towards the rate limit and are not affected by this rule.\n\nYou cannot nest a `RateBasedStatement` inside another statement, for example inside a `NotStatement` or `OrStatement` . You can define a `RateBasedStatement` inside a web ACL and inside a rule group.", + "description": "A rate-based rule tracks the rate of requests for each originating IP address, and triggers the rule action when the rate exceeds a limit that you specify on the number of requests in any 5-minute time span. You can use this to put a temporary block on requests from an IP address that is sending excessive requests.\n\nAWS WAF tracks and manages web requests separately for each instance of a rate-based rule that you use. For example, if you provide the same rate-based rule settings in two web ACLs, each of the two rule statements represents a separate instance of the rate-based rule and gets its own tracking and management by AWS WAF . If you define a rate-based rule inside a rule group, and then use that rule group in multiple places, each use creates a separate instance of the rate-based rule that gets its own tracking and management by AWS WAF .\n\nWhen the rule action triggers, AWS WAF blocks additional requests from the IP address until the request rate falls below the limit.\n\nYou can optionally nest another statement inside the rate-based statement, to narrow the scope of the rule so that it only counts requests that match the nested statement. For example, based on recent requests that you have seen from an attacker, you might create a rate-based rule with a nested AND rule statement that contains the following nested statements:\n\n- An IP match statement with an IP set that specifies the address 192.0.2.44.\n- A string match statement that searches in the User-Agent header for the string BadBot.\n\nIn this rate-based rule, you also define a rate limit. For this example, the rate limit is 1,000. Requests that meet the criteria of both of the nested statements are counted. If the count exceeds 1,000 requests per five minutes, the rule action triggers. Requests that do not meet the criteria of both of the nested statements are not counted towards the rate limit and are not affected by this rule.\n\nYou cannot nest a `RateBasedStatement` inside another statement, for example inside a `NotStatement` or `OrStatement` . You can define a `RateBasedStatement` inside a web ACL and inside a rule group.", "properties": { "AggregateKeyType": "Setting that indicates how to aggregate the request counts. The options are the following:\n\n- IP - Aggregate the request counts on the IP address from the web request origin.\n- FORWARDED_IP - Aggregate the request counts on the first IP address in an HTTP header. If you use this, configure the `ForwardedIPConfig` , to specify the header to use.", "ForwardedIPConfig": "The configuration for inspecting IP addresses in an HTTP header that you specify, instead of using the IP address that's reported by the web request origin. Commonly, this is the X-Forwarded-For (XFF) header, but you can specify any header name.\n\n> If the specified header isn't present in the request, AWS WAF doesn't apply the rule to the web request at all. \n\nThis is required if `AggregateKeyType` is set to `FORWARDED_IP` .", @@ -49838,7 +50281,7 @@ "ManagedRuleGroupStatement": "A rule statement used to run the rules that are defined in a managed rule group. To use this, provide the vendor name and the name of the rule group in this statement.\n\nYou cannot nest a `ManagedRuleGroupStatement` , for example for use inside a `NotStatement` or `OrStatement` . It can only be referenced as a top-level statement within a rule.", "NotStatement": "A logical rule statement used to negate the results of another rule statement. You provide one `Statement` within the `NotStatement` .", "OrStatement": "A logical rule statement used to combine other rule statements with OR logic. You provide more than one `Statement` within the `OrStatement` .", - "RateBasedStatement": "A rate-based rule tracks the rate of requests for each originating IP address, and triggers the rule action when the rate exceeds a limit that you specify on the number of requests in any 5-minute time span. You can use this to put a temporary block on requests from an IP address that is sending excessive requests.\n\nAWS WAF tracks and manages web requests separately for each instance of a rate-based rule that you use. For example, if you provide the same rate-based rule settings in two web ACLs, each of the two rule statements represents a separate instance of the rate-based rule and gets its own tracking and management by AWS WAF . If you define a rate-based rule inside a rule group, and then use that rule group in multiple places, each use creates a separate instance of the rate-based rule that gets its own tracking and management by AWS WAF .\n\nWhen the rule action triggers, AWS WAF blocks additional requests from the IP address until the request rate falls below the limit.\n\nYou can optionally nest another statement inside the rate-based statement, to narrow the scope of the rule so that it only counts requests that match the nested statement. For example, based on recent requests that you have seen from an attacker, you might create a rate-based rule with a nested AND rule statement that contains the following nested statements:\n\n- An IP match statement with an IP set that specified the address 192.0.2.44.\n- A string match statement that searches in the User-Agent header for the string BadBot.\n\nIn this rate-based rule, you also define a rate limit. For this example, the rate limit is 1,000. Requests that meet the criteria of both of the nested statements are counted. If the count exceeds 1,000 requests per five minutes, the rule action triggers. Requests that do not meet the criteria of both of the nested statements are not counted towards the rate limit and are not affected by this rule.\n\nYou cannot nest a `RateBasedStatement` inside another statement, for example inside a `NotStatement` or `OrStatement` . You can define a `RateBasedStatement` inside a web ACL and inside a rule group.", + "RateBasedStatement": "A rate-based rule tracks the rate of requests for each originating IP address, and triggers the rule action when the rate exceeds a limit that you specify on the number of requests in any 5-minute time span. You can use this to put a temporary block on requests from an IP address that is sending excessive requests.\n\nAWS WAF tracks and manages web requests separately for each instance of a rate-based rule that you use. For example, if you provide the same rate-based rule settings in two web ACLs, each of the two rule statements represents a separate instance of the rate-based rule and gets its own tracking and management by AWS WAF . If you define a rate-based rule inside a rule group, and then use that rule group in multiple places, each use creates a separate instance of the rate-based rule that gets its own tracking and management by AWS WAF .\n\nWhen the rule action triggers, AWS WAF blocks additional requests from the IP address until the request rate falls below the limit.\n\nYou can optionally nest another statement inside the rate-based statement, to narrow the scope of the rule so that it only counts requests that match the nested statement. For example, based on recent requests that you have seen from an attacker, you might create a rate-based rule with a nested AND rule statement that contains the following nested statements:\n\n- An IP match statement with an IP set that specifies the address 192.0.2.44.\n- A string match statement that searches in the User-Agent header for the string BadBot.\n\nIn this rate-based rule, you also define a rate limit. For this example, the rate limit is 1,000. Requests that meet the criteria of both of the nested statements are counted. If the count exceeds 1,000 requests per five minutes, the rule action triggers. Requests that do not meet the criteria of both of the nested statements are not counted towards the rate limit and are not affected by this rule.\n\nYou cannot nest a `RateBasedStatement` inside another statement, for example inside a `NotStatement` or `OrStatement` . You can define a `RateBasedStatement` inside a web ACL and inside a rule group.", "RegexMatchStatement": "A rule statement used to search web request components for a match against a single regular expression.", "RegexPatternSetReferenceStatement": "A rule statement used to search web request components for matches with regular expressions. To use this, create a `RegexPatternSet` that specifies the expressions that you want to detect, then use the ARN of that set in this statement. A web request matches the pattern set rule statement if the request component matches any of the patterns in the set.\n\nEach regex pattern set rule statement references a regex pattern set. You create and maintain the set independent of your rules. This allows you to use the single set in multiple rules. When you update the referenced set, AWS WAF automatically updates all rules that reference it.", "RuleGroupReferenceStatement": "A rule statement used to run the rules that are defined in a `RuleGroup` . To use this, create a rule group with your rules, then provide the ARN of the rule group in this statement.\n\nYou cannot nest a `RuleGroupReferenceStatement` , for example for use inside a `NotStatement` or `OrStatement` . You can only use a rule group reference statement at the top level inside a web ACL.", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AmplifyUIBuilder.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AmplifyUIBuilder.json index 902e41f3a82dc..669b7b99a012a 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AmplifyUIBuilder.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AmplifyUIBuilder.json @@ -562,6 +562,12 @@ "Required": false, "UpdateType": "Mutable" }, + "IsArray": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-amplifyuibuilder-form-fieldinputconfig.html#cfn-amplifyuibuilder-form-fieldinputconfig-isarray", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, "MaxValue": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-amplifyuibuilder-form-fieldinputconfig.html#cfn-amplifyuibuilder-form-fieldinputconfig-maxvalue", "PrimitiveType": "Double", @@ -803,6 +809,12 @@ "AWS::AmplifyUIBuilder::Form.SectionalElement": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-amplifyuibuilder-form-sectionalelement.html", "Properties": { + "Excluded": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-amplifyuibuilder-form-sectionalelement.html#cfn-amplifyuibuilder-form-sectionalelement-excluded", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, "Level": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-amplifyuibuilder-form-sectionalelement.html#cfn-amplifyuibuilder-form-sectionalelement-level", "PrimitiveType": "Double", @@ -905,18 +917,18 @@ "ResourceTypes": { "AWS::AmplifyUIBuilder::Component": { "Attributes": { - "AppId": { - "PrimitiveType": "String" - }, - "EnvironmentName": { - "PrimitiveType": "String" - }, "Id": { "PrimitiveType": "String" } }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-amplifyuibuilder-component.html", "Properties": { + "AppId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-amplifyuibuilder-component.html#cfn-amplifyuibuilder-component-appid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "BindingProperties": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-amplifyuibuilder-component.html#cfn-amplifyuibuilder-component-bindingproperties", "ItemType": "ComponentBindingPropertiesValue", @@ -945,6 +957,12 @@ "Required": true, "UpdateType": "Mutable" }, + "EnvironmentName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-amplifyuibuilder-component.html#cfn-amplifyuibuilder-component-environmentname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "Events": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-amplifyuibuilder-component.html#cfn-amplifyuibuilder-component-events", "ItemType": "ComponentEvent", @@ -1081,24 +1099,24 @@ }, "AWS::AmplifyUIBuilder::Theme": { "Attributes": { - "AppId": { - "PrimitiveType": "String" - }, - "CreatedAt": { - "PrimitiveType": "String" - }, - "EnvironmentName": { - "PrimitiveType": "String" - }, "Id": { "PrimitiveType": "String" - }, - "ModifiedAt": { - "PrimitiveType": "String" } }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-amplifyuibuilder-theme.html", "Properties": { + "AppId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-amplifyuibuilder-theme.html#cfn-amplifyuibuilder-theme-appid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "EnvironmentName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-amplifyuibuilder-theme.html#cfn-amplifyuibuilder-theme-environmentname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-amplifyuibuilder-theme.html#cfn-amplifyuibuilder-theme-name", "PrimitiveType": "String", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppConfig.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppConfig.json index 2e6de3737149a..dc9521bba1950 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppConfig.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_AppConfig.json @@ -240,6 +240,12 @@ "Required": true, "UpdateType": "Immutable" }, + "KmsKeyIdentifier": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appconfig-deployment.html#cfn-appconfig-deployment-kmskeyidentifier", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appconfig-deployment.html#cfn-appconfig-deployment-tags", "ItemType": "Tags", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CloudTrail.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CloudTrail.json index c00069e64c131..f085d7e59071b 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CloudTrail.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_CloudTrail.json @@ -1,6 +1,23 @@ { "$version": "109.0.0", "PropertyTypes": { + "AWS::CloudTrail::Channel.Destination": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudtrail-channel-destination.html", + "Properties": { + "Location": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudtrail-channel-destination.html#cfn-cloudtrail-channel-destination-location", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Type": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudtrail-channel-destination.html#cfn-cloudtrail-channel-destination-type", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::CloudTrail::EventDataStore.AdvancedEventSelector": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudtrail-eventdatastore-advancedeventselector.html", "Properties": { @@ -144,6 +161,44 @@ } }, "ResourceTypes": { + "AWS::CloudTrail::Channel": { + "Attributes": { + "ChannelArn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudtrail-channel.html", + "Properties": { + "Destinations": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudtrail-channel.html#cfn-cloudtrail-channel-destinations", + "DuplicatesAllowed": false, + "ItemType": "Destination", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudtrail-channel.html#cfn-cloudtrail-channel-name", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Source": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudtrail-channel.html#cfn-cloudtrail-channel-source", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudtrail-channel.html#cfn-cloudtrail-channel-tags", + "DuplicatesAllowed": true, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, "AWS::CloudTrail::EventDataStore": { "Attributes": { "CreatedTimestamp": { @@ -215,6 +270,23 @@ } } }, + "AWS::CloudTrail::ResourcePolicy": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudtrail-resourcepolicy.html", + "Properties": { + "ResourceArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudtrail-resourcepolicy.html#cfn-cloudtrail-resourcepolicy-resourcearn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "ResourcePolicy": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudtrail-resourcepolicy.html#cfn-cloudtrail-resourcepolicy-resourcepolicy", + "PrimitiveType": "Json", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::CloudTrail::Trail": { "Attributes": { "Arn": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ConnectCampaigns.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ConnectCampaigns.json index 0d0b6369c9e68..aeff22b980239 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ConnectCampaigns.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_ConnectCampaigns.json @@ -1,6 +1,17 @@ { "$version": "109.0.0", "PropertyTypes": { + "AWS::ConnectCampaigns::Campaign.AnswerMachineDetectionConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-connectcampaigns-campaign-answermachinedetectionconfig.html", + "Properties": { + "EnableAnswerMachineDetection": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-connectcampaigns-campaign-answermachinedetectionconfig.html#cfn-connectcampaigns-campaign-answermachinedetectionconfig-enableanswermachinedetection", + "PrimitiveType": "Boolean", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::ConnectCampaigns::Campaign.DialerConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-connectcampaigns-campaign-dialerconfig.html", "Properties": { @@ -21,6 +32,12 @@ "AWS::ConnectCampaigns::Campaign.OutboundCallConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-connectcampaigns-campaign-outboundcallconfig.html", "Properties": { + "AnswerMachineDetectionConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-connectcampaigns-campaign-outboundcallconfig.html#cfn-connectcampaigns-campaign-outboundcallconfig-answermachinedetectionconfig", + "Required": false, + "Type": "AnswerMachineDetectionConfig", + "UpdateType": "Mutable" + }, "ConnectContactFlowArn": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-connectcampaigns-campaign-outboundcallconfig.html#cfn-connectcampaigns-campaign-outboundcallconfig-connectcontactflowarn", "PrimitiveType": "String", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EC2.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EC2.json index b9a4d65e40a07..c2e7c3f4a5c5b 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EC2.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_EC2.json @@ -921,6 +921,17 @@ } } }, + "AWS::EC2::IPAMResourceDiscovery.IpamOperatingRegion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-ipamresourcediscovery-ipamoperatingregion.html", + "Properties": { + "RegionName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-ipamresourcediscovery-ipamoperatingregion.html#cfn-ec2-ipamresourcediscovery-ipamoperatingregion-regionname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::EC2::Instance.AssociationParameter": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-instance-ssmassociations-associationparameters.html", "Properties": { @@ -5590,6 +5601,18 @@ }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ipam.html", "Properties": { + "DefaultResourceDiscoveryAssociationId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ipam.html#cfn-ec2-ipam-defaultresourcediscoveryassociationid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "DefaultResourceDiscoveryId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ipam.html#cfn-ec2-ipam-defaultresourcediscoveryid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "Description": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ipam.html#cfn-ec2-ipam-description", "PrimitiveType": "String", @@ -5604,6 +5627,12 @@ "Type": "List", "UpdateType": "Mutable" }, + "ResourceDiscoveryAssociationCount": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ipam.html#cfn-ec2-ipam-resourcediscoveryassociationcount", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ipam.html#cfn-ec2-ipam-tags", "DuplicatesAllowed": false, @@ -5747,6 +5776,12 @@ "Type": "List", "UpdateType": "Mutable" }, + "PublicIpSource": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ipampool.html#cfn-ec2-ipampool-publicipsource", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, "PubliclyAdvertisable": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ipampool.html#cfn-ec2-ipampool-publiclyadvertisable", "PrimitiveType": "Boolean", @@ -5769,6 +5804,135 @@ } } }, + "AWS::EC2::IPAMPoolCidr": { + "Attributes": { + "IpamPoolCidrId": { + "PrimitiveType": "String" + }, + "State": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ipampoolcidr.html", + "Properties": { + "Cidr": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ipampoolcidr.html#cfn-ec2-ipampoolcidr-cidr", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "IpamPoolId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ipampoolcidr.html#cfn-ec2-ipampoolcidr-ipampoolid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "NetmaskLength": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ipampoolcidr.html#cfn-ec2-ipampoolcidr-netmasklength", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Immutable" + } + } + }, + "AWS::EC2::IPAMResourceDiscovery": { + "Attributes": { + "IpamResourceDiscoveryArn": { + "PrimitiveType": "String" + }, + "IpamResourceDiscoveryId": { + "PrimitiveType": "String" + }, + "IpamResourceDiscoveryRegion": { + "PrimitiveType": "String" + }, + "IsDefault": { + "PrimitiveType": "Boolean" + }, + "OwnerId": { + "PrimitiveType": "String" + }, + "State": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ipamresourcediscovery.html", + "Properties": { + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ipamresourcediscovery.html#cfn-ec2-ipamresourcediscovery-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "OperatingRegions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ipamresourcediscovery.html#cfn-ec2-ipamresourcediscovery-operatingregions", + "DuplicatesAllowed": false, + "ItemType": "IpamOperatingRegion", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ipamresourcediscovery.html#cfn-ec2-ipamresourcediscovery-tags", + "DuplicatesAllowed": false, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::EC2::IPAMResourceDiscoveryAssociation": { + "Attributes": { + "IpamArn": { + "PrimitiveType": "String" + }, + "IpamRegion": { + "PrimitiveType": "String" + }, + "IpamResourceDiscoveryAssociationArn": { + "PrimitiveType": "String" + }, + "IpamResourceDiscoveryAssociationId": { + "PrimitiveType": "String" + }, + "IsDefault": { + "PrimitiveType": "Boolean" + }, + "OwnerId": { + "PrimitiveType": "String" + }, + "ResourceDiscoveryStatus": { + "PrimitiveType": "String" + }, + "State": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ipamresourcediscoveryassociation.html", + "Properties": { + "IpamId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ipamresourcediscoveryassociation.html#cfn-ec2-ipamresourcediscoveryassociation-ipamid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "IpamResourceDiscoveryId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ipamresourcediscoveryassociation.html#cfn-ec2-ipamresourcediscoveryassociation-ipamresourcediscoveryid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ipamresourcediscoveryassociation.html#cfn-ec2-ipamresourcediscoveryassociation-tags", + "DuplicatesAllowed": false, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, "AWS::EC2::IPAMScope": { "Attributes": { "Arn": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoT.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoT.json index b418424e0360e..aebb2572f0614 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoT.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_IoT.json @@ -978,6 +978,12 @@ "AWS::IoT::TopicRule.CloudwatchLogsAction": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-topicrule-cloudwatchlogsaction.html", "Properties": { + "BatchMode": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-topicrule-cloudwatchlogsaction.html#cfn-iot-topicrule-cloudwatchlogsaction-batchmode", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, "LogGroupName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-topicrule-cloudwatchlogsaction.html#cfn-iot-topicrule-cloudwatchlogsaction-loggroupname", "PrimitiveType": "String", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_NetworkFirewall.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_NetworkFirewall.json index 14ebe686d97b4..23086c58d9f5a 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_NetworkFirewall.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_NetworkFirewall.json @@ -4,6 +4,12 @@ "AWS::NetworkFirewall::Firewall.SubnetMapping": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-firewall-subnetmapping.html", "Properties": { + "IPAddressType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-firewall-subnetmapping.html#cfn-networkfirewall-firewall-subnetmapping-ipaddresstype", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "SubnetId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-networkfirewall-firewall-subnetmapping.html#cfn-networkfirewall-firewall-subnetmapping-subnetid", "PrimitiveType": "String", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_NetworkManager.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_NetworkManager.json index c7da895aae5c7..f794afef9a6ee 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_NetworkManager.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_NetworkManager.json @@ -431,7 +431,7 @@ "ConnectAttachmentId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-networkmanager-connectpeer.html#cfn-networkmanager-connectpeer-connectattachmentid", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" }, "CoreNetworkAddress": { @@ -444,14 +444,14 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-networkmanager-connectpeer.html#cfn-networkmanager-connectpeer-insidecidrblocks", "DuplicatesAllowed": true, "PrimitiveItemType": "String", - "Required": false, + "Required": true, "Type": "List", "UpdateType": "Immutable" }, "PeerAddress": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-networkmanager-connectpeer.html#cfn-networkmanager-connectpeer-peeraddress", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" }, "Tags": { @@ -818,7 +818,7 @@ "CoreNetworkId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-networkmanager-sitetositevpnattachment.html#cfn-networkmanager-sitetositevpnattachment-corenetworkid", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" }, "Tags": { @@ -832,7 +832,7 @@ "VpnConnectionArn": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-networkmanager-sitetositevpnattachment.html#cfn-networkmanager-sitetositevpnattachment-vpnconnectionarn", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" } } diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_NimbleStudio.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_NimbleStudio.json index dddc1d5682817..974a773245a68 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_NimbleStudio.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_NimbleStudio.json @@ -36,6 +36,12 @@ "Required": false, "UpdateType": "Mutable" }, + "SessionBackup": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-nimblestudio-launchprofile-streamconfiguration.html#cfn-nimblestudio-launchprofile-streamconfiguration-sessionbackup", + "Required": false, + "Type": "StreamConfigurationSessionBackup", + "UpdateType": "Mutable" + }, "SessionPersistenceMode": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-nimblestudio-launchprofile-streamconfiguration.html#cfn-nimblestudio-launchprofile-streamconfiguration-sessionpersistencemode", "PrimitiveType": "String", @@ -64,6 +70,23 @@ } } }, + "AWS::NimbleStudio::LaunchProfile.StreamConfigurationSessionBackup": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-nimblestudio-launchprofile-streamconfigurationsessionbackup.html", + "Properties": { + "MaxBackupsToRetain": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-nimblestudio-launchprofile-streamconfigurationsessionbackup.html#cfn-nimblestudio-launchprofile-streamconfigurationsessionbackup-maxbackupstoretain", + "PrimitiveType": "Double", + "Required": false, + "UpdateType": "Mutable" + }, + "Mode": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-nimblestudio-launchprofile-streamconfigurationsessionbackup.html#cfn-nimblestudio-launchprofile-streamconfigurationsessionbackup-mode", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::NimbleStudio::LaunchProfile.StreamConfigurationSessionStorage": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-nimblestudio-launchprofile-streamconfigurationsessionstorage.html", "Properties": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Omics.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Omics.json new file mode 100644 index 0000000000000..5205dd7a34ddf --- /dev/null +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_Omics.json @@ -0,0 +1,480 @@ +{ + "$version": "109.0.0", + "PropertyTypes": { + "AWS::Omics::AnnotationStore.ReferenceItem": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-omics-annotationstore-referenceitem.html", + "Properties": { + "ReferenceArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-omics-annotationstore-referenceitem.html#cfn-omics-annotationstore-referenceitem-referencearn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::Omics::AnnotationStore.SseConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-omics-annotationstore-sseconfig.html", + "Properties": { + "KeyArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-omics-annotationstore-sseconfig.html#cfn-omics-annotationstore-sseconfig-keyarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Type": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-omics-annotationstore-sseconfig.html#cfn-omics-annotationstore-sseconfig-type", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::Omics::AnnotationStore.StoreOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-omics-annotationstore-storeoptions.html", + "Properties": { + "TsvStoreOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-omics-annotationstore-storeoptions.html#cfn-omics-annotationstore-storeoptions-tsvstoreoptions", + "Required": true, + "Type": "TsvStoreOptions", + "UpdateType": "Immutable" + } + } + }, + "AWS::Omics::AnnotationStore.TsvStoreOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-omics-annotationstore-tsvstoreoptions.html", + "Properties": { + "AnnotationType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-omics-annotationstore-tsvstoreoptions.html#cfn-omics-annotationstore-tsvstoreoptions-annotationtype", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "FormatToHeader": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-omics-annotationstore-tsvstoreoptions.html#cfn-omics-annotationstore-tsvstoreoptions-formattoheader", + "PrimitiveItemType": "String", + "Required": false, + "Type": "Map", + "UpdateType": "Immutable" + }, + "Schema": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-omics-annotationstore-tsvstoreoptions.html#cfn-omics-annotationstore-tsvstoreoptions-schema", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Immutable" + } + } + }, + "AWS::Omics::ReferenceStore.SseConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-omics-referencestore-sseconfig.html", + "Properties": { + "KeyArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-omics-referencestore-sseconfig.html#cfn-omics-referencestore-sseconfig-keyarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Type": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-omics-referencestore-sseconfig.html#cfn-omics-referencestore-sseconfig-type", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::Omics::SequenceStore.SseConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-omics-sequencestore-sseconfig.html", + "Properties": { + "KeyArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-omics-sequencestore-sseconfig.html#cfn-omics-sequencestore-sseconfig-keyarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Type": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-omics-sequencestore-sseconfig.html#cfn-omics-sequencestore-sseconfig-type", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::Omics::VariantStore.ReferenceItem": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-omics-variantstore-referenceitem.html", + "Properties": { + "ReferenceArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-omics-variantstore-referenceitem.html#cfn-omics-variantstore-referenceitem-referencearn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::Omics::VariantStore.SseConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-omics-variantstore-sseconfig.html", + "Properties": { + "KeyArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-omics-variantstore-sseconfig.html#cfn-omics-variantstore-sseconfig-keyarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Type": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-omics-variantstore-sseconfig.html#cfn-omics-variantstore-sseconfig-type", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::Omics::Workflow.WorkflowParameter": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-omics-workflow-workflowparameter.html", + "Properties": { + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-omics-workflow-workflowparameter.html#cfn-omics-workflow-workflowparameter-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Optional": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-omics-workflow-workflowparameter.html#cfn-omics-workflow-workflowparameter-optional", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" + } + } + } + }, + "ResourceTypes": { + "AWS::Omics::AnnotationStore": { + "Attributes": { + "CreationTime": { + "PrimitiveType": "String" + }, + "Id": { + "PrimitiveType": "String" + }, + "Status": { + "PrimitiveType": "String" + }, + "StatusMessage": { + "PrimitiveType": "String" + }, + "StoreArn": { + "PrimitiveType": "String" + }, + "StoreSizeBytes": { + "PrimitiveType": "Double" + }, + "UpdateTime": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-annotationstore.html", + "Properties": { + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-annotationstore.html#cfn-omics-annotationstore-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-annotationstore.html#cfn-omics-annotationstore-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Reference": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-annotationstore.html#cfn-omics-annotationstore-reference", + "Required": false, + "Type": "ReferenceItem", + "UpdateType": "Immutable" + }, + "SseConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-annotationstore.html#cfn-omics-annotationstore-sseconfig", + "Required": false, + "Type": "SseConfig", + "UpdateType": "Immutable" + }, + "StoreFormat": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-annotationstore.html#cfn-omics-annotationstore-storeformat", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "StoreOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-annotationstore.html#cfn-omics-annotationstore-storeoptions", + "Required": false, + "Type": "StoreOptions", + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-annotationstore.html#cfn-omics-annotationstore-tags", + "PrimitiveItemType": "String", + "Required": false, + "Type": "Map", + "UpdateType": "Immutable" + } + } + }, + "AWS::Omics::ReferenceStore": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + }, + "CreationTime": { + "PrimitiveType": "String" + }, + "ReferenceStoreId": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-referencestore.html", + "Properties": { + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-referencestore.html#cfn-omics-referencestore-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-referencestore.html#cfn-omics-referencestore-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "SseConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-referencestore.html#cfn-omics-referencestore-sseconfig", + "Required": false, + "Type": "SseConfig", + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-referencestore.html#cfn-omics-referencestore-tags", + "PrimitiveItemType": "String", + "Required": false, + "Type": "Map", + "UpdateType": "Immutable" + } + } + }, + "AWS::Omics::RunGroup": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + }, + "CreationTime": { + "PrimitiveType": "String" + }, + "Id": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-rungroup.html", + "Properties": { + "MaxCpus": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-rungroup.html#cfn-omics-rungroup-maxcpus", + "PrimitiveType": "Double", + "Required": false, + "UpdateType": "Mutable" + }, + "MaxDuration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-rungroup.html#cfn-omics-rungroup-maxduration", + "PrimitiveType": "Double", + "Required": false, + "UpdateType": "Mutable" + }, + "MaxRuns": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-rungroup.html#cfn-omics-rungroup-maxruns", + "PrimitiveType": "Double", + "Required": false, + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-rungroup.html#cfn-omics-rungroup-name", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-rungroup.html#cfn-omics-rungroup-tags", + "PrimitiveItemType": "String", + "Required": false, + "Type": "Map", + "UpdateType": "Mutable" + } + } + }, + "AWS::Omics::SequenceStore": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + }, + "CreationTime": { + "PrimitiveType": "String" + }, + "SequenceStoreId": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-sequencestore.html", + "Properties": { + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-sequencestore.html#cfn-omics-sequencestore-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-sequencestore.html#cfn-omics-sequencestore-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "SseConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-sequencestore.html#cfn-omics-sequencestore-sseconfig", + "Required": false, + "Type": "SseConfig", + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-sequencestore.html#cfn-omics-sequencestore-tags", + "PrimitiveItemType": "String", + "Required": false, + "Type": "Map", + "UpdateType": "Immutable" + } + } + }, + "AWS::Omics::VariantStore": { + "Attributes": { + "CreationTime": { + "PrimitiveType": "String" + }, + "Id": { + "PrimitiveType": "String" + }, + "Status": { + "PrimitiveType": "String" + }, + "StatusMessage": { + "PrimitiveType": "String" + }, + "StoreArn": { + "PrimitiveType": "String" + }, + "StoreSizeBytes": { + "PrimitiveType": "Double" + }, + "UpdateTime": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-variantstore.html", + "Properties": { + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-variantstore.html#cfn-omics-variantstore-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-variantstore.html#cfn-omics-variantstore-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Reference": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-variantstore.html#cfn-omics-variantstore-reference", + "Required": true, + "Type": "ReferenceItem", + "UpdateType": "Immutable" + }, + "SseConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-variantstore.html#cfn-omics-variantstore-sseconfig", + "Required": false, + "Type": "SseConfig", + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-variantstore.html#cfn-omics-variantstore-tags", + "PrimitiveItemType": "String", + "Required": false, + "Type": "Map", + "UpdateType": "Immutable" + } + } + }, + "AWS::Omics::Workflow": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + }, + "CreationTime": { + "PrimitiveType": "String" + }, + "Id": { + "PrimitiveType": "String" + }, + "Status": { + "PrimitiveType": "String" + }, + "Type": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-workflow.html", + "Properties": { + "DefinitionUri": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-workflow.html#cfn-omics-workflow-definitionuri", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-workflow.html#cfn-omics-workflow-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Engine": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-workflow.html#cfn-omics-workflow-engine", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Main": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-workflow.html#cfn-omics-workflow-main", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-workflow.html#cfn-omics-workflow-name", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ParameterTemplate": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-workflow.html#cfn-omics-workflow-parametertemplate", + "ItemType": "WorkflowParameter", + "Required": false, + "Type": "Map", + "UpdateType": "Immutable" + }, + "StorageCapacity": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-workflow.html#cfn-omics-workflow-storagecapacity", + "PrimitiveType": "Double", + "Required": false, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-omics-workflow.html#cfn-omics-workflow-tags", + "PrimitiveItemType": "String", + "Required": false, + "Type": "Map", + "UpdateType": "Mutable" + } + } + } + } +} diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_OpsWorksCM.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_OpsWorksCM.json index 41705097ad374..23d03fc9fd2fb 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_OpsWorksCM.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_OpsWorksCM.json @@ -28,7 +28,7 @@ "Endpoint": { "PrimitiveType": "String" }, - "Id": { + "ServerName": { "PrimitiveType": "String" } }, @@ -140,12 +140,6 @@ "Type": "List", "UpdateType": "Immutable" }, - "ServerName": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opsworkscm-server.html#cfn-opsworkscm-server-servername", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Immutable" - }, "ServiceRoleArn": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opsworkscm-server.html#cfn-opsworkscm-server-servicerolearn", "PrimitiveType": "String", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RDS.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RDS.json index 51b77326f96aa..253fe5405dc56 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RDS.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RDS.json @@ -244,12 +244,6 @@ "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" - }, - "UserName": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-dbproxy-authformat.html#cfn-rds-dbproxy-authformat-username", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" } } }, @@ -1391,6 +1385,9 @@ "IsDefault": { "PrimitiveType": "Boolean" }, + "TargetRole": { + "PrimitiveType": "String" + }, "VpcId": { "PrimitiveType": "String" } @@ -1417,12 +1414,6 @@ "Type": "List", "UpdateType": "Mutable" }, - "TargetRole": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbproxyendpoint.html#cfn-rds-dbproxyendpoint-targetrole", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Immutable" - }, "VpcSecurityGroupIds": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbproxyendpoint.html#cfn-rds-dbproxyendpoint-vpcsecuritygroupids", "DuplicatesAllowed": true, @@ -1475,7 +1466,7 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbproxytargetgroup.html#cfn-rds-dbproxytargetgroup-dbproxyname", "PrimitiveType": "String", "Required": true, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "TargetGroupName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbproxytargetgroup.html#cfn-rds-dbproxytargetgroup-targetgroupname", diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RolesAnywhere.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RolesAnywhere.json index 9d5aca4a52cd2..926096d95eea7 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RolesAnywhere.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_RolesAnywhere.json @@ -48,7 +48,7 @@ "CrlData": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rolesanywhere-crl.html#cfn-rolesanywhere-crl-crldata", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "Enabled": { @@ -60,7 +60,7 @@ "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rolesanywhere-crl.html#cfn-rolesanywhere-crl-name", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "Tags": { @@ -113,7 +113,7 @@ "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rolesanywhere-profile.html#cfn-rolesanywhere-profile-name", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "RequireInstanceProperties": { @@ -126,7 +126,7 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rolesanywhere-profile.html#cfn-rolesanywhere-profile-rolearns", "DuplicatesAllowed": true, "PrimitiveItemType": "String", - "Required": false, + "Required": true, "Type": "List", "UpdateType": "Mutable" }, @@ -166,12 +166,12 @@ "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rolesanywhere-trustanchor.html#cfn-rolesanywhere-trustanchor-name", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "Source": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rolesanywhere-trustanchor.html#cfn-rolesanywhere-trustanchor-source", - "Required": false, + "Required": true, "Type": "Source", "UpdateType": "Mutable" }, diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SNS.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SNS.json index 3ba806d42b311..1e829e4928b8a 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SNS.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SNS.json @@ -153,6 +153,12 @@ "PrimitiveType": "String", "Required": false, "UpdateType": "Immutable" + }, + "TracingConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sns-topic.html#cfn-sns-topic-tracingconfig", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" } } }, diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SageMaker.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SageMaker.json index 7c48d561766fa..0e4f30652bba2 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SageMaker.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SageMaker.json @@ -1974,6 +1974,528 @@ } } }, + "AWS::SageMaker::ModelCard.AdditionalInformation": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-additionalinformation.html", + "Properties": { + "CaveatsAndRecommendations": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-additionalinformation.html#cfn-sagemaker-modelcard-additionalinformation-caveatsandrecommendations", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "CustomDetails": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-additionalinformation.html#cfn-sagemaker-modelcard-additionalinformation-customdetails", + "PrimitiveItemType": "String", + "Required": false, + "Type": "Map", + "UpdateType": "Mutable" + }, + "EthicalConsiderations": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-additionalinformation.html#cfn-sagemaker-modelcard-additionalinformation-ethicalconsiderations", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::SageMaker::ModelCard.BusinessDetails": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-businessdetails.html", + "Properties": { + "BusinessProblem": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-businessdetails.html#cfn-sagemaker-modelcard-businessdetails-businessproblem", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "BusinessStakeholders": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-businessdetails.html#cfn-sagemaker-modelcard-businessdetails-businessstakeholders", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "LineOfBusiness": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-businessdetails.html#cfn-sagemaker-modelcard-businessdetails-lineofbusiness", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::SageMaker::ModelCard.Content": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-content.html", + "Properties": { + "AdditionalInformation": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-content.html#cfn-sagemaker-modelcard-content-additionalinformation", + "Required": false, + "Type": "AdditionalInformation", + "UpdateType": "Mutable" + }, + "BusinessDetails": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-content.html#cfn-sagemaker-modelcard-content-businessdetails", + "Required": false, + "Type": "BusinessDetails", + "UpdateType": "Mutable" + }, + "EvaluationDetails": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-content.html#cfn-sagemaker-modelcard-content-evaluationdetails", + "DuplicatesAllowed": true, + "ItemType": "EvaluationDetail", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "IntendedUses": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-content.html#cfn-sagemaker-modelcard-content-intendeduses", + "Required": false, + "Type": "IntendedUses", + "UpdateType": "Mutable" + }, + "ModelOverview": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-content.html#cfn-sagemaker-modelcard-content-modeloverview", + "Required": false, + "Type": "ModelOverview", + "UpdateType": "Mutable" + }, + "TrainingDetails": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-content.html#cfn-sagemaker-modelcard-content-trainingdetails", + "Required": false, + "Type": "TrainingDetails", + "UpdateType": "Mutable" + } + } + }, + "AWS::SageMaker::ModelCard.EvaluationDetail": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-evaluationdetail.html", + "Properties": { + "Datasets": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-evaluationdetail.html#cfn-sagemaker-modelcard-evaluationdetail-datasets", + "DuplicatesAllowed": true, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "EvaluationJobArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-evaluationdetail.html#cfn-sagemaker-modelcard-evaluationdetail-evaluationjobarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "EvaluationObservation": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-evaluationdetail.html#cfn-sagemaker-modelcard-evaluationdetail-evaluationobservation", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Metadata": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-evaluationdetail.html#cfn-sagemaker-modelcard-evaluationdetail-metadata", + "PrimitiveItemType": "String", + "Required": false, + "Type": "Map", + "UpdateType": "Mutable" + }, + "MetricGroups": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-evaluationdetail.html#cfn-sagemaker-modelcard-evaluationdetail-metricgroups", + "DuplicatesAllowed": true, + "ItemType": "MetricGroup", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-evaluationdetail.html#cfn-sagemaker-modelcard-evaluationdetail-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, + "AWS::SageMaker::ModelCard.Function": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-function.html", + "Properties": { + "Condition": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-function.html#cfn-sagemaker-modelcard-function-condition", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Facet": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-function.html#cfn-sagemaker-modelcard-function-facet", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Function": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-function.html#cfn-sagemaker-modelcard-function-function", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::SageMaker::ModelCard.InferenceEnvironment": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-inferenceenvironment.html", + "Properties": { + "ContainerImage": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-inferenceenvironment.html#cfn-sagemaker-modelcard-inferenceenvironment-containerimage", + "DuplicatesAllowed": true, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::SageMaker::ModelCard.IntendedUses": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-intendeduses.html", + "Properties": { + "ExplanationsForRiskRating": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-intendeduses.html#cfn-sagemaker-modelcard-intendeduses-explanationsforriskrating", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "FactorsAffectingModelEfficiency": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-intendeduses.html#cfn-sagemaker-modelcard-intendeduses-factorsaffectingmodelefficiency", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "IntendedUses": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-intendeduses.html#cfn-sagemaker-modelcard-intendeduses-intendeduses", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "PurposeOfModel": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-intendeduses.html#cfn-sagemaker-modelcard-intendeduses-purposeofmodel", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "RiskRating": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-intendeduses.html#cfn-sagemaker-modelcard-intendeduses-riskrating", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::SageMaker::ModelCard.MetricDataItems": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-metricdataitems.html", + "Properties": { + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-metricdataitems.html#cfn-sagemaker-modelcard-metricdataitems-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Notes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-metricdataitems.html#cfn-sagemaker-modelcard-metricdataitems-notes", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Type": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-metricdataitems.html#cfn-sagemaker-modelcard-metricdataitems-type", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-metricdataitems.html#cfn-sagemaker-modelcard-metricdataitems-value", + "PrimitiveType": "Json", + "Required": true, + "UpdateType": "Mutable" + }, + "XAxisName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-metricdataitems.html#cfn-sagemaker-modelcard-metricdataitems-xaxisname", + "DuplicatesAllowed": true, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "YAxisName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-metricdataitems.html#cfn-sagemaker-modelcard-metricdataitems-yaxisname", + "DuplicatesAllowed": true, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::SageMaker::ModelCard.MetricGroup": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-metricgroup.html", + "Properties": { + "MetricData": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-metricgroup.html#cfn-sagemaker-modelcard-metricgroup-metricdata", + "DuplicatesAllowed": true, + "ItemType": "MetricDataItems", + "Required": true, + "Type": "List", + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-metricgroup.html#cfn-sagemaker-modelcard-metricgroup-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, + "AWS::SageMaker::ModelCard.ModelOverview": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-modeloverview.html", + "Properties": { + "AlgorithmType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-modeloverview.html#cfn-sagemaker-modelcard-modeloverview-algorithmtype", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "InferenceEnvironment": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-modeloverview.html#cfn-sagemaker-modelcard-modeloverview-inferenceenvironment", + "Required": false, + "Type": "InferenceEnvironment", + "UpdateType": "Mutable" + }, + "ModelArtifact": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-modeloverview.html#cfn-sagemaker-modelcard-modeloverview-modelartifact", + "DuplicatesAllowed": true, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "ModelCreator": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-modeloverview.html#cfn-sagemaker-modelcard-modeloverview-modelcreator", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ModelDescription": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-modeloverview.html#cfn-sagemaker-modelcard-modeloverview-modeldescription", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ModelId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-modeloverview.html#cfn-sagemaker-modelcard-modeloverview-modelid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ModelName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-modeloverview.html#cfn-sagemaker-modelcard-modeloverview-modelname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ModelOwner": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-modeloverview.html#cfn-sagemaker-modelcard-modeloverview-modelowner", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ModelVersion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-modeloverview.html#cfn-sagemaker-modelcard-modeloverview-modelversion", + "PrimitiveType": "Double", + "Required": false, + "UpdateType": "Mutable" + }, + "ProblemType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-modeloverview.html#cfn-sagemaker-modelcard-modeloverview-problemtype", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::SageMaker::ModelCard.ObjectiveFunction": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-objectivefunction.html", + "Properties": { + "Function": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-objectivefunction.html#cfn-sagemaker-modelcard-objectivefunction-function", + "Required": false, + "Type": "Function", + "UpdateType": "Mutable" + }, + "Notes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-objectivefunction.html#cfn-sagemaker-modelcard-objectivefunction-notes", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::SageMaker::ModelCard.SecurityConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-securityconfig.html", + "Properties": { + "KmsKeyId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-securityconfig.html#cfn-sagemaker-modelcard-securityconfig-kmskeyid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, + "AWS::SageMaker::ModelCard.TrainingDetails": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-trainingdetails.html", + "Properties": { + "ObjectiveFunction": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-trainingdetails.html#cfn-sagemaker-modelcard-trainingdetails-objectivefunction", + "Required": false, + "Type": "ObjectiveFunction", + "UpdateType": "Mutable" + }, + "TrainingJobDetails": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-trainingdetails.html#cfn-sagemaker-modelcard-trainingdetails-trainingjobdetails", + "Required": false, + "Type": "TrainingJobDetails", + "UpdateType": "Mutable" + }, + "TrainingObservations": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-trainingdetails.html#cfn-sagemaker-modelcard-trainingdetails-trainingobservations", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::SageMaker::ModelCard.TrainingEnvironment": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-trainingenvironment.html", + "Properties": { + "ContainerImage": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-trainingenvironment.html#cfn-sagemaker-modelcard-trainingenvironment-containerimage", + "DuplicatesAllowed": true, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::SageMaker::ModelCard.TrainingHyperParameter": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-traininghyperparameter.html", + "Properties": { + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-traininghyperparameter.html#cfn-sagemaker-modelcard-traininghyperparameter-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-traininghyperparameter.html#cfn-sagemaker-modelcard-traininghyperparameter-value", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, + "AWS::SageMaker::ModelCard.TrainingJobDetails": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-trainingjobdetails.html", + "Properties": { + "HyperParameters": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-trainingjobdetails.html#cfn-sagemaker-modelcard-trainingjobdetails-hyperparameters", + "DuplicatesAllowed": true, + "ItemType": "TrainingHyperParameter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "TrainingArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-trainingjobdetails.html#cfn-sagemaker-modelcard-trainingjobdetails-trainingarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "TrainingDatasets": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-trainingjobdetails.html#cfn-sagemaker-modelcard-trainingjobdetails-trainingdatasets", + "DuplicatesAllowed": true, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "TrainingEnvironment": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-trainingjobdetails.html#cfn-sagemaker-modelcard-trainingjobdetails-trainingenvironment", + "Required": false, + "Type": "TrainingEnvironment", + "UpdateType": "Mutable" + }, + "TrainingMetrics": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-trainingjobdetails.html#cfn-sagemaker-modelcard-trainingjobdetails-trainingmetrics", + "DuplicatesAllowed": true, + "ItemType": "TrainingMetric", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "UserProvidedHyperParameters": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-trainingjobdetails.html#cfn-sagemaker-modelcard-trainingjobdetails-userprovidedhyperparameters", + "DuplicatesAllowed": true, + "ItemType": "TrainingHyperParameter", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "UserProvidedTrainingMetrics": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-trainingjobdetails.html#cfn-sagemaker-modelcard-trainingjobdetails-userprovidedtrainingmetrics", + "DuplicatesAllowed": true, + "ItemType": "TrainingMetric", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::SageMaker::ModelCard.TrainingMetric": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-trainingmetric.html", + "Properties": { + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-trainingmetric.html#cfn-sagemaker-modelcard-trainingmetric-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Notes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-trainingmetric.html#cfn-sagemaker-modelcard-trainingmetric-notes", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-trainingmetric.html#cfn-sagemaker-modelcard-trainingmetric-value", + "PrimitiveType": "Double", + "Required": true, + "UpdateType": "Mutable" + } + } + }, + "AWS::SageMaker::ModelCard.UserContext": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-usercontext.html", + "Properties": { + "DomainId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-usercontext.html#cfn-sagemaker-modelcard-usercontext-domainid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "UserProfileArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-usercontext.html#cfn-sagemaker-modelcard-usercontext-userprofilearn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "UserProfileName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelcard-usercontext.html#cfn-sagemaker-modelcard-usercontext-userprofilename", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::SageMaker::ModelExplainabilityJobDefinition.BatchTransformInput": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-modelexplainabilityjobdefinition-batchtransforminput.html", "Properties": { @@ -5123,6 +5645,90 @@ } } }, + "AWS::SageMaker::ModelCard": { + "Attributes": { + "CreatedBy.DomainId": { + "PrimitiveType": "String" + }, + "CreatedBy.UserProfileArn": { + "PrimitiveType": "String" + }, + "CreatedBy.UserProfileName": { + "PrimitiveType": "String" + }, + "CreationTime": { + "PrimitiveType": "String" + }, + "LastModifiedBy.DomainId": { + "PrimitiveType": "String" + }, + "LastModifiedBy.UserProfileArn": { + "PrimitiveType": "String" + }, + "LastModifiedBy.UserProfileName": { + "PrimitiveType": "String" + }, + "LastModifiedTime": { + "PrimitiveType": "String" + }, + "ModelCardArn": { + "PrimitiveType": "String" + }, + "ModelCardProcessingStatus": { + "PrimitiveType": "String" + }, + "ModelCardVersion": { + "PrimitiveType": "Integer" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-modelcard.html", + "Properties": { + "Content": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-modelcard.html#cfn-sagemaker-modelcard-content", + "Required": true, + "Type": "Content", + "UpdateType": "Mutable" + }, + "CreatedBy": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-modelcard.html#cfn-sagemaker-modelcard-createdby", + "Required": false, + "Type": "UserContext", + "UpdateType": "Mutable" + }, + "LastModifiedBy": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-modelcard.html#cfn-sagemaker-modelcard-lastmodifiedby", + "Required": false, + "Type": "UserContext", + "UpdateType": "Mutable" + }, + "ModelCardName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-modelcard.html#cfn-sagemaker-modelcard-modelcardname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "ModelCardStatus": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-modelcard.html#cfn-sagemaker-modelcard-modelcardstatus", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "SecurityConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-modelcard.html#cfn-sagemaker-modelcard-securityconfig", + "Required": false, + "Type": "SecurityConfig", + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-modelcard.html#cfn-sagemaker-modelcard-tags", + "DuplicatesAllowed": true, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, "AWS::SageMaker::ModelExplainabilityJobDefinition": { "Attributes": { "CreationTime": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SimSpaceWeaver.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SimSpaceWeaver.json new file mode 100644 index 0000000000000..129de57bc60be --- /dev/null +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_SimSpaceWeaver.json @@ -0,0 +1,52 @@ +{ + "$version": "109.0.0", + "PropertyTypes": { + "AWS::SimSpaceWeaver::Simulation.S3Location": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-simspaceweaver-simulation-s3location.html", + "Properties": { + "BucketName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-simspaceweaver-simulation-s3location.html#cfn-simspaceweaver-simulation-s3location-bucketname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "ObjectKey": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-simspaceweaver-simulation-s3location.html#cfn-simspaceweaver-simulation-s3location-objectkey", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + } + }, + "ResourceTypes": { + "AWS::SimSpaceWeaver::Simulation": { + "Attributes": { + "DescribePayload": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-simspaceweaver-simulation.html", + "Properties": { + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-simspaceweaver-simulation.html#cfn-simspaceweaver-simulation-name", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "RoleArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-simspaceweaver-simulation.html#cfn-simspaceweaver-simulation-rolearn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "SchemaS3Location": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-simspaceweaver-simulation.html#cfn-simspaceweaver-simulation-schemas3location", + "Required": false, + "Type": "S3Location", + "UpdateType": "Mutable" + } + } + } + } +} diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_WAFv2.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_WAFv2.json index b29b125d99b9f..439f99b49c6c1 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_WAFv2.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/000_official/000_AWS_WAFv2.json @@ -172,11 +172,11 @@ } } }, - "AWS::WAFv2::RuleGroup.Allow": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-allow.html", + "AWS::WAFv2::RuleGroup.AllowAction": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-allowaction.html", "Properties": { "CustomRequestHandling": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-allow.html#cfn-wafv2-rulegroup-allow-customrequesthandling", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-allowaction.html#cfn-wafv2-rulegroup-allowaction-customrequesthandling", "Required": false, "Type": "CustomRequestHandling", "UpdateType": "Mutable" @@ -196,11 +196,11 @@ } } }, - "AWS::WAFv2::RuleGroup.Block": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-block.html", + "AWS::WAFv2::RuleGroup.BlockAction": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-blockaction.html", "Properties": { "CustomResponse": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-block.html#cfn-wafv2-rulegroup-block-customresponse", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-blockaction.html#cfn-wafv2-rulegroup-blockaction-customresponse", "Required": false, "Type": "CustomResponse", "UpdateType": "Mutable" @@ -255,11 +255,11 @@ } } }, - "AWS::WAFv2::RuleGroup.Captcha": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-captcha.html", + "AWS::WAFv2::RuleGroup.CaptchaAction": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-captchaaction.html", "Properties": { "CustomRequestHandling": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-captcha.html#cfn-wafv2-rulegroup-captcha-customrequesthandling", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-captchaaction.html#cfn-wafv2-rulegroup-captchaaction-customrequesthandling", "Required": false, "Type": "CustomRequestHandling", "UpdateType": "Mutable" @@ -277,11 +277,11 @@ } } }, - "AWS::WAFv2::RuleGroup.Challenge": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-challenge.html", + "AWS::WAFv2::RuleGroup.ChallengeAction": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-challengeaction.html", "Properties": { "CustomRequestHandling": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-challenge.html#cfn-wafv2-rulegroup-challenge-customrequesthandling", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-challengeaction.html#cfn-wafv2-rulegroup-challengeaction-customrequesthandling", "Required": false, "Type": "CustomRequestHandling", "UpdateType": "Mutable" @@ -349,11 +349,11 @@ } } }, - "AWS::WAFv2::RuleGroup.Count": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-count.html", + "AWS::WAFv2::RuleGroup.CountAction": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-countaction.html", "Properties": { "CustomRequestHandling": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-count.html#cfn-wafv2-rulegroup-count-customrequesthandling", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-countaction.html#cfn-wafv2-rulegroup-countaction-customrequesthandling", "Required": false, "Type": "CustomRequestHandling", "UpdateType": "Mutable" @@ -885,31 +885,31 @@ "Allow": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-ruleaction.html#cfn-wafv2-rulegroup-ruleaction-allow", "Required": false, - "Type": "Allow", + "Type": "AllowAction", "UpdateType": "Mutable" }, "Block": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-ruleaction.html#cfn-wafv2-rulegroup-ruleaction-block", "Required": false, - "Type": "Block", + "Type": "BlockAction", "UpdateType": "Mutable" }, "Captcha": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-ruleaction.html#cfn-wafv2-rulegroup-ruleaction-captcha", "Required": false, - "Type": "Captcha", + "Type": "CaptchaAction", "UpdateType": "Mutable" }, "Challenge": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-ruleaction.html#cfn-wafv2-rulegroup-ruleaction-challenge", "Required": false, - "Type": "Challenge", + "Type": "ChallengeAction", "UpdateType": "Mutable" }, "Count": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-ruleaction.html#cfn-wafv2-rulegroup-ruleaction-count", "Required": false, - "Type": "Count", + "Type": "CountAction", "UpdateType": "Mutable" } } diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/100_sam/000_official/spec.json b/packages/@aws-cdk/cfnspec/spec-source/specification/100_sam/000_official/spec.json index cedf2b0bc1e79..a6d72e218bb7c 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/100_sam/000_official/spec.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/100_sam/000_official/spec.json @@ -2255,7 +2255,7 @@ }, "Models": { "Documentation": "https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-api.html#sam-api-models", - "PrimitiveItemType": "String", + "PrimitiveItemType": "Map", "Required": false, "Type": "Map", "UpdateType": "Immutable" 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 5fa99faf9a687..809ef1c9f91f4 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 @@ -63,6 +63,15 @@ export interface DockerImageSource { */ readonly dockerBuildArgs?: { [name: string]: string }; + /** + * Additional build secrets + * + * Only allowed when `directory` is set. + * + * @default - No additional build secrets + */ + readonly dockerBuildSecrets?: { [name: string]: string }; + /** * Networking mode for the RUN commands during build. _Requires Docker Engine API v1.25+_. * diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts index 93f6f6a85e2f9..c3d4ac127a46f 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts @@ -118,6 +118,13 @@ export interface ContainerImageAssetMetadataEntry extends BaseAssetMetadataEntry */ readonly buildArgs?: { [key: string]: string }; + /** + * Build secrets to pass to the `docker build` command + * + * @default no build secrets are passed + */ + readonly buildSecrets?: { [key: string]: string }; + /** * Docker target to build to * 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 c861e5f3819db..28dea7efbf357 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/assets.schema.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/assets.schema.json @@ -155,6 +155,13 @@ "type": "string" } }, + "dockerBuildSecrets": { + "description": "Additional build secrets\n\nOnly allowed when `directory` is set. (Default - No additional build secrets)", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, "networkMode": { "description": "Networking mode for the RUN commands during build. _Requires Docker Engine API v1.25+_.\n\nSpecify this property to build images on a specific networking mode. (Default - no networking mode specified)", "type": "string" diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json index ae253314c97c0..ec3245e51505d 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json @@ -218,6 +218,13 @@ "type": "string" } }, + "buildSecrets": { + "description": "Build secrets to pass to the `docker build` command (Default no build secrets are passed)", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, "target": { "description": "Docker target to build to (Default no build target)", "type": "string" 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 d8b441d447f8a..ae4b03c54e770 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":"29.0.0"} \ No newline at end of file +{"version":"30.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/cloudformation-include/package.json b/packages/@aws-cdk/cloudformation-include/package.json index f8f8f93014a27..bc8c9c1b0412f 100644 --- a/packages/@aws-cdk/cloudformation-include/package.json +++ b/packages/@aws-cdk/cloudformation-include/package.json @@ -222,6 +222,7 @@ "@aws-cdk/aws-networkmanager": "0.0.0", "@aws-cdk/aws-nimblestudio": "0.0.0", "@aws-cdk/aws-oam": "0.0.0", + "@aws-cdk/aws-omics": "0.0.0", "@aws-cdk/aws-opensearchserverless": "0.0.0", "@aws-cdk/aws-opensearchservice": "0.0.0", "@aws-cdk/aws-opsworks": "0.0.0", @@ -264,6 +265,7 @@ "@aws-cdk/aws-servicediscovery": "0.0.0", "@aws-cdk/aws-ses": "0.0.0", "@aws-cdk/aws-signer": "0.0.0", + "@aws-cdk/aws-simspaceweaver": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/aws-ssm": "0.0.0", @@ -436,6 +438,7 @@ "@aws-cdk/aws-networkmanager": "0.0.0", "@aws-cdk/aws-nimblestudio": "0.0.0", "@aws-cdk/aws-oam": "0.0.0", + "@aws-cdk/aws-omics": "0.0.0", "@aws-cdk/aws-opensearchserverless": "0.0.0", "@aws-cdk/aws-opensearchservice": "0.0.0", "@aws-cdk/aws-opsworks": "0.0.0", @@ -478,6 +481,7 @@ "@aws-cdk/aws-servicediscovery": "0.0.0", "@aws-cdk/aws-ses": "0.0.0", "@aws-cdk/aws-signer": "0.0.0", + "@aws-cdk/aws-simspaceweaver": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/aws-ssm": "0.0.0", diff --git a/packages/@aws-cdk/core/lib/annotations.ts b/packages/@aws-cdk/core/lib/annotations.ts index 1a6f900f4799a..4907f0b7987b0 100644 --- a/packages/@aws-cdk/core/lib/annotations.ts +++ b/packages/@aws-cdk/core/lib/annotations.ts @@ -1,8 +1,6 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; -import { IConstruct, Node } from 'constructs'; - -const DEPRECATIONS_SYMBOL = Symbol.for('@aws-cdk/core.deprecations'); +import { IConstruct } from 'constructs'; /** * Includes API for attaching annotations such as warning messages to constructs. @@ -77,38 +75,23 @@ export class Annotations { // throw if CDK_BLOCK_DEPRECATIONS is set if (process.env.CDK_BLOCK_DEPRECATIONS) { - throw new Error(`${Node.of(this.scope).path}: ${text}`); - } - - // de-dup based on api key - const set = this.deprecationsReported; - if (set.has(api)) { - return; + throw new Error(`${this.scope.node.path}: ${text}`); } this.addWarning(text); - set.add(api); } /** * Adds a message metadata entry to the construct node, to be displayed by the CDK CLI. + * + * Records the message once per construct. * @param level The message level * @param message The message itself */ private addMessage(level: string, message: string) { - Node.of(this.scope).addMetadata(level, message, { stackTrace: this.stackTraces }); - } - - /** - * Returns the set of deprecations reported on this construct. - */ - private get deprecationsReported() { - let set = (this.scope as any)[DEPRECATIONS_SYMBOL]; - if (!set) { - set = new Set(); - Object.defineProperty(this.scope, DEPRECATIONS_SYMBOL, { value: set }); + const isNew = !this.scope.node.metadata.find((x) => x.data === message); + if (isNew) { + this.scope.node.addMetadata(level, message, { stackTrace: this.stackTraces }); } - - return set; } } diff --git a/packages/@aws-cdk/core/lib/assets.ts b/packages/@aws-cdk/core/lib/assets.ts index ea7903f7f23c2..10e1a61e7e5d0 100644 --- a/packages/@aws-cdk/core/lib/assets.ts +++ b/packages/@aws-cdk/core/lib/assets.ts @@ -174,6 +174,19 @@ export interface DockerImageAssetSource { */ readonly dockerBuildArgs?: { [key: string]: string }; + /** + * Build secrets to pass to the `docker build` command. + * + * Since Docker build secrets are resolved before deployment, keys and + * values cannot refer to unresolved tokens (such as `lambda.functionArn` or + * `queue.queueUrl`). + * + * Only allowed when `directoryName` is specified. + * + * @default - no build secrets are passed + */ + readonly dockerBuildSecrets?: { [key: string]: string }; + /** * Docker target to build to * diff --git a/packages/@aws-cdk/core/lib/bundling.ts b/packages/@aws-cdk/core/lib/bundling.ts index d88a84043a652..5d97d65d967fa 100644 --- a/packages/@aws-cdk/core/lib/bundling.ts +++ b/packages/@aws-cdk/core/lib/bundling.ts @@ -5,6 +5,24 @@ import { FileSystem } from './fs'; import { dockerExec } from './private/asset-staging'; import { quiet, reset } from './private/jsii-deprecated'; +/** + * Methods to build Docker CLI arguments for builds using secrets. + * + * Docker BuildKit must be enabled to use build secrets. + * + * @see https://docs.docker.com/build/buildkit/ + */ +export class DockerBuildSecret { + /** + * A Docker build secret from a file source + * @param src The path to the source file, relative to the build directory. + * @returns The latter half required for `--secret` + */ + public static fromSrc(src: string): string { + return `src=${src}`; + } +} + /** * Bundling options * diff --git a/packages/@aws-cdk/core/lib/private/refs.ts b/packages/@aws-cdk/core/lib/private/refs.ts index cb208a5d47a3d..9fa5dff403454 100644 --- a/packages/@aws-cdk/core/lib/private/refs.ts +++ b/packages/@aws-cdk/core/lib/private/refs.ts @@ -61,7 +61,7 @@ function resolveValue(consumer: Stack, reference: CfnReference): IResolvable { // unsupported: stacks are not in the same account if (producerAccount !== consumerAccount) { throw new Error( - `Stack "${consumer.node.path}" cannot consume a cross reference from stack "${producer.node.path}". ` + + `Stack "${consumer.node.path}" cannot reference ${renderReference(reference)} in stack "${producer.node.path}". ` + 'Cross stack references are only supported for stacks deployed to the same account or between nested stacks and their parent stack'); } @@ -69,7 +69,7 @@ function resolveValue(consumer: Stack, reference: CfnReference): IResolvable { // Stacks are in the same account, but different regions if (producerRegion !== consumerRegion && !consumer._crossRegionReferences) { throw new Error( - `Stack "${consumer.node.path}" cannot consume a cross reference from stack "${producer.node.path}". ` + + `Stack "${consumer.node.path}" cannot reference ${renderReference(reference)} in stack "${producer.node.path}". ` + 'Cross stack references are only supported for stacks deployed to the same environment or between nested stacks and their parent stack. ' + 'Set crossRegionReferences=true to enable cross region references'); } @@ -113,7 +113,7 @@ function resolveValue(consumer: Stack, reference: CfnReference): IResolvable { if (producerRegion !== consumerRegion && consumer._crossRegionReferences) { if (producerRegion === cxapi.UNKNOWN_REGION || consumerRegion === cxapi.UNKNOWN_REGION) { throw new Error( - `Stack "${consumer.node.path}" cannot consume a cross reference from stack "${producer.node.path}". ` + + `Stack "${consumer.node.path}" cannot reference ${renderReference(reference)} in stack "${producer.node.path}". ` + 'Cross stack/region references are only supported for stacks with an explicit region defined. '); } consumer.addDependency(producer, @@ -133,6 +133,13 @@ function resolveValue(consumer: Stack, reference: CfnReference): IResolvable { return createImportValue(reference); } +/** + * Return a human readable version of this reference + */ +function renderReference(ref: CfnReference) { + return `{${ref.target.node.path}[${ref.displayName}]}`; +} + /** * Finds all the CloudFormation references in a construct tree. */ diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/asset-manifest-builder.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/asset-manifest-builder.ts index 92497aacb27fc..801a951630192 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/asset-manifest-builder.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/asset-manifest-builder.ts @@ -65,6 +65,7 @@ export class AssetManifestBuilder { executable: asset.executable, directory: asset.directoryName, dockerBuildArgs: asset.dockerBuildArgs, + dockerBuildSecrets: asset.dockerBuildSecrets, dockerBuildTarget: asset.dockerBuildTarget, dockerFile: asset.dockerFile, networkMode: asset.networkMode, diff --git a/packages/@aws-cdk/core/test/annotations.test.ts b/packages/@aws-cdk/core/test/annotations.test.ts index 2f8fd0eef12a1..fc5c7430d22a8 100644 --- a/packages/@aws-cdk/core/test/annotations.test.ts +++ b/packages/@aws-cdk/core/test/annotations.test.ts @@ -1,7 +1,7 @@ import { Construct } from 'constructs'; +import { getWarnings } from './util'; import { App, Stack } from '../lib'; import { Annotations } from '../lib/annotations'; -import { getWarnings } from './util'; const restore = process.env.CDK_BLOCK_DEPRECATIONS; @@ -74,4 +74,23 @@ describe('annotations', () => { process.env.CDK_BLOCK_DEPRECATIONS = '1'; expect(() => Annotations.of(c1).addDeprecation('foo', 'bar')).toThrow(/MyStack\/Hello: The API foo is deprecated: bar\. This API will be removed in the next major release/); }); + + test('addMessage deduplicates the message on the node level', () => { + const app = new App(); + const stack = new Stack(app, 'S1'); + const c1 = new Construct(stack, 'C1'); + Annotations.of(c1).addWarning('You should know this!'); + Annotations.of(c1).addWarning('You should know this!'); + Annotations.of(c1).addWarning('You should know this!'); + Annotations.of(c1).addWarning('You should know this, too!'); + expect(getWarnings(app.synth())).toEqual([{ + path: '/S1/C1', + message: 'You should know this!', + }, + { + path: '/S1/C1', + message: 'You should know this, too!', + }], + ); + }); }); diff --git a/packages/@aws-cdk/core/test/bundling.test.ts b/packages/@aws-cdk/core/test/bundling.test.ts index ed15b8daa4282..fe2e0cea608f7 100644 --- a/packages/@aws-cdk/core/test/bundling.test.ts +++ b/packages/@aws-cdk/core/test/bundling.test.ts @@ -2,7 +2,7 @@ import * as child_process from 'child_process'; import * as crypto from 'crypto'; import * as path from 'path'; import * as sinon from 'sinon'; -import { DockerImage, FileSystem } from '../lib'; +import { DockerBuildSecret, DockerImage, FileSystem } from '../lib'; const dockerCmd = process.env.CDK_DOCKER ?? 'docker'; @@ -600,4 +600,12 @@ describe('bundling', () => { 'cool', 'command', ], { stdio: ['ignore', process.stderr, 'inherit'] })).toEqual(true); }); + + test('ensure correct Docker CLI arguments are returned', () => { + // GIVEN + const fromSrc = DockerBuildSecret.fromSrc('path.json'); + + // THEN + expect(fromSrc).toEqual('src=path.json'); + }); }); 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 2be3a7e008cd7..7d8ffbc2635ea 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 @@ -223,6 +223,27 @@ describe('new style synthesis', () => { }); + test('dockerBuildArgs or dockerBuildSecrets without directoryName', () => { + // WHEN + expect(() => { + stack.synthesizer.addDockerImageAsset({ + sourceHash: 'abcdef', + dockerBuildArgs: { + ABC: '123', + }, + }); + }).toThrowError(/Exactly one of 'directoryName' or 'executable' is required/); + + expect(() => { + stack.synthesizer.addDockerImageAsset({ + sourceHash: 'abcdef', + dockerBuildSecrets: { + DEF: '456', + }, + }); + }).toThrowError(/Exactly one of 'directoryName' or 'executable' is required/); + }); + test('synthesis', () => { // GIVEN stack.synthesizer.addFileAsset({ diff --git a/packages/@aws-cdk/core/test/stack.test.ts b/packages/@aws-cdk/core/test/stack.test.ts index ee1a12412d3ca..0000a59789f6e 100644 --- a/packages/@aws-cdk/core/test/stack.test.ts +++ b/packages/@aws-cdk/core/test/stack.test.ts @@ -1698,7 +1698,7 @@ describe('stack', () => { expect(() => { app.synth(); - }).toThrow(/Stack "Stack2" cannot consume a cross reference from stack "Stack1"/); + }).toThrow(/Stack "Stack2" cannot reference [^ ]+ in stack "Stack1"/); }); test('urlSuffix does not imply a stack dependency', () => { diff --git a/packages/@aws-cdk/cx-api/lib/assets.ts b/packages/@aws-cdk/cx-api/lib/assets.ts index ea6585c2a1103..3cc8312f646d4 100644 --- a/packages/@aws-cdk/cx-api/lib/assets.ts +++ b/packages/@aws-cdk/cx-api/lib/assets.ts @@ -12,6 +12,7 @@ export const ASSET_RESOURCE_METADATA_ENABLED_CONTEXT = 'aws:cdk:enable-asset-met export const ASSET_RESOURCE_METADATA_PATH_KEY = 'aws:asset:path'; export const ASSET_RESOURCE_METADATA_DOCKERFILE_PATH_KEY = 'aws:asset:dockerfile-path'; export const ASSET_RESOURCE_METADATA_DOCKER_BUILD_ARGS_KEY = 'aws:asset:docker-build-args'; +export const ASSET_RESOURCE_METADATA_DOCKER_BUILD_SECRETS_KEY = 'aws:asset:docker-build-secrets'; export const ASSET_RESOURCE_METADATA_DOCKER_BUILD_TARGET_KEY = 'aws:asset:docker-build-target'; export const ASSET_RESOURCE_METADATA_PROPERTY_KEY = 'aws:asset:property'; export const ASSET_RESOURCE_METADATA_IS_BUNDLED_KEY = 'aws:asset:is-bundled'; diff --git a/packages/@aws-cdk/integ-runner/THIRD_PARTY_LICENSES b/packages/@aws-cdk/integ-runner/THIRD_PARTY_LICENSES index b0e7a8cac9066..34936b3641a2c 100644 --- a/packages/@aws-cdk/integ-runner/THIRD_PARTY_LICENSES +++ b/packages/@aws-cdk/integ-runner/THIRD_PARTY_LICENSES @@ -156,7 +156,7 @@ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH RE ---------------- -** aws-sdk@2.1304.0 - https://www.npmjs.com/package/aws-sdk/v/2.1304.0 | Apache-2.0 +** aws-sdk@2.1306.0 - https://www.npmjs.com/package/aws-sdk/v/2.1306.0 | Apache-2.0 AWS SDK for JavaScript Copyright 2012-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/lambda-layer-awscli/README.md b/packages/@aws-cdk/lambda-layer-awscli/README.md index c902510290dde..8221cbe348aa9 100644 --- a/packages/@aws-cdk/lambda-layer-awscli/README.md +++ b/packages/@aws-cdk/lambda-layer-awscli/README.md @@ -25,3 +25,8 @@ fn.addLayers(new AwsCliLayer(this, 'AwsCliLayer')); ``` The CLI will be installed under `/opt/awscli/aws`. + +## Alternatives + +This module bundles AWS cli v1. To use AWS cli v2, you can use the +external module [awscdk-asset-awscli](https://github.com/cdklabs/awscdk-asset-awscli/tree/awscli-v2/main). diff --git a/packages/@aws-cdk/lambda-layer-kubectl/README.md b/packages/@aws-cdk/lambda-layer-kubectl/README.md index 2a90c534c9f24..59427504f1a22 100644 --- a/packages/@aws-cdk/lambda-layer-kubectl/README.md +++ b/packages/@aws-cdk/lambda-layer-kubectl/README.md @@ -26,3 +26,10 @@ fn.addLayers(new KubectlLayer(this, 'KubectlLayer')); ``` `kubectl` will be installed under `/opt/kubectl/kubectl`, and `helm` will be installed under `/opt/helm/helm`. + +## Alternatives + +This module bundles Kubectl v1.20.0 and the associated helm version +To use alternative Kubectl versions, including the latest available, +you can use the external module +[awscdk-asset-kubectl](https://github.com/cdklabs/awscdk-asset-kubectl). diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index 91ec38e0f4184..48b3611dd517e 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -907,21 +907,37 @@ If you wish to use an existing `CodePipeline.Pipeline` while using the modern AP methods and classes, you can pass in the existing `CodePipeline.Pipeline` to be built upon instead of having the `pipelines.CodePipeline` construct create a new `CodePipeline.Pipeline`. This also gives you more direct control over the underlying `CodePipeline.Pipeline` construct -if the way the modern API creates it doesn't allow for desired configurations. +if the way the modern API creates it doesn't allow for desired configurations. Use `CodePipelineFileset` to convert CodePipeline **artifacts** into CDK Pipelines **file sets**, +that can be used everywhere a file set or file set producer is expected. -Here's an example of passing in an existing pipeline: +Here's an example of passing in an existing pipeline and using a *source* that's already +in the pipeline: ```ts declare const codePipeline: codepipeline.Pipeline; +const sourceArtifact = new codepipeline.Artifact('MySourceArtifact'); + const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + codePipeline: codePipeline, synth: new pipelines.ShellStep('Synth', { - input: pipelines.CodePipelineSource.connection('my-org/my-app', 'main', { - connectionArn: 'arn:aws:codestar-connections:us-east-1:222222222222:connection/7d2469ff-514a-4e4f-9003-5ca4a43cdc41', // Created using the AWS console * });', - }), + input: pipelines.CodePipelineFileSet.fromArtifact(sourceArtifact), commands: ['npm ci','npm run build','npx cdk synth'], }), +}); +``` + +If your existing pipeline already provides a synth step, pass the existing +artifact in place of the `synth` step: + +```ts +declare const codePipeline: codepipeline.Pipeline; + +const buildArtifact = new codepipeline.Artifact('MyBuildArtifact'); + +const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { codePipeline: codePipeline, + synth: pipelines.CodePipelineFileSet.fromArtifact(buildArtifact), }); ``` diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts index 6b42855f05b4b..6bb82e57408fb 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts @@ -236,6 +236,12 @@ export interface GitHubSourceOptions { */ readonly trigger?: GitHubTrigger; + /** + * The action name used for this source in the CodePipeline + * + * @default - The repository string + */ + readonly actionName?: string; } /** @@ -262,7 +268,7 @@ class GitHubSource extends CodePipelineSource { protected getAction(output: Artifact, actionName: string, runOrder: number, variablesNamespace?: string) { return new cp_actions.GitHubSourceAction({ output, - actionName, + actionName: this.props.actionName ?? actionName, runOrder, oauthToken: this.authentication, owner: this.owner, @@ -379,7 +385,6 @@ export interface ConnectionSourceOptions { */ readonly connectionArn: string; - // long URL in @see /** * If this is set, the next CodeBuild job clones the repository (instead of CodePipeline downloading the files). @@ -403,6 +408,13 @@ export interface ConnectionSourceOptions { * @see https://docs.aws.amazon.com/codepipeline/latest/userguide/action-reference-CodestarConnectionSource.html */ readonly triggerOnPush?: boolean; + + /** + * The action name used for this source in the CodePipeline + * + * @default - The repository string + */ + readonly actionName?: string; } class CodeStarConnectionSource extends CodePipelineSource { @@ -424,7 +436,7 @@ class CodeStarConnectionSource extends CodePipelineSource { protected getAction(output: Artifact, actionName: string, runOrder: number, variablesNamespace?: string) { return new cp_actions.CodeStarConnectionsSourceAction({ output, - actionName, + actionName: this.props.actionName ?? actionName, runOrder, connectionArn: this.props.connectionArn, owner: this.owner, @@ -468,6 +480,13 @@ export interface CodeCommitSourceOptions { * @see https://docs.aws.amazon.com/codepipeline/latest/userguide/action-reference-CodeCommit.html */ readonly codeBuildCloneOutput?: boolean; + + /** + * The action name used for this source in the CodePipeline + * + * @default - The repository name + */ + readonly actionName?: string; } class CodeCommitSource extends CodePipelineSource { @@ -483,7 +502,7 @@ class CodeCommitSource extends CodePipelineSource { return new cp_actions.CodeCommitSourceAction({ output, // Guaranteed to be okay as action name - actionName: this.repository.repositoryName, + actionName: this.props.actionName ?? this.repository.repositoryName, runOrder, branch: this.branch, trigger: this.props.trigger, diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts index ef94a85989f73..0f6997e392b18 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts @@ -320,6 +320,7 @@ export class CodePipeline extends PipelineBase { private _pipeline?: cp.Pipeline; private artifacts = new ArtifactMap(); private _synthProject?: cb.IProject; + private _selfMutationProject?: cb.IProject; private readonly selfMutation: boolean; private readonly useChangeSets: boolean; private _myCxAsmRoot?: string; @@ -365,6 +366,22 @@ export class CodePipeline extends PipelineBase { return this._synthProject; } + /** + * The CodeBuild project that performs the SelfMutation + * + * Will throw an error if this is accessed before `buildPipeline()` + * is called. + * + * May return no value if `selfMutation` was set to `false` when + * the `CodePipeline` was defined. + */ + public get selfMutationProject(): cb.IProject | undefined { + if (!this._pipeline) { + throw new Error('Call pipeline.buildPipeline() before reading this property'); + } + return this._selfMutationProject; + } + /** * The CodePipeline pipeline that deploys the CDK app * @@ -527,6 +544,9 @@ export class CodePipeline extends PipelineBase { if (nodeType === CodeBuildProjectType.SYNTH) { this._synthProject = result.project; } + if (nodeType === CodeBuildProjectType.SELF_MUTATE) { + this._selfMutationProject = result.project; + } } if (node.data?.type === 'step' && node.data.step.primaryOutput?.primaryOutput && !this._fallbackArtifact) { diff --git a/packages/@aws-cdk/pipelines/lib/helpers-internal/graph.ts b/packages/@aws-cdk/pipelines/lib/helpers-internal/graph.ts index 5f9c2af63df17..b7c42c6b7aada 100644 --- a/packages/@aws-cdk/pipelines/lib/helpers-internal/graph.ts +++ b/packages/@aws-cdk/pipelines/lib/helpers-internal/graph.ts @@ -251,7 +251,7 @@ export class Graph extends GraphNode { /** * Return topologically sorted tranches of nodes at this graph level */ - public sortedChildren(): GraphNode[][] { + public sortedChildren(fail=true): GraphNode[][] { // Project dependencies to current children const nodes = this.nodes; const projectedDependencies = projectDependencies(this.deepDependencies(), (node) => { @@ -261,7 +261,7 @@ export class Graph extends GraphNode { return nodes.has(node) ? [node] : []; }); - return topoSort(nodes, projectedDependencies); + return topoSort(nodes, projectedDependencies, fail); } /** @@ -302,7 +302,7 @@ export class Graph extends GraphNode { lines.push(`${indent} ${bullet} ${x}${depString(x)}`); if (x instanceof Graph) { let i = 0; - const sortedNodes = Array.prototype.concat.call([], ...x.sortedChildren()); + const sortedNodes = Array.prototype.concat.call([], ...x.sortedChildren(false)); for (const child of sortedNodes) { recurse(child, `${indent} ${follow} `, i++ == x.nodes.size - 1); } @@ -357,7 +357,7 @@ export class Graph extends GraphNode { // Add dependency arrows between the "subgraph begin" and the first rank of // the children, and the last rank of the children and "subgraph end" nodes. - const sortedChildren = node.sortedChildren(); + const sortedChildren = node.sortedChildren(false); for (const first of sortedChildren[0]) { const src = first instanceof Graph ? graphBegin(first) : nodeLabel(first); lines.push(`${graphBegin(node)} -> ${src};`); diff --git a/packages/@aws-cdk/pipelines/lib/helpers-internal/toposort.ts b/packages/@aws-cdk/pipelines/lib/helpers-internal/toposort.ts index eb5e0cc3483aa..68f403e263179 100644 --- a/packages/@aws-cdk/pipelines/lib/helpers-internal/toposort.ts +++ b/packages/@aws-cdk/pipelines/lib/helpers-internal/toposort.ts @@ -9,7 +9,7 @@ export function printDependencyMap(dependencies: Map, Set(nodes: Set>, dependencies: Map, Set>>): GraphNode[][] { +export function topoSort(nodes: Set>, dependencies: Map, Set>>, fail=true): GraphNode[][] { const remaining = new Set>(nodes); const ret: GraphNode[][] = []; @@ -26,7 +26,14 @@ export function topoSort(nodes: Set>, dependencies: Map n.id).join(' => ')}`); + + if (fail) { + throw new Error(`Dependency cycle in graph: ${cycle.map(n => n.id).join(' => ')}`); + } + + // If we're trying not to fail, pick one at random from the cycle and treat it + // as selectable, then continue. + selectable.push(cycle[0]); } ret.push(selectable); diff --git a/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts b/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts index 9295f104a25bf..d8a795d0b3e45 100644 --- a/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts +++ b/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts @@ -288,3 +288,54 @@ test('pass role to s3 codepipeline source', () => { }]), }); }); + +type SourceFactory = (stack: Stack) => cdkp.CodePipelineSource; + +test.each([ + ['CodeCommit', (stack) => { + const repo = new ccommit.Repository(stack, 'Repo', { + repositoryName: 'MyRepo', + }); + return cdkp.CodePipelineSource.codeCommit(repo, 'main', { + actionName: 'ConfiguredName', + }); + }], + ['S3', (stack) => { + const bucket = new s3.Bucket(stack, 'Bucket'); + return cdkp.CodePipelineSource.s3(bucket, 'thefile.zip', { + actionName: 'ConfiguredName', + }); + }], + ['ECR', (stack) => { + const repository = new ecr.Repository(stack, 'Repository', { repositoryName: 'namespace/repo' }); + return cdkp.CodePipelineSource.ecr(repository, { + actionName: 'ConfiguredName', + }); + }], + ['GitHub', () => { + return cdkp.CodePipelineSource.gitHub('owner/repo', 'main', { + actionName: 'ConfiguredName', + }); + }], + ['CodeStar', () => { + return cdkp.CodePipelineSource.connection('owner/repo', 'main', { + connectionArn: 'arn:aws:codestar-connections:us-west-2:123456789012:connection/39e4c34d-e13a-4e94-a886', + actionName: 'ConfiguredName', + }); + }], +] as Array<[string, SourceFactory]>)('can configure actionName for %s', (_name: string, fac: SourceFactory) => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Pipeline', { + input: fac(pipelineStack), + }); + + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ + Name: 'Source', + Actions: [ + Match.objectLike({ + Name: 'ConfiguredName', + }), + ], + }]), + }); +}); diff --git a/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline.test.ts b/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline.test.ts index 246c3173efc0f..a0ea38fe6f264 100644 --- a/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline.test.ts +++ b/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline.test.ts @@ -8,7 +8,7 @@ import { Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import * as cdkp from '../../lib'; import { CodePipeline } from '../../lib'; -import { PIPELINE_ENV, TestApp, ModernTestGitHubNpmPipeline, FileAssetApp, TwoStackApp } from '../testhelpers'; +import { PIPELINE_ENV, TestApp, ModernTestGitHubNpmPipeline, FileAssetApp, TwoStackApp, StageWithStackOutput } from '../testhelpers'; let app: TestApp; @@ -392,6 +392,68 @@ test('action name is calculated properly if it has cross-stack dependencies', () }); }); +test('synths with change set approvers', () => { + // GIVEN + const pipelineStack = new cdk.Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + + // WHEN + const csApproval = new cdkp.ManualApprovalStep('ChangeSetApproval'); + + // The issue we were diagnosing only manifests if the stacks don't have + // a dependency on each other + const stage = new TwoStackApp(app, 'TheApp', { withDependency: false }); + pipeline.addStage(stage, { + stackSteps: [ + { stack: stage.stack1, changeSet: [csApproval] }, + { stack: stage.stack2, changeSet: [csApproval] }, + ], + }); + + // THEN + const template = Template.fromStack(pipelineStack); + template.hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ + Name: 'TheApp', + Actions: Match.arrayWith([ + Match.objectLike({ Name: 'Stack1.Prepare', RunOrder: 1 }), + Match.objectLike({ Name: 'Stack2.Prepare', RunOrder: 1 }), + Match.objectLike({ Name: 'Stack1.ChangeSetApproval', RunOrder: 2 }), + Match.objectLike({ Name: 'Stack1.Deploy', RunOrder: 3 }), + Match.objectLike({ Name: 'Stack2.Deploy', RunOrder: 3 }), + ]), + }]), + }); +}); + +test('selfMutationProject can be accessed after buildPipeline', () => { + // GIVEN + const pipelineStack = new cdk.Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new StageWithStackOutput(pipelineStack, 'Stage')); + + // WHEN + pipeline.buildPipeline(); + + // THEN + expect(pipeline.selfMutationProject).toBeTruthy(); +}); + +test('selfMutationProject is undefined if switched off', () => { + // GIVEN + const pipelineStack = new cdk.Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + selfMutation: false, + }); + pipeline.addStage(new StageWithStackOutput(pipelineStack, 'Stage')); + + // WHEN + pipeline.buildPipeline(); + + // THEN + expect(pipeline.selfMutationProject).toBeUndefined(); +}); + interface ReuseCodePipelineStackProps extends cdk.StackProps { reuseCrossRegionSupportStacks?: boolean; } diff --git a/packages/aws-cdk-lib/package.json b/packages/aws-cdk-lib/package.json index c258acc743dba..d03670b41c4d6 100644 --- a/packages/aws-cdk-lib/package.json +++ b/packages/aws-cdk-lib/package.json @@ -295,6 +295,7 @@ "@aws-cdk/aws-networkmanager": "0.0.0", "@aws-cdk/aws-nimblestudio": "0.0.0", "@aws-cdk/aws-oam": "0.0.0", + "@aws-cdk/aws-omics": "0.0.0", "@aws-cdk/aws-opensearchserverless": "0.0.0", "@aws-cdk/aws-opensearchservice": "0.0.0", "@aws-cdk/aws-opsworks": "0.0.0", @@ -343,6 +344,7 @@ "@aws-cdk/aws-ses": "0.0.0", "@aws-cdk/aws-ses-actions": "0.0.0", "@aws-cdk/aws-signer": "0.0.0", + "@aws-cdk/aws-simspaceweaver": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sns-subscriptions": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index 5fab1e24809fa..009dd9a192389 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -375,15 +375,20 @@ $ cdk deploy --hotswap [StackNames] ``` This will attempt to perform a faster, short-circuit deployment if possible -(for example, if you only changed the code of a Lambda function in your CDK app, -but nothing else in your CDK code), +(for example, if you changed the code of a Lambda function in your CDK app), skipping CloudFormation, and updating the affected resources directly; this includes changes to resources in nested stacks. If the tool detects that the change does not support hotswapping, -it will fall back and perform a full CloudFormation deployment, -exactly like `cdk deploy` does without the `--hotswap` flag. +it will ignore it and display that ignored change. +To have hotswap fall back and perform a full CloudFormation deployment, +exactly like `cdk deploy` does without the `--hotswap` flag, +specify `--hotswap-fallback`, like so: -Passing this option to `cdk deploy` will make it use your current AWS credentials to perform the API calls - +```console +$ cdk deploy --hotswap-fallback [StackNames] +``` + +Passing either option to `cdk deploy` will make it use your current AWS credentials to perform the API calls - it will not assume the Roles from your bootstrap stack, even if the `@aws-cdk/core:newStyleStackSynthesis` feature flag is set to `true` (as those Roles do not have the necessary permissions to update AWS resources directly, without using CloudFormation). diff --git a/packages/aws-cdk/THIRD_PARTY_LICENSES b/packages/aws-cdk/THIRD_PARTY_LICENSES index 95c923b3af2f4..4d6011297607a 100644 --- a/packages/aws-cdk/THIRD_PARTY_LICENSES +++ b/packages/aws-cdk/THIRD_PARTY_LICENSES @@ -268,7 +268,7 @@ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH RE ---------------- -** aws-sdk@2.1304.0 - https://www.npmjs.com/package/aws-sdk/v/2.1304.0 | Apache-2.0 +** aws-sdk@2.1306.0 - https://www.npmjs.com/package/aws-sdk/v/2.1306.0 | Apache-2.0 AWS SDK for JavaScript Copyright 2012-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/aws-cdk/lib/api/cloudformation-deployments.ts b/packages/aws-cdk/lib/api/cloudformation-deployments.ts index c2477c56fbc3f..fee95cecb2ff7 100644 --- a/packages/aws-cdk/lib/api/cloudformation-deployments.ts +++ b/packages/aws-cdk/lib/api/cloudformation-deployments.ts @@ -7,6 +7,7 @@ import { Mode } from './aws-auth/credentials'; import { ISDK } from './aws-auth/sdk'; import { SdkProvider } from './aws-auth/sdk-provider'; import { deployStack, DeployStackResult, destroyStack, makeBodyParameterAndUpload, DeploymentMethod } from './deploy-stack'; +import { HotswapMode } from './hotswap/common'; import { loadCurrentTemplateWithNestedStacks, loadCurrentTemplate } from './nested-stack-helpers'; import { ToolkitInfo } from './toolkit-info'; import { CloudFormationStack, Template, ResourcesToImport, ResourceIdentifierSummaries } from './util/cloudformation'; @@ -224,9 +225,9 @@ export interface DeployStackOptions { * A 'hotswap' deployment will attempt to short-circuit CloudFormation * and update the affected resources like Lambda functions directly. * - * @default - false for regular deployments, true for 'watch' deployments + * @default - `HotswapMode.FULL_DEPLOYMENT` for regular deployments, `HotswapMode.HOTSWAP_ONLY` for 'watch' deployments */ - readonly hotswap?: boolean; + readonly hotswap?: HotswapMode; /** * The extra string to append to the User-Agent header when performing AWS SDK calls. diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index 31dd7938b4cf2..9c53cbcf8a8b4 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -13,7 +13,7 @@ import { contentHash } from '../util/content-hash'; import { ISDK, SdkProvider } from './aws-auth'; import { CfnEvaluationException } from './evaluate-cloudformation-template'; import { tryHotswapDeployment } from './hotswap-deployments'; -import { ICON } from './hotswap/common'; +import { HotswapMode, ICON } from './hotswap/common'; import { ToolkitInfo } from './toolkit-info'; import { changeSetHasNoChanges, CloudFormationStack, TemplateParameters, waitForChangeSet, @@ -175,9 +175,9 @@ export interface DeployStackOptions { * A 'hotswap' deployment will attempt to short-circuit CloudFormation * and update the affected resources like Lambda functions directly. * - * @default - false for regular deployments, true for 'watch' deployments + * @default - `HotswapMode.FULL_DEPLOYMENT` for regular deployments, `HotswapMode.HOTSWAP_ONLY` for 'watch' deployments */ - readonly hotswap?: boolean; + readonly hotswap?: HotswapMode; /** * The extra string to append to the User-Agent header when performing AWS SDK calls. @@ -298,10 +298,13 @@ export async function deployStack(options: DeployStackOptions): Promise Promise; + +const RESOURCE_DETECTORS: { [key:string]: HotswapDetector } = { + // Lambda + 'AWS::Lambda::Function': isHotswappableLambdaFunctionChange, + 'AWS::Lambda::Version': isHotswappableLambdaFunctionChange, + 'AWS::Lambda::Alias': isHotswappableLambdaFunctionChange, + // AppSync + 'AWS::AppSync::Resolver': isHotswappableAppSyncChange, + 'AWS::AppSync::FunctionConfiguration': isHotswappableAppSyncChange, + + 'AWS::ECS::TaskDefinition': isHotswappableEcsServiceChange, + 'AWS::CodeBuild::Project': isHotswappableCodeBuildProjectChange, + 'AWS::StepFunctions::StateMachine': isHotswappableStateMachineChange, + 'Custom::CDKBucketDeployment': isHotswappableS3BucketDeploymentChange, + 'AWS::IAM::Policy': isHotswappableS3BucketDeploymentChange, + + 'AWS::CDK::Metadata': async () => [], +}; + /** * Perform a hotswap deployment, * short-circuiting CloudFormation if possible. @@ -25,6 +47,7 @@ import { CloudFormationStack } from './util/cloudformation'; export async function tryHotswapDeployment( sdkProvider: SdkProvider, assetParams: { [key: string]: string }, cloudFormationStack: CloudFormationStack, stackArtifact: cxapi.CloudFormationStackArtifact, + hotswapMode: HotswapMode, ): Promise { // resolve the environment, so we can substitute things like AWS::Region in CFN expressions const resolvedEnv = await sdkProvider.resolveEnvironment(stackArtifact.environment); @@ -47,13 +70,17 @@ export async function tryHotswapDeployment( const currentTemplate = await loadCurrentTemplateWithNestedStacks(stackArtifact, sdk); const stackChanges = cfn_diff.diffTemplate(currentTemplate.deployedTemplate, stackArtifact.template); - const hotswappableChanges = await findAllHotswappableChanges( + const { hotswappableChanges, nonHotswappableChanges } = await classifyResourceChanges( stackChanges, evaluateCfnTemplate, sdk, currentTemplate.nestedStackNames, ); - if (!hotswappableChanges) { - // this means there were changes to the template that cannot be short-circuited - return undefined; + logNonHotswappableChanges(nonHotswappableChanges, hotswapMode); + + // preserve classic hotswap behavior + if (hotswapMode === HotswapMode.FALL_BACK) { + if (nonHotswappableChanges.length > 0) { + return undefined; + } } // apply the short-circuitable changes @@ -62,84 +89,78 @@ export async function tryHotswapDeployment( return { noOp: hotswappableChanges.length === 0, stackArn: cloudFormationStack.stackId, outputs: cloudFormationStack.outputs }; } -async function findAllHotswappableChanges( +/** + * Classifies all changes to all resources as either hotswappable or not. + * Metadata changes are excluded from the list of (non)hotswappable resources. + */ +async function classifyResourceChanges( stackChanges: cfn_diff.TemplateDiff, evaluateCfnTemplate: EvaluateCloudFormationTemplate, sdk: ISDK, nestedStackNames: { [nestedStackName: string]: NestedStackNames }, -): Promise { - // Skip hotswap if there is any change on stack outputs - if (stackChanges.outputs.differenceCount > 0) { - return undefined; - } - +): Promise { const resourceDifferences = getStackResourceDifferences(stackChanges); - let foundNonHotswappableChange = false; - const promises: Array<() => Array>> = []; - const hotswappableResources = new Array(); - + const promises: Array<() => Promise> = []; + const hotswappableResources = new Array(); + const nonHotswappableResources = new Array(); + for (const logicalId of Object.keys(stackChanges.outputs.changes)) { + nonHotswappableResources.push({ + hotswappable: false, + reason: 'output was changed', + logicalId, + rejectedChanges: [], + resourceType: 'Stack Output', + }); + } // gather the results of the detector functions for (const [logicalId, change] of Object.entries(resourceDifferences)) { if (change.newValue?.Type === 'AWS::CloudFormation::Stack' && change.oldValue?.Type === 'AWS::CloudFormation::Stack') { const nestedHotswappableResources = await findNestedHotswappableChanges(logicalId, change, nestedStackNames, evaluateCfnTemplate, sdk); - if (!nestedHotswappableResources) { - return undefined; - } - hotswappableResources.push(...nestedHotswappableResources); + hotswappableResources.push(...nestedHotswappableResources.hotswappableChanges); + nonHotswappableResources.push(...nestedHotswappableResources.nonHotswappableChanges); + continue; } - const resourceHotswapEvaluation = isCandidateForHotswapping(change); + const hotswappableChangeCandidate = isCandidateForHotswapping(change, logicalId); + // we don't need to run this through the detector functions, we can already judge this + if ('hotswappable' in hotswappableChangeCandidate) { + if (!hotswappableChangeCandidate.hotswappable) { + nonHotswappableResources.push(hotswappableChangeCandidate); + } - if (resourceHotswapEvaluation === ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT) { - foundNonHotswappableChange = true; - } else if (resourceHotswapEvaluation === ChangeHotswapImpact.IRRELEVANT) { - // empty 'if' just for flow-aware typing to kick in... + continue; + } + + const resourceType: string = hotswappableChangeCandidate.newValue.Type; + if (resourceType in RESOURCE_DETECTORS) { + // run detector functions lazily to prevent unhandled promise rejections + promises.push(() => RESOURCE_DETECTORS[resourceType](logicalId, hotswappableChangeCandidate, evaluateCfnTemplate)); } else { - // run isHotswappable* functions lazily to prevent unhandled rejections - promises.push(() => [ - isHotswappableLambdaFunctionChange(logicalId, resourceHotswapEvaluation, evaluateCfnTemplate), - isHotswappableStateMachineChange(logicalId, resourceHotswapEvaluation, evaluateCfnTemplate), - isHotswappableEcsServiceChange(logicalId, resourceHotswapEvaluation, evaluateCfnTemplate), - isHotswappableS3BucketDeploymentChange(logicalId, resourceHotswapEvaluation, evaluateCfnTemplate), - isHotswappableCodeBuildProjectChange(logicalId, resourceHotswapEvaluation, evaluateCfnTemplate), - isHotswappableAppSyncChange(logicalId, resourceHotswapEvaluation, evaluateCfnTemplate), - ]); + reportNonHotswappableChange(nonHotswappableResources, hotswappableChangeCandidate, undefined, 'This resource type is not supported for hotswap deployments'); } } // resolve all detector results - const changesDetectionResults: Array> = []; + const changesDetectionResults: Array = []; for (const detectorResultPromises of promises) { - const hotswapDetectionResults = await Promise.all(detectorResultPromises()); + const hotswapDetectionResults = await Promise.all(await detectorResultPromises()); changesDetectionResults.push(hotswapDetectionResults); } - for (const hotswapDetectionResults of changesDetectionResults) { - const perChangeHotswappableResources = new Array(); - - for (const result of hotswapDetectionResults) { - if (typeof result !== 'string') { - perChangeHotswappableResources.push(result); - } - } - - // if we found any hotswappable changes, return now - if (perChangeHotswappableResources.length > 0) { - hotswappableResources.push(...perChangeHotswappableResources); - continue; - } - - // no hotswappable changes found, so at least one IRRELEVANT means we can ignore this change; - // otherwise, all answers are REQUIRES_FULL_DEPLOYMENT, so this means we can't hotswap this change, - // and have to do a full deployment instead - if (!hotswapDetectionResults.some(hdr => hdr === ChangeHotswapImpact.IRRELEVANT)) { - foundNonHotswappableChange = true; + for (const resourceDetectionResults of changesDetectionResults) { + for (const propertyResult of resourceDetectionResults) { + propertyResult.hotswappable ? + hotswappableResources.push(propertyResult) : + nonHotswappableResources.push(propertyResult); } } - return foundNonHotswappableChange ? undefined : hotswappableResources; + return { + hotswappableChanges: hotswappableResources, + nonHotswappableChanges: nonHotswappableResources, + }; } /** @@ -195,11 +216,19 @@ async function findNestedHotswappableChanges( nestedStackNames: { [nestedStackName: string]: NestedStackNames }, evaluateCfnTemplate: EvaluateCloudFormationTemplate, sdk: ISDK, -): Promise { +): Promise { const nestedStackName = nestedStackNames[logicalId].nestedStackPhysicalName; - // the stack name could not be found in CFN, so this is a newly created nested stack if (!nestedStackName) { - return undefined; + return { + hotswappableChanges: [], + nonHotswappableChanges: [{ + hotswappable: false, + logicalId, + reason: `physical name for AWS::CloudFormation::Stack '${logicalId}' could not be found in CloudFormation, so this is a newly created nested stack and cannot be hotswapped`, + rejectedChanges: [], + resourceType: 'AWS::CloudFormation::Stack', + }], + }; } const nestedStackParameters = await evaluateCfnTemplate.evaluateCfnExpression(change.newValue?.Properties?.Parameters); @@ -211,7 +240,7 @@ async function findNestedHotswappableChanges( change.oldValue?.Properties?.NestedTemplate, change.newValue?.Properties?.NestedTemplate, ); - return findAllHotswappableChanges(nestedDiff, evaluateNestedCfnTemplate, sdk, nestedStackNames[logicalId].nestedChildStackNames); + return classifyResourceChanges(nestedDiff, evaluateNestedCfnTemplate, sdk, nestedStackNames[logicalId].nestedChildStackNames); } /** Returns 'true' if a pair of changes is for the same resource. */ @@ -241,52 +270,109 @@ function makeRenameDifference( } /** - * returns `ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT` if a resource was deleted, or a change that we cannot short-circuit occured. - * Returns `ChangeHotswapImpact.IRRELEVANT` if a change that does not impact shortcircuiting occured, such as a metadata change. + * Returns a `HotswappableChangeCandidate` if the change is hotswappable + * Returns an empty `HotswappableChange` if the change is to CDK::Metadata + * Returns a `NonHotswappableChange` if the change is not hotswappable */ -function isCandidateForHotswapping(change: cfn_diff.ResourceDifference): HotswappableChangeCandidate | ChangeHotswapImpact { +function isCandidateForHotswapping( + change: cfn_diff.ResourceDifference, logicalId: string, +): HotswappableChange | NonHotswappableChange | HotswappableChangeCandidate { // a resource has been removed OR a resource has been added; we can't short-circuit that change - if (!change.newValue || !change.oldValue) { - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; + if (!change.oldValue) { + return { + hotswappable: false, + resourceType: change.newValue!.Type, + logicalId, + rejectedChanges: [], + reason: `resource '${logicalId}' was created by this deployment`, + }; + } else if (!change.newValue) { + return { + hotswappable: false, + resourceType: change.oldValue!.Type, + logicalId, + rejectedChanges: [], + reason: `resource '${logicalId}' was destroyed by this deployment`, + }; } // a resource has had its type changed - if (change.newValue.Type !== change.oldValue.Type) { - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; - } - - // Ignore Metadata changes - if (change.newValue.Type === 'AWS::CDK::Metadata') { - return ChangeHotswapImpact.IRRELEVANT; + if (change.newValue?.Type !== change.oldValue?.Type) { + return { + hotswappable: false, + resourceType: change.newValue?.Type, + logicalId, + rejectedChanges: [], + reason: `resource '${logicalId}' had its type changed from '${change.oldValue?.Type}' to '${change.newValue?.Type}'`, + }; } return { + logicalId, + oldValue: change.oldValue, newValue: change.newValue, propertyUpdates: change.propertyUpdates, }; } -async function applyAllHotswappableChanges(sdk: ISDK, hotswappableChanges: HotswapOperation[]): Promise { - print(`\n${ICON} hotswapping resources:`); +async function applyAllHotswappableChanges(sdk: ISDK, hotswappableChanges: HotswappableChange[]): Promise { + if (hotswappableChanges.length > 0) { + print(`\n${ICON} hotswapping resources:`); + } return Promise.all(hotswappableChanges.map(hotswapOperation => { return applyHotswappableChange(sdk, hotswapOperation); })); } -async function applyHotswappableChange(sdk: ISDK, hotswapOperation: HotswapOperation): Promise { +async function applyHotswappableChange(sdk: ISDK, hotswapOperation: HotswappableChange): Promise { // note the type of service that was successfully hotswapped in the User-Agent const customUserAgent = `cdk-hotswap/success-${hotswapOperation.service}`; sdk.appendCustomUserAgent(customUserAgent); - try { - for (const name of hotswapOperation.resourceNames) { - print(` ${ICON} %s`, chalk.bold(name)); - } - return await hotswapOperation.apply(sdk); - } finally { - for (const name of hotswapOperation.resourceNames) { - print(`${ICON} %s %s`, chalk.bold(name), chalk.green('hotswapped!')); + for (const name of hotswapOperation.resourceNames) { + print(` ${ICON} %s`, chalk.bold(name)); + } + + // if the SDK call fails, an error will be thrown by the SDK + // and will prevent the green 'hotswapped!' text from being displayed + await hotswapOperation.apply(sdk); + + for (const name of hotswapOperation.resourceNames) { + print(`${ICON} %s %s`, chalk.bold(name), chalk.green('hotswapped!')); + } + + sdk.removeCustomUserAgent(customUserAgent); +} + +function logNonHotswappableChanges(nonHotswappableChanges: NonHotswappableChange[], hotswapMode: HotswapMode): void { + if (nonHotswappableChanges.length === 0) { + return; + } + /** + * EKS Services can have a task definition that doesn't refer to the task definition being updated. + * We have to log this as a non-hotswappable change to the task definition, but when we do, + * we wind up hotswapping the task definition and logging it as a non-hotswappable change. + * + * This logic prevents us from logging that change as non-hotswappable when we hotswap it. + */ + if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + nonHotswappableChanges = nonHotswappableChanges.filter((change) => change.hotswapOnlyVisible === true); + + if (nonHotswappableChanges.length === 0) { + return; } - sdk.removeCustomUserAgent(customUserAgent); } + if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + print('\n%s %s', chalk.red('⚠️'), chalk.red('The following non-hotswappable changes were found. To reconcile these using CloudFormation, specify --hotswap-fallback')); + } else { + print('\n%s %s', chalk.red('⚠️'), chalk.red('The following non-hotswappable changes were found:')); + } + + for (const change of nonHotswappableChanges) { + change.rejectedChanges.length > 0 ? + print(' logicalID: %s, type: %s, rejected changes: %s, reason: %s', chalk.bold(change.logicalId), chalk.bold(change.resourceType), chalk.bold(change.rejectedChanges), chalk.red(change.reason)): + print(' logicalID: %s, type: %s, reason: %s', chalk.bold(change.logicalId), chalk.bold(change.resourceType), chalk.red(change.reason)); + } + + print(''); // newline } diff --git a/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts b/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts index f7e4570a77b98..b7f95d6edeb0e 100644 --- a/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts +++ b/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts @@ -1,7 +1,6 @@ -import * as AWS from 'aws-sdk'; import { ISDK } from '../aws-auth'; import { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; -import { ChangeHotswapImpact, ChangeHotswapResult, HotswapOperation, HotswappableChangeCandidate, lowerCaseFirstCharacter, transformObjectKeys } from './common'; +import { ChangeHotswapResult, classifyChanges, HotswappableChangeCandidate, lowerCaseFirstCharacter, reportNonHotswappableChange, transformObjectKeys } from './common'; export async function isHotswappableAppSyncChange( logicalId: string, change: HotswappableChangeCandidate, evaluateCfnTemplate: EvaluateCloudFormationTemplate, @@ -10,73 +9,65 @@ export async function isHotswappableAppSyncChange( const isFunction = change.newValue.Type === 'AWS::AppSync::FunctionConfiguration'; if (!isResolver && !isFunction) { - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; + return []; } - for (const updatedPropName in change.propertyUpdates) { - if (updatedPropName !== 'RequestMappingTemplate' && updatedPropName !== 'ResponseMappingTemplate') { - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; - } - } - - const resourceProperties = change.newValue.Properties; - if (isResolver && resourceProperties?.Kind === 'PIPELINE') { - // Pipeline resolvers can't be hotswapped as they reference - // the FunctionId of the underlying functions, which can't be resolved. - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; - } - - const resourcePhysicalName = await evaluateCfnTemplate.establishResourcePhysicalName(logicalId, isFunction ? resourceProperties?.Name : undefined); - if (!resourcePhysicalName) { - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; + const ret: ChangeHotswapResult = []; + if (isResolver && change.newValue.Properties?.Kind === 'PIPELINE') { + reportNonHotswappableChange( + ret, + change, + undefined, + 'Pipeline resolvers cannot be hotswapped since they reference the FunctionId of the underlying functions, which cannot be resolved', + ); + return ret; } - const evaluatedResourceProperties = await evaluateCfnTemplate.evaluateCfnExpression(resourceProperties); - const sdkCompatibleResourceProperties = transformObjectKeys(evaluatedResourceProperties, lowerCaseFirstCharacter); - - if (isResolver) { - // Resolver physical name is the ARN in the format: - // arn:aws:appsync:us-east-1:111111111111:apis//types//resolvers/. - // We'll use `.` as the resolver name. - const arnParts = resourcePhysicalName.split('/'); - const resolverName = `${arnParts[3]}.${arnParts[5]}`; - return new ResolverHotswapOperation(resolverName, sdkCompatibleResourceProperties); - } else { - return new FunctionHotswapOperation(resourcePhysicalName, sdkCompatibleResourceProperties); - } -} + const classifiedChanges = classifyChanges(change, ['RequestMappingTemplate', 'ResponseMappingTemplate']); + classifiedChanges.reportNonHotswappablePropertyChanges(ret); -class ResolverHotswapOperation implements HotswapOperation { - public readonly service = 'appsync' - public readonly resourceNames: string[]; - - constructor(resolverName: string, private readonly updateResolverRequest: AWS.AppSync.UpdateResolverRequest) { - this.resourceNames = [`AppSync resolver '${resolverName}'`]; - } - - public async apply(sdk: ISDK): Promise { - return sdk.appsync().updateResolver(this.updateResolverRequest).promise(); - } -} + const namesOfHotswappableChanges = Object.keys(classifiedChanges.hotswappableProps); + if (namesOfHotswappableChanges.length > 0) { + let physicalName: string | undefined = undefined; + const arn = await evaluateCfnTemplate.establishResourcePhysicalName(logicalId, isFunction ? change.newValue.Properties?.Name : undefined); + if (isResolver) { + const arnParts = arn?.split('/'); + physicalName = arnParts ? `${arnParts[3]}.${arnParts[5]}` : undefined; + } else { + physicalName = arn; + } + ret.push({ + hotswappable: true, + resourceType: change.newValue.Type, + propsChanged: namesOfHotswappableChanges, + service: 'appsync', + resourceNames: [`${change.newValue.Type} '${physicalName}'`], + apply: async (sdk: ISDK) => { + if (!physicalName) { + return; + } -class FunctionHotswapOperation implements HotswapOperation { - public readonly service = 'appsync' - public readonly resourceNames: string[]; + const sdkProperties: { [name: string]: any } = { + ...change.oldValue.Properties, + requestMappingTemplate: change.newValue.Properties?.RequestMappingTemplate, + responseMappingTemplate: change.newValue.Properties?.ResponseMappingTemplate, + }; + const evaluatedResourceProperties = await evaluateCfnTemplate.evaluateCfnExpression(sdkProperties); + const sdkRequestObject = transformObjectKeys(evaluatedResourceProperties, lowerCaseFirstCharacter); - constructor( - private readonly functionName: string, - private readonly updateFunctionRequest: Omit, - ) { - this.resourceNames = [`AppSync function '${functionName}'`]; + if (isResolver) { + await sdk.appsync().updateResolver(sdkRequestObject).promise(); + } else { + const { functions } = await sdk.appsync().listFunctions({ apiId: sdkRequestObject.apiId }).promise(); + const { functionId } = functions?.find(fn => fn.name === physicalName) ?? {}; + await sdk.appsync().updateFunction({ + ...sdkRequestObject, + functionId: functionId!, + }).promise(); + } + }, + }); } - public async apply(sdk: ISDK): Promise { - const { functions } = await sdk.appsync().listFunctions({ apiId: this.updateFunctionRequest.apiId }).promise(); - const { functionId } = functions?.find(fn => fn.name === this.functionName) ?? {}; - const request = { - ...this.updateFunctionRequest, - functionId: functionId!, - }; - return sdk.appsync().updateFunction(request).promise(); - } + return ret; } diff --git a/packages/aws-cdk/lib/api/hotswap/code-build-projects.ts b/packages/aws-cdk/lib/api/hotswap/code-build-projects.ts index 55270d29f8ceb..1f163c6485dd3 100644 --- a/packages/aws-cdk/lib/api/hotswap/code-build-projects.ts +++ b/packages/aws-cdk/lib/api/hotswap/code-build-projects.ts @@ -1,62 +1,63 @@ import * as AWS from 'aws-sdk'; import { ISDK } from '../aws-auth'; import { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; -import { ChangeHotswapImpact, ChangeHotswapResult, HotswapOperation, HotswappableChangeCandidate, lowerCaseFirstCharacter, transformObjectKeys } from './common'; +import { ChangeHotswapResult, classifyChanges, HotswappableChangeCandidate, lowerCaseFirstCharacter, transformObjectKeys } from './common'; export async function isHotswappableCodeBuildProjectChange( logicalId: string, change: HotswappableChangeCandidate, evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): Promise { if (change.newValue.Type !== 'AWS::CodeBuild::Project') { - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; + return []; } - const updateProjectInput: AWS.CodeBuild.UpdateProjectInput = { - name: '', - }; - for (const updatedPropName in change.propertyUpdates) { - const updatedProp = change.propertyUpdates[updatedPropName]; - switch (updatedPropName) { - case 'Source': - updateProjectInput.source = transformObjectKeys( - await evaluateCfnTemplate.evaluateCfnExpression(updatedProp.newValue), - convertSourceCloudformationKeyToSdkKey, - ); - break; - case 'Environment': - updateProjectInput.environment = await transformObjectKeys( - await evaluateCfnTemplate.evaluateCfnExpression(updatedProp.newValue), - lowerCaseFirstCharacter, - ); - break; - case 'SourceVersion': - updateProjectInput.sourceVersion = await evaluateCfnTemplate.evaluateCfnExpression(updatedProp.newValue); - break; - default: - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; - } - } + const ret: ChangeHotswapResult = []; - const projectName = await evaluateCfnTemplate.establishResourcePhysicalName(logicalId, change.newValue.Properties?.Name); - if (!projectName) { - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; - } - updateProjectInput.name = projectName; - return new ProjectHotswapOperation(updateProjectInput); -} + const classifiedChanges = classifyChanges(change, ['Source', 'Environment', 'SourceVersion']); + classifiedChanges.reportNonHotswappablePropertyChanges(ret); + if (classifiedChanges.namesOfHotswappableProps.length > 0) { + const updateProjectInput: AWS.CodeBuild.UpdateProjectInput = { + name: '', + }; + const projectName = await evaluateCfnTemplate.establishResourcePhysicalName(logicalId, change.newValue.Properties?.Name); + ret.push({ + hotswappable: true, + resourceType: change.newValue.Type, + propsChanged: classifiedChanges.namesOfHotswappableProps, + service: 'codebuild', + resourceNames: [`CodeBuild Project '${projectName}'`], + apply: async (sdk: ISDK) => { + if (!projectName) { + return; + } + updateProjectInput.name = projectName; -class ProjectHotswapOperation implements HotswapOperation { - public readonly service = 'codebuild' - public readonly resourceNames: string[]; + for (const updatedPropName in change.propertyUpdates) { + const updatedProp = change.propertyUpdates[updatedPropName]; + switch (updatedPropName) { + case 'Source': + updateProjectInput.source = transformObjectKeys( + await evaluateCfnTemplate.evaluateCfnExpression(updatedProp.newValue), + convertSourceCloudformationKeyToSdkKey, + ); + break; + case 'Environment': + updateProjectInput.environment = await transformObjectKeys( + await evaluateCfnTemplate.evaluateCfnExpression(updatedProp.newValue), + lowerCaseFirstCharacter, + ); + break; + case 'SourceVersion': + updateProjectInput.sourceVersion = await evaluateCfnTemplate.evaluateCfnExpression(updatedProp.newValue); + break; + } + } - constructor( - private readonly updateProjectInput: AWS.CodeBuild.UpdateProjectInput, - ) { - this.resourceNames = [`CodeBuild project '${updateProjectInput.name}'`]; + await sdk.codeBuild().updateProject(updateProjectInput).promise(); + }, + }); } - public async apply(sdk: ISDK): Promise { - return sdk.codeBuild().updateProject(this.updateProjectInput).promise(); - } + return ret; } function convertSourceCloudformationKeyToSdkKey(key: string): string { diff --git a/packages/aws-cdk/lib/api/hotswap/common.ts b/packages/aws-cdk/lib/api/hotswap/common.ts index 8f4c17ccc21b2..d72d7b270fc85 100644 --- a/packages/aws-cdk/lib/api/hotswap/common.ts +++ b/packages/aws-cdk/lib/api/hotswap/common.ts @@ -3,10 +3,10 @@ import { ISDK } from '../aws-auth'; export const ICON = '✨'; -/** - * An interface that represents a change that can be deployed in a short-circuit manner. - */ -export interface HotswapOperation { +export interface HotswappableChange { + readonly hotswappable: true; + readonly resourceType: string; + readonly propsChanged: Array; /** * The name of the service being hotswapped. * Used to set a custom User-Agent for SDK calls. @@ -18,44 +18,79 @@ export interface HotswapOperation { */ readonly resourceNames: string[]; - apply(sdk: ISDK): Promise; + readonly apply: (sdk: ISDK) => Promise; } -/** - * An enum that represents the result of detection whether a given change can be hotswapped. - */ -export enum ChangeHotswapImpact { +export interface NonHotswappableChange { + readonly hotswappable: false; + readonly resourceType: string; + readonly rejectedChanges: Array; + readonly logicalId: string; /** - * This result means that the given change cannot be hotswapped, - * and requires a full deployment. + * Tells the user exactly why this change was deemed non-hotswappable and what its logical ID is. + * If not specified, `reason` will be autofilled to state that the properties listed in `rejectedChanges` are not hotswappable. */ - REQUIRES_FULL_DEPLOYMENT = 'requires-full-deployment', - + readonly reason?: string; /** - * This result means that the given change can be safely be ignored when determining - * whether the given Stack can be hotswapped or not - * (for example, it's a change to the CDKMetadata resource). + * Whether or not to show this change when listing non-hotswappable changes in HOTSWAP_ONLY mode. Does not affect + * listing in FALL_BACK mode. + * + * @default true */ - IRRELEVANT = 'irrelevant', + readonly hotswapOnlyVisible?: boolean; } -export type ChangeHotswapResult = HotswapOperation | ChangeHotswapImpact; +export type ChangeHotswapResult = Array; + +export interface ClassifiedResourceChanges { + hotswappableChanges: HotswappableChange[]; + nonHotswappableChanges: NonHotswappableChange[]; +} + +export enum HotswapMode { + /** + * Will fall back to CloudFormation when a non-hotswappable change is detected + */ + FALL_BACK = 'fall-back', + + /** + * Will not fall back to CloudFormation when a non-hotswappable change is detected + */ + HOTSWAP_ONLY = 'hotswap-only', + + /** + * Will not attempt to hotswap anything and instead go straight to CloudFormation + */ + FULL_DEPLOYMENT = 'full-deployment', +} /** * Represents a change that can be hotswapped. */ export class HotswappableChangeCandidate { /** - * The value the resource is being updated to. + * The logical ID of the resource which is being changed + */ + public readonly logicalId: string; + + /** + * The value the resource is being updated from + */ + public readonly oldValue: cfn_diff.Resource; + + /** + * The value the resource is being updated to */ public readonly newValue: cfn_diff.Resource; /** - * The changes made to the resource properties. + * The changes made to the resource properties */ - public readonly propertyUpdates: { [key: string]: cfn_diff.PropertyDifference }; + public readonly propertyUpdates: PropDiffs; - public constructor(newValue: cfn_diff.Resource, propertyUpdates: { [key: string]: cfn_diff.PropertyDifference }) { + public constructor(logicalId: string, oldValue: cfn_diff.Resource, newValue: cfn_diff.Resource, propertyUpdates: PropDiffs) { + this.logicalId = logicalId; + this.oldValue = oldValue; this.newValue = newValue; this.propertyUpdates = propertyUpdates; } @@ -99,3 +134,82 @@ export function transformObjectKeys(val: any, transform: (str: string) => string export function lowerCaseFirstCharacter(str: string): string { return str.length > 0 ? `${str[0].toLowerCase()}${str.slice(1)}` : str; } + +export type PropDiffs = Record>; + +export class ClassifiedChanges { + public constructor( + public readonly change: HotswappableChangeCandidate, + public readonly hotswappableProps: PropDiffs, + public readonly nonHotswappableProps: PropDiffs, + ) { } + + public reportNonHotswappablePropertyChanges(ret: ChangeHotswapResult):void { + const nonHotswappablePropNames = Object.keys(this.nonHotswappableProps); + if (nonHotswappablePropNames.length > 0) { + const tagOnlyChange = nonHotswappablePropNames.length === 1 && nonHotswappablePropNames[0] === 'Tags'; + reportNonHotswappableChange( + ret, + this.change, + this.nonHotswappableProps, + tagOnlyChange ? 'Tags are not hotswappable' : `resource properties '${nonHotswappablePropNames}' are not hotswappable on this resource type`, + ); + } + } + + public get namesOfHotswappableProps(): string[] { + return Object.keys(this.hotswappableProps); + } +} + +export function classifyChanges( + xs: HotswappableChangeCandidate, + hotswappablePropNames: string[], +): ClassifiedChanges { + const hotswappableProps: PropDiffs = {}; + const nonHotswappableProps: PropDiffs = {}; + + for (const [name, propDiff] of Object.entries(xs.propertyUpdates)) { + if (hotswappablePropNames.includes(name)) { + hotswappableProps[name] = propDiff; + } else { + nonHotswappableProps[name] = propDiff; + } + } + + return new ClassifiedChanges(xs, hotswappableProps, nonHotswappableProps); +} + +export function reportNonHotswappableChange( + ret: ChangeHotswapResult, + change: HotswappableChangeCandidate, + nonHotswappableProps?: PropDiffs, + reason?: string, + hotswapOnlyVisible?: boolean, +): void { + let hotswapOnlyVisibility = true; + if (hotswapOnlyVisible === false) { + hotswapOnlyVisibility = false; + } + ret.push({ + hotswappable: false, + rejectedChanges: Object.keys(nonHotswappableProps ?? change.propertyUpdates), + logicalId: change.logicalId, + resourceType: change.newValue.Type, + reason, + hotswapOnlyVisible: hotswapOnlyVisibility, + }); +} + +export function reportNonHotswappableResource( + change: HotswappableChangeCandidate, + reason?: string, +): ChangeHotswapResult { + return [{ + hotswappable: false, + rejectedChanges: Object.keys(change.propertyUpdates), + logicalId: change.logicalId, + resourceType: change.newValue.Type, + reason, + }]; +} diff --git a/packages/aws-cdk/lib/api/hotswap/ecs-services.ts b/packages/aws-cdk/lib/api/hotswap/ecs-services.ts index 47bba75413e8a..b8289b89d0e78 100644 --- a/packages/aws-cdk/lib/api/hotswap/ecs-services.ts +++ b/packages/aws-cdk/lib/api/hotswap/ecs-services.ts @@ -1,29 +1,23 @@ import * as AWS from 'aws-sdk'; import { ISDK } from '../aws-auth'; import { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; -import { ChangeHotswapImpact, ChangeHotswapResult, HotswapOperation, HotswappableChangeCandidate, lowerCaseFirstCharacter, transformObjectKeys } from './common'; +import { ChangeHotswapResult, classifyChanges, HotswappableChangeCandidate, lowerCaseFirstCharacter, reportNonHotswappableChange, transformObjectKeys } from './common'; export async function isHotswappableEcsServiceChange( logicalId: string, change: HotswappableChangeCandidate, evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): Promise { - // the only resource change we should allow is an ECS TaskDefinition + // the only resource change we can evaluate here is an ECS TaskDefinition if (change.newValue.Type !== 'AWS::ECS::TaskDefinition') { - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; + return []; } - for (const updatedPropName in change.propertyUpdates) { - // We only allow a change in the ContainerDefinitions of the TaskDefinition for now - - // it contains the image and environment variables, so seems like a safe bet for now. - // We might revisit this decision in the future though! - if (updatedPropName !== 'ContainerDefinitions') { - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; - } - const containerDefinitionsDifference = (change.propertyUpdates)[updatedPropName]; - if (containerDefinitionsDifference.newValue === undefined) { - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; - } - } - // at this point, we know the TaskDefinition can be hotswapped + const ret: ChangeHotswapResult = []; + + // We only allow a change in the ContainerDefinitions of the TaskDefinition for now - + // it contains the image and environment variables, so seems like a safe bet for now. + // We might revisit this decision in the future though! + const classifiedChanges = classifyChanges(change, ['ContainerDefinitions']); + classifiedChanges.reportNonHotswappablePropertyChanges(ret); // find all ECS Services that reference the TaskDefinition that changed const resourcesReferencingTaskDef = evaluateCfnTemplate.findReferencesTo(logicalId); @@ -35,21 +29,161 @@ export async function isHotswappableEcsServiceChange( ecsServicesReferencingTaskDef.push({ serviceArn }); } } - if (ecsServicesReferencingTaskDef.length === 0 || - resourcesReferencingTaskDef.length > ecsServicesReferencingTaskDef.length) { - // if there are either no resources referencing the TaskDefinition, - // or something besides an ECS Service is referencing it, - // hotswap is not possible - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; + if (ecsServicesReferencingTaskDef.length === 0) { + // if there are no resources referencing the TaskDefinition, + // hotswap is not possible in FALL_BACK mode + reportNonHotswappableChange(ret, change, undefined, 'No ECS services reference the changed task definition', false); + } if (resourcesReferencingTaskDef.length > ecsServicesReferencingTaskDef.length) { + // if something besides an ECS Service is referencing the TaskDefinition, + // hotswap is not possible in FALL_BACK mode + const nonEcsServiceTaskDefRefs = resourcesReferencingTaskDef.filter(r => r.Type !== 'AWS::ECS::Service'); + for (const taskRef of nonEcsServiceTaskDefRefs) { + reportNonHotswappableChange(ret, change, undefined, `A resource '${taskRef.LogicalId}' with Type '${taskRef.Type}' that is not an ECS Service was found referencing the changed TaskDefinition '${logicalId}'`); + } } - const taskDefinitionResource = change.newValue.Properties; + const namesOfHotswappableChanges = Object.keys(classifiedChanges.hotswappableProps); + if (namesOfHotswappableChanges.length > 0) { + const taskDefinitionResource = await prepareTaskDefinitionChange(evaluateCfnTemplate, logicalId, change); + ret.push({ + hotswappable: true, + resourceType: change.newValue.Type, + propsChanged: namesOfHotswappableChanges, + service: 'ecs-service', + resourceNames: [ + `ECS Task Definition '${await taskDefinitionResource.Family}'`, + ...ecsServicesReferencingTaskDef.map(ecsService => `ECS Service '${ecsService.serviceArn.split('/')[2]}'`), + ], + apply: async (sdk: ISDK) => { + // Step 1 - update the changed TaskDefinition, creating a new TaskDefinition Revision + // we need to lowercase the evaluated TaskDef from CloudFormation, + // as the AWS SDK uses lowercase property names for these + + // The SDK requires more properties here than its worth doing explicit typing for + // instead, just use all the old values in the diff to fill them in implicitly + const lowercasedTaskDef = transformObjectKeys(taskDefinitionResource, lowerCaseFirstCharacter, { + // All the properties that take arbitrary string as keys i.e. { "string" : "string" } + // https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_RegisterTaskDefinition.html#API_RegisterTaskDefinition_RequestSyntax + ContainerDefinitions: { + DockerLabels: true, + FirelensConfiguration: { + Options: true, + }, + LogConfiguration: { + Options: true, + }, + }, + Volumes: { + DockerVolumeConfiguration: { + DriverOpts: true, + Labels: true, + }, + }, + }); + const registerTaskDefResponse = await sdk.ecs().registerTaskDefinition(lowercasedTaskDef).promise(); + const taskDefRevArn = registerTaskDefResponse.taskDefinition?.taskDefinitionArn; + + // Step 2 - update the services using that TaskDefinition to point to the new TaskDefinition Revision + const servicePerClusterUpdates: { [cluster: string]: Array<{ promise: Promise, ecsService: EcsService }> } = {}; + for (const ecsService of ecsServicesReferencingTaskDef) { + const clusterName = ecsService.serviceArn.split('/')[1]; + + const existingClusterPromises = servicePerClusterUpdates[clusterName]; + let clusterPromises: Array<{ promise: Promise, ecsService: EcsService }>; + if (existingClusterPromises) { + clusterPromises = existingClusterPromises; + } else { + clusterPromises = []; + servicePerClusterUpdates[clusterName] = clusterPromises; + } + // Forcing New Deployment and setting Minimum Healthy Percent to 0. + // As CDK HotSwap is development only, this seems the most efficient way to ensure all tasks are replaced immediately, regardless of original amount. + clusterPromises.push({ + promise: sdk.ecs().updateService({ + service: ecsService.serviceArn, + taskDefinition: taskDefRevArn, + cluster: clusterName, + forceNewDeployment: true, + deploymentConfiguration: { + minimumHealthyPercent: 0, + }, + }).promise(), + ecsService: ecsService, + }); + } + await Promise.all(Object.values(servicePerClusterUpdates) + .map(clusterUpdates => { + return Promise.all(clusterUpdates.map(serviceUpdate => serviceUpdate.promise)); + }), + ); + + // Step 3 - wait for the service deployments triggered in Step 2 to finish + // configure a custom Waiter + (sdk.ecs() as any).api.waiters.deploymentToFinish = { + name: 'DeploymentToFinish', + operation: 'describeServices', + delay: 10, + maxAttempts: 60, + acceptors: [ + { + matcher: 'pathAny', + argument: 'failures[].reason', + expected: 'MISSING', + state: 'failure', + }, + { + matcher: 'pathAny', + argument: 'services[].status', + expected: 'DRAINING', + state: 'failure', + }, + { + matcher: 'pathAny', + argument: 'services[].status', + expected: 'INACTIVE', + state: 'failure', + }, + { + matcher: 'path', + argument: "length(services[].deployments[? status == 'PRIMARY' && runningCount < desiredCount][]) == `0`", + expected: true, + state: 'success', + }, + ], + }; + // create a custom Waiter that uses the deploymentToFinish configuration added above + const deploymentWaiter = new (AWS as any).ResourceWaiter(sdk.ecs(), 'deploymentToFinish'); + // wait for all of the waiters to finish + await Promise.all(Object.entries(servicePerClusterUpdates).map(([clusterName, serviceUpdates]) => { + return deploymentWaiter.wait({ + cluster: clusterName, + services: serviceUpdates.map(serviceUpdate => serviceUpdate.ecsService.serviceArn), + }).promise(); + })); + }, + }); + } + + return ret; +} + +interface EcsService { + readonly serviceArn: string; +} + +async function prepareTaskDefinitionChange( + evaluateCfnTemplate: EvaluateCloudFormationTemplate, logicalId: string, change: HotswappableChangeCandidate, +) { + const taskDefinitionResource: { [name: string]: any } = { + ...change.oldValue.Properties, + ContainerDefinitions: change.newValue.Properties?.ContainerDefinitions, + }; // first, let's get the name of the family const familyNameOrArn = await evaluateCfnTemplate.establishResourcePhysicalName(logicalId, taskDefinitionResource?.Family); if (!familyNameOrArn) { - // if the Family property has not bee provided, and we can't find it in the current Stack, + // if the Family property has not been provided, and we can't find it in the current Stack, // this means hotswapping is not possible - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; + return; } // the physical name of the Task Definition in CloudFormation includes its current revision number at the end, // remove it if needed @@ -61,134 +195,11 @@ export async function isHotswappableEcsServiceChange( // otherwise, familyNameOrArn is just the simple name evaluated from the CloudFormation template : familyNameOrArn; // then, let's evaluate the body of the remainder of the TaskDef (without the Family property) - const evaluatedTaskDef = { + return { ...await evaluateCfnTemplate.evaluateCfnExpression({ ...(taskDefinitionResource ?? {}), Family: undefined, }), Family: family, }; - return new EcsServiceHotswapOperation(evaluatedTaskDef, ecsServicesReferencingTaskDef); -} - -interface EcsService { - readonly serviceArn: string; -} - -class EcsServiceHotswapOperation implements HotswapOperation { - public readonly service = 'ecs-service'; - public readonly resourceNames: string[] = []; - - constructor( - private readonly taskDefinitionResource: any, - private readonly servicesReferencingTaskDef: EcsService[], - ) { - this.resourceNames = servicesReferencingTaskDef.map(ecsService => - `ECS Service '${ecsService.serviceArn.split('/')[2]}'`); - } - - public async apply(sdk: ISDK): Promise { - // Step 1 - update the changed TaskDefinition, creating a new TaskDefinition Revision - // we need to lowercase the evaluated TaskDef from CloudFormation, - // as the AWS SDK uses lowercase property names for these - const lowercasedTaskDef = transformObjectKeys(this.taskDefinitionResource, lowerCaseFirstCharacter, { - // All the properties that take arbitrary string as keys i.e. { "string" : "string" } - // https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_RegisterTaskDefinition.html#API_RegisterTaskDefinition_RequestSyntax - ContainerDefinitions: { - DockerLabels: true, - FirelensConfiguration: { - Options: true, - }, - LogConfiguration: { - Options: true, - }, - }, - Volumes: { - DockerVolumeConfiguration: { - DriverOpts: true, - Labels: true, - }, - }, - }); - const registerTaskDefResponse = await sdk.ecs().registerTaskDefinition(lowercasedTaskDef).promise(); - const taskDefRevArn = registerTaskDefResponse.taskDefinition?.taskDefinitionArn; - - // Step 2 - update the services using that TaskDefinition to point to the new TaskDefinition Revision - const servicePerClusterUpdates: { [cluster: string]: Array<{ promise: Promise, ecsService: EcsService }> } = {}; - for (const ecsService of this.servicesReferencingTaskDef) { - const clusterName = ecsService.serviceArn.split('/')[1]; - - const existingClusterPromises = servicePerClusterUpdates[clusterName]; - let clusterPromises: Array<{ promise: Promise, ecsService: EcsService }>; - if (existingClusterPromises) { - clusterPromises = existingClusterPromises; - } else { - clusterPromises = []; - servicePerClusterUpdates[clusterName] = clusterPromises; - } - // Forcing New Deployment and setting Minimum Healthy Percent to 0. - // As CDK HotSwap is development only, this seems the most efficient way to ensure all tasks are replaced immediately, regardless of original amount. - clusterPromises.push({ - promise: sdk.ecs().updateService({ - service: ecsService.serviceArn, - taskDefinition: taskDefRevArn, - cluster: clusterName, - forceNewDeployment: true, - deploymentConfiguration: { - minimumHealthyPercent: 0, - }, - }).promise(), - ecsService: ecsService, - }); - } - await Promise.all(Object.values(servicePerClusterUpdates) - .map(clusterUpdates => { - return Promise.all(clusterUpdates.map(serviceUpdate => serviceUpdate.promise)); - }), - ); - - // Step 3 - wait for the service deployments triggered in Step 2 to finish - // configure a custom Waiter - (sdk.ecs() as any).api.waiters.deploymentToFinish = { - name: 'DeploymentToFinish', - operation: 'describeServices', - delay: 10, - maxAttempts: 60, - acceptors: [ - { - matcher: 'pathAny', - argument: 'failures[].reason', - expected: 'MISSING', - state: 'failure', - }, - { - matcher: 'pathAny', - argument: 'services[].status', - expected: 'DRAINING', - state: 'failure', - }, - { - matcher: 'pathAny', - argument: 'services[].status', - expected: 'INACTIVE', - state: 'failure', - }, - { - matcher: 'path', - argument: "length(services[].deployments[? status == 'PRIMARY' && runningCount < desiredCount][]) == `0`", - expected: true, - state: 'success', - }, - ], - }; - // create a custom Waiter that uses the deploymentToFinish configuration added above - const deploymentWaiter = new (AWS as any).ResourceWaiter(sdk.ecs(), 'deploymentToFinish'); - // wait for all of the waiters to finish - return Promise.all(Object.entries(servicePerClusterUpdates).map(([clusterName, serviceUpdates]) => { - return deploymentWaiter.wait({ - cluster: clusterName, - services: serviceUpdates.map(serviceUpdate => serviceUpdate.ecsService.serviceArn), - }).promise(); - })); - } } diff --git a/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts b/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts index 57ef14610fe7e..5be1b5f6c69d0 100644 --- a/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts +++ b/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts @@ -3,18 +3,12 @@ import * as AWS from 'aws-sdk'; import { flatMap } from '../../util'; import { ISDK } from '../aws-auth'; import { CfnEvaluationException, EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; -import { ChangeHotswapImpact, ChangeHotswapResult, HotswapOperation, HotswappableChangeCandidate } from './common'; +import { ChangeHotswapResult, classifyChanges, HotswappableChangeCandidate, PropDiffs } from './common'; // namespace object imports won't work in the bundle for function exports // eslint-disable-next-line @typescript-eslint/no-require-imports const archiver = require('archiver'); -/** - * Returns `ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT` if the change cannot be short-circuited, - * `ChangeHotswapImpact.IRRELEVANT` if the change is irrelevant from a short-circuit perspective - * (like a change to CDKMetadata), - * or a LambdaFunctionResource if the change can be short-circuited. - */ export async function isHotswappableLambdaFunctionChange( logicalId: string, change: HotswappableChangeCandidate, evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): Promise { @@ -23,76 +17,150 @@ export async function isHotswappableLambdaFunctionChange( // we will publish a new version when we get to hotswapping the actual Function this Version points to, below // (Versions can't be changed in CloudFormation anyway, they're immutable) if (change.newValue.Type === 'AWS::Lambda::Version') { - return ChangeHotswapImpact.IRRELEVANT; + return [{ + hotswappable: true, + resourceType: 'AWS::Lambda::Version', + resourceNames: [], + propsChanged: [], + service: 'lambda', + apply: async (_sdk: ISDK) => {}, + }]; } // we handle Aliases specially too if (change.newValue.Type === 'AWS::Lambda::Alias') { - return checkAliasHasVersionOnlyChange(change); + return classifyAliasChanges(change); } - const lambdaCodeChange = await isLambdaFunctionCodeOnlyChange(change, evaluateCfnTemplate); - if (typeof lambdaCodeChange === 'string') { - return lambdaCodeChange; + if (change.newValue.Type !== 'AWS::Lambda::Function') { + return []; } + const ret: ChangeHotswapResult = []; + const classifiedChanges = classifyChanges(change, ['Code', 'Environment', 'Description']); + classifiedChanges.reportNonHotswappablePropertyChanges(ret); + const functionName = await evaluateCfnTemplate.establishResourcePhysicalName(logicalId, change.newValue.Properties?.FunctionName); - if (!functionName) { - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; - } + const namesOfHotswappableChanges = Object.keys(classifiedChanges.hotswappableProps); + if (namesOfHotswappableChanges.length > 0) { + ret.push({ + hotswappable: true, + resourceType: change.newValue.Type, + propsChanged: namesOfHotswappableChanges, + service: 'lambda', + resourceNames: [ + `Lambda Function '${functionName}'`, + // add Version here if we're publishing a new one + ...await renderVersions(logicalId, evaluateCfnTemplate, [`Lambda Version for Function '${functionName}'`]), + // add any Aliases that we are hotswapping here + ...await renderAliases(logicalId, evaluateCfnTemplate, async (alias) => `Lambda Alias '${alias}' for Function '${functionName}'`), + ], + apply: async (sdk: ISDK) => { + const lambdaCodeChange = await evaluateLambdaFunctionProps( + classifiedChanges.hotswappableProps, change.newValue.Properties?.Runtime, evaluateCfnTemplate, + ); + if (lambdaCodeChange === undefined) { + return; + } - const functionArn = await evaluateCfnTemplate.evaluateCfnExpression({ - 'Fn::Sub': 'arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:' + functionName, - }); + if (!functionName) { + return; + } - // find all Lambda Versions that reference this Function - const versionsReferencingFunction = evaluateCfnTemplate.findReferencesTo(logicalId) - .filter(r => r.Type === 'AWS::Lambda::Version'); - // find all Lambda Aliases that reference the above Versions - const aliasesReferencingVersions = flatMap(versionsReferencingFunction, v => - evaluateCfnTemplate.findReferencesTo(v.LogicalId)); - const aliasesNames = await Promise.all(aliasesReferencingVersions.map(a => - evaluateCfnTemplate.evaluateCfnExpression(a.Properties?.Name))); + const { versionsReferencingFunction, aliasesNames } = await versionsAndAliases(logicalId, evaluateCfnTemplate); + const lambda = sdk.lambda(); + const operations: Promise[] = []; + + if (lambdaCodeChange.code !== undefined || lambdaCodeChange.configurations !== undefined) { + if (lambdaCodeChange.code !== undefined) { + const updateFunctionCodeResponse = await lambda.updateFunctionCode({ + FunctionName: functionName, + S3Bucket: lambdaCodeChange.code.s3Bucket, + S3Key: lambdaCodeChange.code.s3Key, + ImageUri: lambdaCodeChange.code.imageUri, + ZipFile: lambdaCodeChange.code.functionCodeZip, + S3ObjectVersion: lambdaCodeChange.code.s3ObjectVersion, + }).promise(); + + await waitForLambdasPropertiesUpdateToFinish(updateFunctionCodeResponse, lambda, functionName); + } - return new LambdaFunctionHotswapOperation({ - physicalName: functionName, - functionArn: functionArn, - resource: lambdaCodeChange, - publishVersion: versionsReferencingFunction.length > 0, - aliasesNames, - }); + if (lambdaCodeChange.configurations !== undefined) { + const updateRequest: AWS.Lambda.UpdateFunctionConfigurationRequest = { + FunctionName: functionName, + }; + if (lambdaCodeChange.configurations.description !== undefined) { + updateRequest.Description = lambdaCodeChange.configurations.description; + } + if (lambdaCodeChange.configurations.environment !== undefined) { + updateRequest.Environment = lambdaCodeChange.configurations.environment; + } + const updateFunctionCodeResponse = await lambda.updateFunctionConfiguration(updateRequest).promise(); + await waitForLambdasPropertiesUpdateToFinish(updateFunctionCodeResponse, lambda, functionName); + } + + // only if the code changed is there any point in publishing a new Version + if (versionsReferencingFunction.length > 0) { + const publishVersionPromise = lambda.publishVersion({ + FunctionName: functionName, + }).promise(); + + if (aliasesNames.length > 0) { + // we need to wait for the Version to finish publishing + const versionUpdate = await publishVersionPromise; + for (const alias of aliasesNames) { + operations.push(lambda.updateAlias({ + FunctionName: functionName, + Name: alias, + FunctionVersion: versionUpdate.Version, + }).promise()); + } + } else { + operations.push(publishVersionPromise); + } + } + } + + // run all of our updates in parallel + await Promise.all(operations); + }, + }); + } + + return ret; } /** - * Returns is a given Alias change is only in the 'FunctionVersion' property, - * and `ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT` is the change is for any other property. + * Determines which changes to this Alias are hotswappable or not */ -function checkAliasHasVersionOnlyChange(change: HotswappableChangeCandidate): ChangeHotswapResult { - for (const updatedPropName in change.propertyUpdates) { - if (updatedPropName !== 'FunctionVersion') { - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; - } +function classifyAliasChanges(change: HotswappableChangeCandidate): ChangeHotswapResult { + const ret: ChangeHotswapResult = []; + const classifiedChanges = classifyChanges(change, ['FunctionVersion']); + classifiedChanges.reportNonHotswappablePropertyChanges(ret); + + const namesOfHotswappableChanges = Object.keys(classifiedChanges.hotswappableProps); + if (namesOfHotswappableChanges.length > 0) { + ret.push({ + hotswappable: true, + resourceType: change.newValue.Type, + propsChanged: [], + service: 'lambda', + resourceNames: [], + apply: async (_sdk: ISDK) => {}, + }); } - return ChangeHotswapImpact.IRRELEVANT; + + return ret; } /** - * Returns `ChangeHotswapImpact.IRRELEVANT` if the change is not for a AWS::Lambda::Function, - * but doesn't prevent short-circuiting - * (like a change to CDKMetadata resource), - * `ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT` if the change is to a AWS::Lambda::Function, - * but not only to its Code property, - * or a LambdaFunctionCode if the change is to a AWS::Lambda::Function, - * and only affects its Code property. + * Evaluates the hotswappable properties of an AWS::Lambda::Function and + * Returns a `LambdaFunctionChange` if the change is hotswappable. + * Returns `undefined` if the change is not hotswappable. */ -async function isLambdaFunctionCodeOnlyChange( - change: HotswappableChangeCandidate, evaluateCfnTemplate: EvaluateCloudFormationTemplate, -): Promise { - const newResourceType = change.newValue.Type; - if (newResourceType !== 'AWS::Lambda::Function') { - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; - } - +async function evaluateLambdaFunctionProps( + hotswappablePropChanges: PropDiffs, runtime: string, evaluateCfnTemplate: EvaluateCloudFormationTemplate, +): Promise { /* * At first glance, we would want to initialize these using the "previous" values (change.oldValue), * in case only one of them changed, like the key, and the Bucket stayed the same. @@ -104,79 +172,51 @@ async function isLambdaFunctionCodeOnlyChange( * even if only one of them was actually changed, * which means we don't need the "old" values at all, and we can safely initialize these with just `''`. */ - const propertyUpdates = change.propertyUpdates; let code: LambdaFunctionCode | undefined = undefined; - let tags: LambdaFunctionTags | undefined = undefined; let description: string | undefined = undefined; let environment: { [key: string]: string } | undefined = undefined; - for (const updatedPropName in propertyUpdates) { - const updatedProp = propertyUpdates[updatedPropName]; + for (const updatedPropName in hotswappablePropChanges) { + const updatedProp = hotswappablePropChanges[updatedPropName]; switch (updatedPropName) { case 'Code': - let foundCodeDifference = false; - let s3Bucket, s3Key, imageUri, functionCodeZip; + let s3Bucket, s3Key, s3ObjectVersion, imageUri, functionCodeZip; for (const newPropName in updatedProp.newValue) { switch (newPropName) { case 'S3Bucket': - foundCodeDifference = true; s3Bucket = await evaluateCfnTemplate.evaluateCfnExpression(updatedProp.newValue[newPropName]); break; case 'S3Key': - foundCodeDifference = true; s3Key = await evaluateCfnTemplate.evaluateCfnExpression(updatedProp.newValue[newPropName]); break; + case 'S3ObjectVersion': + s3ObjectVersion = await evaluateCfnTemplate.evaluateCfnExpression(updatedProp.newValue[newPropName]); + break; case 'ImageUri': - foundCodeDifference = true; imageUri = await evaluateCfnTemplate.evaluateCfnExpression(updatedProp.newValue[newPropName]); break; case 'ZipFile': - foundCodeDifference = true; // We must create a zip package containing a file with the inline code const functionCode = await evaluateCfnTemplate.evaluateCfnExpression(updatedProp.newValue[newPropName]); - const functionRuntime = await evaluateCfnTemplate.evaluateCfnExpression(change.newValue.Properties?.Runtime); + const functionRuntime = await evaluateCfnTemplate.evaluateCfnExpression(runtime); if (!functionRuntime) { - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; + return undefined; } // file extension must be chosen depending on the runtime const codeFileExt = determineCodeFileExtFromRuntime(functionRuntime); functionCodeZip = await zipString(`index.${codeFileExt}`, functionCode); break; - default: - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; } } - if (foundCodeDifference) { - code = { - s3Bucket, - s3Key, - imageUri, - functionCodeZip, - }; - } - break; - case 'Tags': - /* - * Tag updates are a bit odd; they manifest as two lists, are flagged only as - * `isDifferent`, and we have to reconcile them. - */ - const tagUpdates: { [tag: string]: string | TagDeletion } = {}; - if (updatedProp?.isDifferent) { - const tasks = updatedProp.newValue.map(async (tag: CfnDiffTagValue) => { - tagUpdates[tag.Key] = await evaluateCfnTemplate.evaluateCfnExpression(tag.Value); - }); - await Promise.all(tasks); - - updatedProp.oldValue.forEach((tag: CfnDiffTagValue) => { - if (tagUpdates[tag.Key] === undefined) { - tagUpdates[tag.Key] = TagDeletion.DELETE; - } - }); - - tags = { tagUpdates }; - } + code = { + s3Bucket, + s3Key, + s3ObjectVersion, + imageUri, + functionCodeZip, + }; break; case 'Description': description = await evaluateCfnTemplate.evaluateCfnExpression(updatedProp.newValue); @@ -185,34 +225,23 @@ async function isLambdaFunctionCodeOnlyChange( environment = await evaluateCfnTemplate.evaluateCfnExpression(updatedProp.newValue); break; default: - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; + // we will never get here, but just in case we do throw an error + throw new Error ('while apply()ing, found a property that cannot be hotswapped. Please report this at github.com/aws/aws-cdk/issues/new/choose'); } } const configurations = description || environment ? { description, environment } : undefined; - return code || tags || configurations ? { code, tags, configurations } : ChangeHotswapImpact.IRRELEVANT; -} - -interface CfnDiffTagValue { - readonly Key: string; - readonly Value: string; + return code || configurations ? { code, configurations } : undefined; } interface LambdaFunctionCode { readonly s3Bucket?: string; readonly s3Key?: string; + readonly s3ObjectVersion?: string; readonly imageUri?: string; readonly functionCodeZip?: Buffer; } -enum TagDeletion { - DELETE = -1, -} - -interface LambdaFunctionTags { - readonly tagUpdates: { [tag : string] : string | TagDeletion }; -} - interface LambdaFunctionConfigurations { readonly description?: string; readonly environment?: { [key: string]: string }; @@ -220,168 +249,9 @@ interface LambdaFunctionConfigurations { interface LambdaFunctionChange { readonly code?: LambdaFunctionCode; - readonly tags?: LambdaFunctionTags; readonly configurations?: LambdaFunctionConfigurations; } -interface LambdaFunctionResource { - readonly physicalName: string; - readonly functionArn: string; - readonly resource: LambdaFunctionChange; - readonly publishVersion: boolean; - readonly aliasesNames: string[]; -} - -class LambdaFunctionHotswapOperation implements HotswapOperation { - public readonly service = 'lambda-function'; - public readonly resourceNames: string[]; - - constructor(private readonly lambdaFunctionResource: LambdaFunctionResource) { - this.resourceNames = [ - `Lambda Function '${lambdaFunctionResource.physicalName}'`, - // add Version here if we're publishing a new one - ...(lambdaFunctionResource.publishVersion ? [`Lambda Version for Function '${lambdaFunctionResource.physicalName}'`] : []), - // add any Aliases that we are hotswapping here - ...lambdaFunctionResource.aliasesNames.map(alias => `Lambda Alias '${alias}' for Function '${lambdaFunctionResource.physicalName}'`), - ]; - } - - public async apply(sdk: ISDK): Promise { - const lambda = sdk.lambda(); - const resource = this.lambdaFunctionResource.resource; - const operations: Promise[] = []; - - if (resource.code !== undefined || resource.configurations !== undefined) { - if (resource.code !== undefined) { - const updateFunctionCodeResponse = await lambda.updateFunctionCode({ - FunctionName: this.lambdaFunctionResource.physicalName, - S3Bucket: resource.code.s3Bucket, - S3Key: resource.code.s3Key, - ImageUri: resource.code.imageUri, - ZipFile: resource.code.functionCodeZip, - }).promise(); - - await this.waitForLambdasPropertiesUpdateToFinish(updateFunctionCodeResponse, lambda); - } - - if (resource.configurations !== undefined) { - const updateRequest: AWS.Lambda.UpdateFunctionConfigurationRequest = { - FunctionName: this.lambdaFunctionResource.physicalName, - }; - if (resource.configurations.description !== undefined) { - updateRequest.Description = resource.configurations.description; - } - if (resource.configurations.environment !== undefined) { - updateRequest.Environment = resource.configurations.environment; - } - const updateFunctionCodeResponse = await lambda.updateFunctionConfiguration(updateRequest).promise(); - await this.waitForLambdasPropertiesUpdateToFinish(updateFunctionCodeResponse, lambda); - } - - // only if the code changed is there any point in publishing a new Version - if (this.lambdaFunctionResource.publishVersion) { - const publishVersionPromise = lambda.publishVersion({ - FunctionName: this.lambdaFunctionResource.physicalName, - }).promise(); - - if (this.lambdaFunctionResource.aliasesNames.length > 0) { - // we need to wait for the Version to finish publishing - const versionUpdate = await publishVersionPromise; - - for (const alias of this.lambdaFunctionResource.aliasesNames) { - operations.push(lambda.updateAlias({ - FunctionName: this.lambdaFunctionResource.physicalName, - Name: alias, - FunctionVersion: versionUpdate.Version, - }).promise()); - } - } else { - operations.push(publishVersionPromise); - } - } - } - - if (resource.tags !== undefined) { - const tagsToDelete: string[] = Object.entries(resource.tags.tagUpdates) - .filter(([_key, val]) => val === TagDeletion.DELETE) - .map(([key, _val]) => key); - - const tagsToSet: { [tag: string]: string } = {}; - Object.entries(resource.tags!.tagUpdates) - .filter(([_key, val]) => val !== TagDeletion.DELETE) - .forEach(([tagName, tagValue]) => { - tagsToSet[tagName] = tagValue as string; - }); - - if (tagsToDelete.length > 0) { - operations.push(lambda.untagResource({ - Resource: this.lambdaFunctionResource.functionArn, - TagKeys: tagsToDelete, - }).promise()); - } - - if (Object.keys(tagsToSet).length > 0) { - operations.push(lambda.tagResource({ - Resource: this.lambdaFunctionResource.functionArn, - Tags: tagsToSet, - }).promise()); - } - } - - // run all of our updates in parallel - return Promise.all(operations); - } - - /** - * After a Lambda Function is updated, it cannot be updated again until the - * `State=Active` and the `LastUpdateStatus=Successful`. - * - * Depending on the configuration of the Lambda Function this could happen relatively quickly - * or very slowly. For example, Zip based functions _not_ in a VPC can take ~1 second whereas VPC - * or Container functions can take ~25 seconds (and 'idle' VPC functions can take minutes). - */ - private async waitForLambdasPropertiesUpdateToFinish( - currentFunctionConfiguration: AWS.Lambda.FunctionConfiguration, lambda: AWS.Lambda, - ): Promise { - const functionIsInVpcOrUsesDockerForCode = currentFunctionConfiguration.VpcConfig?.VpcId || - currentFunctionConfiguration.PackageType === 'Image'; - - // if the function is deployed in a VPC or if it is a container image function - // then the update will take much longer and we can wait longer between checks - // otherwise, the update will be quick, so a 1-second delay is fine - const delaySeconds = functionIsInVpcOrUsesDockerForCode ? 5 : 1; - - // configure a custom waiter to wait for the function update to complete - (lambda as any).api.waiters.updateFunctionPropertiesToFinish = { - name: 'UpdateFunctionPropertiesToFinish', - operation: 'getFunction', - // equates to 1 minute for zip function not in a VPC and - // 5 minutes for container functions or function in a VPC - maxAttempts: 60, - delay: delaySeconds, - acceptors: [ - { - matcher: 'path', - argument: "Configuration.LastUpdateStatus == 'Successful' && Configuration.State == 'Active'", - expected: true, - state: 'success', - }, - { - matcher: 'path', - argument: 'Configuration.LastUpdateStatus', - expected: 'Failed', - state: 'failure', - }, - ], - }; - - const updateFunctionPropertiesWaiter = new (AWS as any).ResourceWaiter(lambda, 'updateFunctionPropertiesToFinish'); - await updateFunctionPropertiesWaiter.wait({ - FunctionName: this.lambdaFunctionResource.physicalName, - }).promise(); - } -} - /** * Compress a string as a file, returning a promise for the zip buffer * https://github.com/archiverjs/node-archiver/issues/342 @@ -418,6 +288,55 @@ function zipString(fileName: string, rawString: string): Promise { }); } +/** + * After a Lambda Function is updated, it cannot be updated again until the + * `State=Active` and the `LastUpdateStatus=Successful`. + * + * Depending on the configuration of the Lambda Function this could happen relatively quickly + * or very slowly. For example, Zip based functions _not_ in a VPC can take ~1 second whereas VPC + * or Container functions can take ~25 seconds (and 'idle' VPC functions can take minutes). + */ +async function waitForLambdasPropertiesUpdateToFinish( + currentFunctionConfiguration: AWS.Lambda.FunctionConfiguration, lambda: AWS.Lambda, functionName: string, +): Promise { + const functionIsInVpcOrUsesDockerForCode = currentFunctionConfiguration.VpcConfig?.VpcId || + currentFunctionConfiguration.PackageType === 'Image'; + + // if the function is deployed in a VPC or if it is a container image function + // then the update will take much longer and we can wait longer between checks + // otherwise, the update will be quick, so a 1-second delay is fine + const delaySeconds = functionIsInVpcOrUsesDockerForCode ? 5 : 1; + + // configure a custom waiter to wait for the function update to complete + (lambda as any).api.waiters.updateFunctionPropertiesToFinish = { + name: 'UpdateFunctionPropertiesToFinish', + operation: 'getFunction', + // equates to 1 minute for zip function not in a VPC and + // 5 minutes for container functions or function in a VPC + maxAttempts: 60, + delay: delaySeconds, + acceptors: [ + { + matcher: 'path', + argument: "Configuration.LastUpdateStatus == 'Successful' && Configuration.State == 'Active'", + expected: true, + state: 'success', + }, + { + matcher: 'path', + argument: 'Configuration.LastUpdateStatus', + expected: 'Failed', + state: 'failure', + }, + ], + }; + + const updateFunctionPropertiesWaiter = new (AWS as any).ResourceWaiter(lambda, 'updateFunctionPropertiesToFinish'); + await updateFunctionPropertiesWaiter.wait({ + FunctionName: functionName, + }).promise(); +} + /** * Get file extension from Lambda runtime string. * We use this extension to create a deployment package from Lambda inline code. @@ -433,3 +352,42 @@ function determineCodeFileExtFromRuntime(runtime: string): string { // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html#aws-properties-lambda-function-code-properties throw new CfnEvaluationException(`runtime ${runtime} is unsupported, only node.js and python runtimes are currently supported.`); } + +/** + * Finds all Versions that reference an AWS::Lambda::Function with logical ID `logicalId` + * and Aliases that reference those Versions. + */ +async function versionsAndAliases(logicalId: string, evaluateCfnTemplate: EvaluateCloudFormationTemplate) { + // find all Lambda Versions that reference this Function + const versionsReferencingFunction = evaluateCfnTemplate.findReferencesTo(logicalId) + .filter(r => r.Type === 'AWS::Lambda::Version'); + // find all Lambda Aliases that reference the above Versions + const aliasesReferencingVersions = flatMap(versionsReferencingFunction, v => + evaluateCfnTemplate.findReferencesTo(v.LogicalId)); + const aliasesNames = await Promise.all(aliasesReferencingVersions.map(a => + evaluateCfnTemplate.evaluateCfnExpression(a.Properties?.Name))); + + return { versionsReferencingFunction, aliasesNames }; +} + +/** + * Renders the string used in displaying Alias resource names that reference the specified Lambda Function + */ +async function renderAliases( + logicalId: string, + evaluateCfnTemplate: EvaluateCloudFormationTemplate, + callbackfn: (value: any, index: number, array: any[]) => Promise, +): Promise { + const aliasesNames = (await versionsAndAliases(logicalId, evaluateCfnTemplate)).aliasesNames; + + return Promise.all(aliasesNames.map(callbackfn)); +} + +/** + * Renders the string used in displaying Version resource names that reference the specified Lambda Function + */ +async function renderVersions(logicalId: string, evaluateCfnTemplate: EvaluateCloudFormationTemplate, versionString: string[]): Promise { + const versions = (await versionsAndAliases(logicalId, evaluateCfnTemplate)).versionsReferencingFunction; + + return versions.length > 0 ? versionString : []; +} diff --git a/packages/aws-cdk/lib/api/hotswap/s3-bucket-deployments.ts b/packages/aws-cdk/lib/api/hotswap/s3-bucket-deployments.ts index 7eebe6d0a9437..06087d64c350d 100644 --- a/packages/aws-cdk/lib/api/hotswap/s3-bucket-deployments.ts +++ b/packages/aws-cdk/lib/api/hotswap/s3-bucket-deployments.ts @@ -1,9 +1,9 @@ import { ISDK } from '../aws-auth'; import { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; -import { ChangeHotswapImpact, ChangeHotswapResult, HotswapOperation, HotswappableChangeCandidate } from './common'; +import { ChangeHotswapResult, HotswappableChangeCandidate, reportNonHotswappableResource } from './common'; /** - * This means that the value is required to exist by CloudFormation's API (or our S3 Bucket Deployment Lambda) + * This means that the value is required to exist by CloudFormation's Custom Resource API (or our S3 Bucket Deployment Lambda's API) * but the actual value specified is irrelevant */ export const REQUIRED_BY_CFN = 'required-to-be-present-by-cfn'; @@ -13,51 +13,51 @@ export async function isHotswappableS3BucketDeploymentChange( ): Promise { // In old-style synthesis, the policy used by the lambda to copy assets Ref's the assets directly, // meaning that the changes made to the Policy are artifacts that can be safely ignored + const ret: ChangeHotswapResult = []; if (change.newValue.Type === 'AWS::IAM::Policy') { return changeIsForS3DeployCustomResourcePolicy(logicalId, change, evaluateCfnTemplate); } if (change.newValue.Type !== 'Custom::CDKBucketDeployment') { - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; - } - - // note that this gives the ARN of the lambda, not the name. This is fine though, the invoke() sdk call will take either - const functionName = await evaluateCfnTemplate.evaluateCfnExpression(change.newValue.Properties?.ServiceToken); - if (!functionName) { - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; + return []; } + // no classification to be done here; all the properties of this custom resource thing are hotswappable const customResourceProperties = await evaluateCfnTemplate.evaluateCfnExpression({ ...change.newValue.Properties, ServiceToken: undefined, }); - return new S3BucketDeploymentHotswapOperation(functionName, customResourceProperties); -} - -class S3BucketDeploymentHotswapOperation implements HotswapOperation { - public readonly service = 'custom-s3-deployment'; - public readonly resourceNames: string[]; + ret.push({ + hotswappable: true, + resourceType: change.newValue.Type, + propsChanged: ['*'], + service: 'custom-s3-deployment', + resourceNames: [`Contents of S3 Bucket '${customResourceProperties.DestinationBucketName}'`], + apply: async (sdk: ISDK) => { + // note that this gives the ARN of the lambda, not the name. This is fine though, the invoke() sdk call will take either + const functionName = await evaluateCfnTemplate.evaluateCfnExpression(change.newValue.Properties?.ServiceToken); + if (!functionName) { + return; + } - constructor(private readonly functionName: string, private readonly customResourceProperties: any) { - this.resourceNames = [`Contents of S3 Bucket '${this.customResourceProperties.DestinationBucketName}'`]; - } + await sdk.lambda().invoke({ + FunctionName: functionName, + // Lambda refuses to take a direct JSON object and requires it to be stringify()'d + Payload: JSON.stringify({ + RequestType: 'Update', + ResponseURL: REQUIRED_BY_CFN, + PhysicalResourceId: REQUIRED_BY_CFN, + StackId: REQUIRED_BY_CFN, + RequestId: REQUIRED_BY_CFN, + LogicalResourceId: REQUIRED_BY_CFN, + ResourceProperties: stringifyObject(customResourceProperties), // JSON.stringify() doesn't turn the actual objects to strings, but the lambda expects strings + }), + }).promise(); + }, + }); - public async apply(sdk: ISDK): Promise { - return sdk.lambda().invoke({ - FunctionName: this.functionName, - // Lambda refuses to take a direct JSON object and requires it to be stringify()'d - Payload: JSON.stringify({ - RequestType: 'Update', - ResponseURL: REQUIRED_BY_CFN, - PhysicalResourceId: REQUIRED_BY_CFN, - StackId: REQUIRED_BY_CFN, - RequestId: REQUIRED_BY_CFN, - LogicalResourceId: REQUIRED_BY_CFN, - ResourceProperties: stringifyObject(this.customResourceProperties), // JSON.stringify() doesn't turn the actual objects to strings, but the lambda expects strings - }), - }).promise(); - } + return ret; } async function changeIsForS3DeployCustomResourcePolicy( @@ -65,13 +65,20 @@ async function changeIsForS3DeployCustomResourcePolicy( ): Promise { const roles = change.newValue.Properties?.Roles; if (!roles) { - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; + return reportNonHotswappableResource( + change, + 'This IAM Policy does not have have any Roles', + ); } for (const role of roles) { - const roleLogicalId = await evaluateCfnTemplate.findLogicalIdForPhysicalName(await evaluateCfnTemplate.evaluateCfnExpression(role)); + const roleArn = await evaluateCfnTemplate.evaluateCfnExpression(role); + const roleLogicalId = await evaluateCfnTemplate.findLogicalIdForPhysicalName(roleArn); if (!roleLogicalId) { - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; + return reportNonHotswappableResource( + change, + `could not find logicalId for role with name '${roleArn}'`, + ); } const roleRefs = evaluateCfnTemplate.findReferencesTo(roleLogicalId); @@ -82,20 +89,31 @@ async function changeIsForS3DeployCustomResourcePolicy( // If S3Deployment -> Lambda -> Role and IAM::Policy -> Role, then this IAM::Policy change is an // artifact of old-style synthesis if (lambdaRef.Type !== 'Custom::CDKBucketDeployment') { - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; + return reportNonHotswappableResource( + change, + `found an AWS::IAM::Policy that has Role '${roleLogicalId}' that is referred to by AWS::Lambda::Function '${roleRef.LogicalId}' that is referred to by ${lambdaRef.Type} '${lambdaRef.LogicalId}', which does not have type 'Custom::CDKBucketDeployment'`, + ); } } } else if (roleRef.Type === 'AWS::IAM::Policy') { if (roleRef.LogicalId !== iamPolicyLogicalId) { - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; + return reportNonHotswappableResource( + change, + `found an AWS::IAM::Policy that has Role '${roleLogicalId}' that is referred to by AWS::IAM::Policy '${roleRef.LogicalId}' that is not the policy of the s3 bucket deployment`, + ); } } else { - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; + return reportNonHotswappableResource( + change, + `found a resource which refers to the role '${roleLogicalId}' that is not of type AWS::Lambda::Function or AWS::IAM::Policy, so the bucket deployment cannot be hotswapped`, + ); } } } - return ChangeHotswapImpact.IRRELEVANT; + // this doesn't block the hotswap, but it also isn't a hotswappable change by itself. Return + // an empty change to signify this. + return []; } function stringifyObject(obj: any): any { diff --git a/packages/aws-cdk/lib/api/hotswap/stepfunctions-state-machines.ts b/packages/aws-cdk/lib/api/hotswap/stepfunctions-state-machines.ts index 53ca21edc9dbe..7e9a988741a49 100644 --- a/packages/aws-cdk/lib/api/hotswap/stepfunctions-state-machines.ts +++ b/packages/aws-cdk/lib/api/hotswap/stepfunctions-state-machines.ts @@ -1,74 +1,44 @@ import { ISDK } from '../aws-auth'; import { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; -import { ChangeHotswapImpact, ChangeHotswapResult, HotswapOperation, HotswappableChangeCandidate } from './common'; +import { ChangeHotswapResult, classifyChanges, HotswappableChangeCandidate } from './common'; export async function isHotswappableStateMachineChange( logicalId: string, change: HotswappableChangeCandidate, evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): Promise { - const stateMachineDefinitionChange = await isStateMachineDefinitionOnlyChange(change, evaluateCfnTemplate); - if (stateMachineDefinitionChange === ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT || - stateMachineDefinitionChange === ChangeHotswapImpact.IRRELEVANT) { - return stateMachineDefinitionChange; + if (change.newValue.Type !== 'AWS::StepFunctions::StateMachine') { + return []; } - - const stateMachineNameInCfnTemplate = change.newValue?.Properties?.StateMachineName; - const stateMachineArn = stateMachineNameInCfnTemplate - ? await evaluateCfnTemplate.evaluateCfnExpression({ - 'Fn::Sub': 'arn:${AWS::Partition}:states:${AWS::Region}:${AWS::AccountId}:stateMachine:' + stateMachineNameInCfnTemplate, - }) - : await evaluateCfnTemplate.findPhysicalNameFor(logicalId); - - if (!stateMachineArn) { - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; - } - - return new StateMachineHotswapOperation({ - definition: stateMachineDefinitionChange, - stateMachineArn: stateMachineArn, - }); -} - -async function isStateMachineDefinitionOnlyChange( - change: HotswappableChangeCandidate, evaluateCfnTemplate: EvaluateCloudFormationTemplate, -): Promise { - const newResourceType = change.newValue.Type; - if (newResourceType !== 'AWS::StepFunctions::StateMachine') { - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; - } - - const propertyUpdates = change.propertyUpdates; - if (Object.keys(propertyUpdates).length === 0) { - return ChangeHotswapImpact.IRRELEVANT; - } - - for (const updatedPropName in propertyUpdates) { - // ensure that only changes to the definition string result in a hotswap - if (updatedPropName !== 'DefinitionString') { - return ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT; - } + const ret: ChangeHotswapResult = []; + const classifiedChanges = classifyChanges(change, ['DefinitionString']); + classifiedChanges.reportNonHotswappablePropertyChanges(ret); + + const namesOfHotswappableChanges = Object.keys(classifiedChanges.hotswappableProps); + if (namesOfHotswappableChanges.length > 0) { + const stateMachineNameInCfnTemplate = change.newValue?.Properties?.StateMachineName; + const stateMachineArn = stateMachineNameInCfnTemplate + ? await evaluateCfnTemplate.evaluateCfnExpression({ + 'Fn::Sub': 'arn:${AWS::Partition}:states:${AWS::Region}:${AWS::AccountId}:stateMachine:' + stateMachineNameInCfnTemplate, + }) + : await evaluateCfnTemplate.findPhysicalNameFor(logicalId); + ret.push({ + hotswappable: true, + resourceType: change.newValue.Type, + propsChanged: namesOfHotswappableChanges, + service: 'stepfunctions-service', + resourceNames: [`${change.newValue.Type} '${stateMachineArn?.split(':')[6]}'`], + apply: async (sdk: ISDK) => { + if (!stateMachineArn) { + return; + } + + // not passing the optional properties leaves them unchanged + await sdk.stepFunctions().updateStateMachine({ + stateMachineArn, + definition: await evaluateCfnTemplate.evaluateCfnExpression(change.propertyUpdates.DefinitionString.newValue), + }).promise(); + }, + }); } - return evaluateCfnTemplate.evaluateCfnExpression(propertyUpdates.DefinitionString.newValue); -} - -interface StateMachineResource { - readonly stateMachineArn: string; - readonly definition: string; -} - -class StateMachineHotswapOperation implements HotswapOperation { - public readonly service = 'stepfunctions-state-machine'; - public readonly resourceNames: string[]; - - constructor(private readonly stepFunctionResource: StateMachineResource) { - this.resourceNames = [`StateMachine '${this.stepFunctionResource.stateMachineArn.split(':')[6]}'`]; - } - - public async apply(sdk: ISDK): Promise { - // not passing the optional properties leaves them unchanged - return sdk.stepFunctions().updateStateMachine({ - stateMachineArn: this.stepFunctionResource.stateMachineArn, - definition: this.stepFunctionResource.definition, - }).promise(); - } + return ret; } diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 66c54c4744763..daac1af2210d9 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -12,6 +12,7 @@ import { Bootstrapper, BootstrapEnvironmentOptions } from './api/bootstrap'; import { CloudFormationDeployments } from './api/cloudformation-deployments'; import { CloudAssembly, DefaultSelection, ExtendedStackSelection, StackCollection, StackSelector } from './api/cxapp/cloud-assembly'; import { CloudExecutable } from './api/cxapp/cloud-executable'; +import { HotswapMode } from './api/hotswap/common'; import { findCloudWatchLogGroups } from './api/logs/find-cloudwatch-logs'; import { CloudWatchLogEventMonitor } from './api/logs/logs-monitor'; import { StackActivityProgress } from './api/util/cloudformation/stack-activity-monitor'; @@ -179,15 +180,14 @@ export class CdkToolkit { } } - if (options.hotswap) { - warning('⚠️ The --hotswap flag deliberately introduces CloudFormation drift to speed up deployments'); - warning('⚠️ It should only be used for development - never use it for your production Stacks!'); + if (options.hotswap !== HotswapMode.FULL_DEPLOYMENT) { + warning('⚠️ The --hotswap and --hotswap-fallback flags deliberately introduce CloudFormation drift to speed up deployments'); + warning('⚠️ They should only be used for development - never use them for your production Stacks!\n'); } const stacks = stackCollection.stackArtifacts; const assetBuildTime = options.assetBuildTime ?? AssetBuildTime.ALL_BEFORE_DEPLOY; - const stackOutputs: { [key: string]: any } = { }; const outputsFile = options.outputsFile; @@ -766,8 +766,6 @@ export class CdkToolkit { } private async invokeDeployFromWatch(options: WatchOptions, cloudWatchLogMonitor?: CloudWatchLogEventMonitor): Promise { - // 'watch' has different defaults than regular 'deploy' - const hotswap = options.hotswap === undefined ? true : options.hotswap; const deployOptions: DeployOptions = { ...options, requireApproval: RequireApproval.Never, @@ -777,8 +775,8 @@ export class CdkToolkit { watch: false, cloudWatchLogMonitor, cacheCloudAssembly: false, - hotswap: hotswap, - extraUserAgent: `cdk-watch/hotswap-${hotswap ? 'on' : 'off'}`, + hotswap: options.hotswap, + extraUserAgent: `cdk-watch/hotswap-${options.hotswap !== HotswapMode.FALL_BACK ? 'on' : 'off'}`, concurrency: options.concurrency, }; @@ -952,9 +950,9 @@ interface WatchOptions extends Omit { * A 'hotswap' deployment will attempt to short-circuit CloudFormation * and update the affected resources like Lambda functions directly. * - * @default - false for regular deployments, true for 'watch' deployments + * @default - `HotswapMode.FALL_BACK` for regular deployments, `HotswapMode.HOTSWAP_ONLY` for 'watch' deployments */ - readonly hotswap?: boolean; + readonly hotswap: HotswapMode; /** * The extra string to append to the User-Agent header when performing AWS SDK calls. diff --git a/packages/aws-cdk/lib/cli.ts b/packages/aws-cdk/lib/cli.ts index a7a89913e70aa..eda7428e3d7c6 100644 --- a/packages/aws-cdk/lib/cli.ts +++ b/packages/aws-cdk/lib/cli.ts @@ -24,6 +24,7 @@ import { displayNotices, refreshNotices } from '../lib/notices'; import { Command, Configuration, Settings } from '../lib/settings'; import * as version from '../lib/version'; import { DeploymentMethod } from './api'; +import { HotswapMode } from './api/hotswap/common'; import { ILock } from './api/util/rwlock'; import { checkForPlatformWarnings } from './platform-warnings'; import { enableTracing } from './util/tracing'; @@ -141,6 +142,13 @@ async function parseCommandLineArguments(args: string[]) { // Hack to get '-R' as an alias for '--no-rollback', suggested by: https://github.com/yargs/yargs/issues/1729 .option('R', { type: 'boolean', hidden: true }).middleware(yargsNegativeAlias('R', 'rollback'), true) .option('hotswap', { + type: 'boolean', + desc: "Attempts to perform a 'hotswap' deployment, " + + 'but does not fall back to a full deployment if that is not possible. ' + + 'Instead, changes to any non-hotswappable properties are ignored.' + + 'Do not use this in production environments', + }) + .option('hotswap-fallback', { type: 'boolean', desc: "Attempts to perform a 'hotswap' deployment, " + 'which skips CloudFormation and updates the resources directly, ' + @@ -222,10 +230,16 @@ async function parseCommandLineArguments(args: string[]) { .option('hotswap', { type: 'boolean', desc: "Attempts to perform a 'hotswap' deployment, " + - 'which skips CloudFormation and updates the resources directly, ' + - 'and falls back to a full deployment if that is not possible. ' + + 'but does not fall back to a full deployment if that is not possible. ' + + 'Instead, changes to any non-hotswappable properties are ignored.' + "'true' by default, use --no-hotswap to turn off", }) + .option('hotswap-fallback', { + type: 'boolean', + desc: "Attempts to perform a 'hotswap' deployment, " + + 'which skips CloudFormation and updates the resources directly, ' + + 'and falls back to a full deployment if that is not possible.', + }) .options('logs', { type: 'boolean', default: true, @@ -548,7 +562,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise { diff --git a/packages/aws-cdk/lib/init-templates/app/typescript/package.json b/packages/aws-cdk/lib/init-templates/app/typescript/package.json index 56a24cc96ff2b..b49b1b54c3c6c 100644 --- a/packages/aws-cdk/lib/init-templates/app/typescript/package.json +++ b/packages/aws-cdk/lib/init-templates/app/typescript/package.json @@ -17,7 +17,7 @@ "ts-jest": "^29.0.5", "aws-cdk": "%cdk-version%", "ts-node": "^10.9.1", - "typescript": "~4.9.4" + "typescript": "~4.9.5" }, "dependencies": { "aws-cdk-lib": "%cdk-version%", diff --git a/packages/aws-cdk/lib/init-templates/lib/typescript/package.json b/packages/aws-cdk/lib/init-templates/lib/typescript/package.json index 97493125c23ff..3bee8e01f6edb 100644 --- a/packages/aws-cdk/lib/init-templates/lib/typescript/package.json +++ b/packages/aws-cdk/lib/init-templates/lib/typescript/package.json @@ -15,7 +15,7 @@ "constructs": "%constructs-version%", "jest": "^29.4.1", "ts-jest": "^29.0.5", - "typescript": "~4.9.4" + "typescript": "~4.9.5" }, "peerDependencies": { "aws-cdk-lib": "%cdk-version%", diff --git a/packages/aws-cdk/lib/init-templates/sample-app/typescript/package.json b/packages/aws-cdk/lib/init-templates/sample-app/typescript/package.json index bdd6f477b5f3b..ea1e9b9d43d7f 100644 --- a/packages/aws-cdk/lib/init-templates/sample-app/typescript/package.json +++ b/packages/aws-cdk/lib/init-templates/sample-app/typescript/package.json @@ -17,7 +17,7 @@ "ts-jest": "^29.0.5", "aws-cdk": "%cdk-version%", "ts-node": "^10.9.1", - "typescript": "~4.9.4" + "typescript": "~4.9.5" }, "dependencies": { "aws-cdk-lib": "%cdk-version%", diff --git a/packages/aws-cdk/test/api/cloudformation-deployments.test.ts b/packages/aws-cdk/test/api/cloudformation-deployments.test.ts index c2ce217ca1941..07d2cd68468ea 100644 --- a/packages/aws-cdk/test/api/cloudformation-deployments.test.ts +++ b/packages/aws-cdk/test/api/cloudformation-deployments.test.ts @@ -15,6 +15,7 @@ import { buildAssets, publishAssets } from '../../lib/util/asset-publishing'; import { testStack } from '../util'; import { mockBootstrapStack, MockSdkProvider } from '../util/mock-sdk'; import { FakeCloudformationStack } from './fake-cloudformation-stack'; +import { HotswapMode } from '../../lib/api/hotswap/common'; let sdkProvider: MockSdkProvider; let deployments: CloudFormationDeployments; @@ -99,12 +100,12 @@ test('passes through hotswap=true to deployStack()', async () => { stack: testStack({ stackName: 'boop', }), - hotswap: true, + hotswap: HotswapMode.FALL_BACK, }); // THEN expect(deployStack).toHaveBeenCalledWith(expect.objectContaining({ - hotswap: true, + hotswap: HotswapMode.FALL_BACK, })); }); diff --git a/packages/aws-cdk/test/api/deploy-stack.test.ts b/packages/aws-cdk/test/api/deploy-stack.test.ts index 2ff4d691983be..b8f9ad8191e3f 100644 --- a/packages/aws-cdk/test/api/deploy-stack.test.ts +++ b/packages/aws-cdk/test/api/deploy-stack.test.ts @@ -1,5 +1,6 @@ import { deployStack, DeployStackOptions, ToolkitInfo } from '../../lib/api'; import { tryHotswapDeployment } from '../../lib/api/hotswap-deployments'; +import { HotswapMode } from '../../lib/api/hotswap/common'; import { setCI } from '../../lib/logging'; import { DEFAULT_FAKE_TEMPLATE, testStack } from '../util'; import { MockedObject, mockResolvedEnvironment, MockSdk, MockSdkProvider, SyncHandlerSubsetOf } from '../util/mock-sdk'; @@ -83,11 +84,11 @@ function standardDeployStackArguments(): DeployStackOptions { }; } -test("calls tryHotswapDeployment() if 'hotswap' is true", async () => { +test("calls tryHotswapDeployment() if 'hotswap' is `HotswapMode.CLASSIC`", async () => { // WHEN await deployStack({ ...standardDeployStackArguments(), - hotswap: true, + hotswap: HotswapMode.FALL_BACK, extraUserAgent: 'extra-user-agent', }); @@ -99,6 +100,54 @@ test("calls tryHotswapDeployment() if 'hotswap' is true", async () => { expect(sdk.appendCustomUserAgent).toHaveBeenCalledWith('cdk-hotswap/fallback'); }); +test("calls tryHotswapDeployment() if 'hotswap' is `HotswapMode.HOTSWAP_ONLY`", async () => { + cfnMocks.describeStacks = jest.fn() + // we need the first call to return something in the Stacks prop, + // otherwise the access to `stackId` will fail + .mockImplementation(() => ({ + Stacks: [ + { + StackStatus: 'CREATE_COMPLETE', + StackStatusReason: 'It is magic', + EnableTerminationProtection: false, + }, + ], + })); + sdk.stubCloudFormation(cfnMocks as any); + // WHEN + const deployStackResult = await deployStack({ + ...standardDeployStackArguments(), + hotswap: HotswapMode.HOTSWAP_ONLY, + extraUserAgent: 'extra-user-agent', + force: true, // otherwise, deployment would be skipped + }); + + // THEN + expect(deployStackResult.noOp).toEqual(true); + expect(tryHotswapDeployment).toHaveBeenCalled(); + // check that the extra User-Agent is honored + expect(sdk.appendCustomUserAgent).toHaveBeenCalledWith('extra-user-agent'); + // check that the fallback has not been called if hotswapping failed + expect(sdk.appendCustomUserAgent).not.toHaveBeenCalledWith('cdk-hotswap/fallback'); +}); + +test('correctly passes CFN parameters when hotswapping', async () => { + // WHEN + await deployStack({ + ...standardDeployStackArguments(), + hotswap: HotswapMode.FALL_BACK, + parameters: { + A: 'A-value', + B: 'B=value', + C: undefined, + D: '', + }, + }); + + // THEN + expect(tryHotswapDeployment).toHaveBeenCalledWith(expect.anything(), { A: 'A-value', B: 'B=value' }, expect.anything(), expect.anything(), HotswapMode.FALL_BACK); +}); + test('call CreateStack when method=direct and the stack doesnt exist yet', async () => { // WHEN await deployStack({ @@ -128,7 +177,7 @@ test("does not call tryHotswapDeployment() if 'hotswap' is false", async () => { // WHEN await deployStack({ ...standardDeployStackArguments(), - hotswap: false, + hotswap: undefined, }); // THEN @@ -139,7 +188,7 @@ test("rollback still defaults to enabled even if 'hotswap' is enabled", async () // WHEN await deployStack({ ...standardDeployStackArguments(), - hotswap: true, + hotswap: HotswapMode.FALL_BACK, rollback: undefined, }); @@ -149,11 +198,11 @@ test("rollback still defaults to enabled even if 'hotswap' is enabled", async () })); }); -test("rollback defaults to enabled if 'hotswap' is false", async () => { +test("rollback defaults to enabled if 'hotswap' is undefined", async () => { // WHEN await deployStack({ ...standardDeployStackArguments(), - hotswap: false, + hotswap: undefined, rollback: undefined, }); diff --git a/packages/aws-cdk/test/api/hotswap/appsync-mapping-templates-hotswap-deployments.test.ts b/packages/aws-cdk/test/api/hotswap/appsync-mapping-templates-hotswap-deployments.test.ts index 93b83de5cd96e..c31600fe38519 100644 --- a/packages/aws-cdk/test/api/hotswap/appsync-mapping-templates-hotswap-deployments.test.ts +++ b/packages/aws-cdk/test/api/hotswap/appsync-mapping-templates-hotswap-deployments.test.ts @@ -1,4 +1,5 @@ import { AppSync } from 'aws-sdk'; +import { HotswapMode } from '../../../lib/api/hotswap/common'; import * as setup from './hotswap-test-setup'; let hotswapMockSdkProvider: setup.HotswapMockSdkProvider; @@ -12,55 +13,44 @@ beforeEach(() => { hotswapMockSdkProvider.stubAppSync({ updateResolver: mockUpdateResolver, updateFunction: mockUpdateFunction }); }); -test('returns undefined when a new Resolver is added to the Stack', async () => { - // GIVEN - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { - Resources: { - AppSyncResolver: { - Type: 'AWS::AppSync::Resolver', +describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hotswapMode) => { + test(`A new Resolver being added to the Stack returns undefined in CLASSIC mode and + returns a noOp in HOTSWAP_ONLY mode`, + async () => { + // GIVEN + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + AppSyncResolver: { + Type: 'AWS::AppSync::Resolver', + }, }, }, - }, - }); + }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN - expect(deployStackResult).toBeUndefined(); -}); + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateFunction).not.toHaveBeenCalled(); + expect(mockUpdateResolver).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); -test('calls the updateResolver() API when it receives only a mapping template difference in a Unit Resolver', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - AppSyncResolver: { - Type: 'AWS::AppSync::Resolver', - Properties: { - ApiId: 'apiId', - FieldName: 'myField', - TypeName: 'Query', - DataSourceName: 'my-datasource', - Kind: 'UNIT', - RequestMappingTemplate: '## original request template', - ResponseMappingTemplate: '## original response template', - }, - Metadata: { - 'aws:asset:path': 'old-path', - }, - }, - }, + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(deployStackResult?.noOp).toEqual(true); + expect(mockUpdateFunction).not.toHaveBeenCalled(); + expect(mockUpdateResolver).not.toHaveBeenCalled(); + } }); - setup.pushStackResourceSummaries( - setup.stackSummaryOf( - 'AppSyncResolver', - 'AWS::AppSync::Resolver', - 'arn:aws:appsync:us-east-1:111111111111:apis/apiId/types/Query/resolvers/myField', - ), - ); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('calls the updateResolver() API when it receives only a mapping template difference in a Unit Resolver', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { AppSyncResolver: { Type: 'AWS::AppSync::Resolver', @@ -70,57 +60,63 @@ test('calls the updateResolver() API when it receives only a mapping template di TypeName: 'Query', DataSourceName: 'my-datasource', Kind: 'UNIT', - RequestMappingTemplate: '## new request template', + RequestMappingTemplate: '## original request template', ResponseMappingTemplate: '## original response template', }, Metadata: { - 'aws:asset:path': 'new-path', + 'aws:asset:path': 'old-path', }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateResolver).toHaveBeenCalledWith({ - apiId: 'apiId', - dataSourceName: 'my-datasource', - typeName: 'Query', - fieldName: 'myField', - kind: 'UNIT', - requestMappingTemplate: '## new request template', - responseMappingTemplate: '## original response template', - }); -}); - -test('does not call the updateResolver() API when it receives only a mapping template difference in a Pipeline Resolver', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - AppSyncResolver: { - Type: 'AWS::AppSync::Resolver', - Properties: { - ApiId: 'apiId', - FieldName: 'myField', - TypeName: 'Query', - DataSourceName: 'my-datasource', - Kind: 'PIPELINE', - PipelineConfig: ['function1'], - RequestMappingTemplate: '## original request template', - ResponseMappingTemplate: '## original response template', - }, - Metadata: { - 'aws:asset:path': 'old-path', + }); + setup.pushStackResourceSummaries( + setup.stackSummaryOf( + 'AppSyncResolver', + 'AWS::AppSync::Resolver', + 'arn:aws:appsync:us-east-1:111111111111:apis/apiId/types/Query/resolvers/myField', + ), + ); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + AppSyncResolver: { + Type: 'AWS::AppSync::Resolver', + Properties: { + ApiId: 'apiId', + FieldName: 'myField', + TypeName: 'Query', + DataSourceName: 'my-datasource', + Kind: 'UNIT', + RequestMappingTemplate: '## new request template', + ResponseMappingTemplate: '## original response template', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, + }, }, }, - }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateResolver).toHaveBeenCalledWith({ + apiId: 'apiId', + dataSourceName: 'my-datasource', + typeName: 'Query', + fieldName: 'myField', + kind: 'UNIT', + requestMappingTemplate: '## new request template', + responseMappingTemplate: '## original response template', + }); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('does not call the updateResolver() API when it receives only a mapping template difference in a Pipeline Resolver', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { AppSyncResolver: { Type: 'AWS::AppSync::Resolver', @@ -131,126 +127,184 @@ test('does not call the updateResolver() API when it receives only a mapping tem DataSourceName: 'my-datasource', Kind: 'PIPELINE', PipelineConfig: ['function1'], - RequestMappingTemplate: '## new request template', + RequestMappingTemplate: '## original request template', ResponseMappingTemplate: '## original response template', }, Metadata: { - 'aws:asset:path': 'new-path', + 'aws:asset:path': 'old-path', }, }, }, - }, - }); + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + AppSyncResolver: { + Type: 'AWS::AppSync::Resolver', + Properties: { + ApiId: 'apiId', + FieldName: 'myField', + TypeName: 'Query', + DataSourceName: 'my-datasource', + Kind: 'PIPELINE', + PipelineConfig: ['function1'], + RequestMappingTemplate: '## new request template', + ResponseMappingTemplate: '## original response template', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, + }, + }, + }, + }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockUpdateResolver).not.toHaveBeenCalled(); -}); + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateFunction).not.toHaveBeenCalled(); + expect(mockUpdateResolver).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); -test('does not call the updateResolver() API when it receives a change that is not a mapping template difference in a Resolver', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - AppSyncResolver: { - Type: 'AWS::AppSync::Resolver', - Properties: { - RequestMappingTemplate: '## original template', - FieldName: 'oldField', - }, - Metadata: { - 'aws:asset:path': 'old-path', - }, - }, - }, + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(deployStackResult?.noOp).toEqual(true); + expect(mockUpdateFunction).not.toHaveBeenCalled(); + expect(mockUpdateResolver).not.toHaveBeenCalled(); + } }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test(`when it receives a change that is not a mapping template difference in a Resolver, it does not call the updateResolver() API in CLASSIC mode + but does call the updateResolver() API in HOTSWAP_ONLY mode`, + async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { AppSyncResolver: { Type: 'AWS::AppSync::Resolver', Properties: { - RequestMappingTemplate: '## new template', - FieldName: 'newField', + ResponseMappingTemplate: '## original response template', + RequestMappingTemplate: '## original request template', + FieldName: 'oldField', + ApiId: 'apiId', + TypeName: 'Query', + }, + Metadata: { + 'aws:asset:path': 'old-path', }, }, }, - }, - }); + }); + setup.pushStackResourceSummaries( + setup.stackSummaryOf( + 'AppSyncResolver', + 'AWS::AppSync::Resolver', + 'arn:aws:appsync:us-east-1:111111111111:apis/apiId/types/Query/resolvers/myField', + ), + ); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + AppSyncResolver: { + Type: 'AWS::AppSync::Resolver', + Properties: { + ResponseMappingTemplate: '## original response template', + RequestMappingTemplate: '## new request template', + FieldName: 'newField', + ApiId: 'apiId', + TypeName: 'Query', + }, + }, + }, + }, + }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockUpdateResolver).not.toHaveBeenCalled(); -}); + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateFunction).not.toHaveBeenCalled(); + expect(mockUpdateResolver).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); -test('does not call the updateResolver() API when a resource with type that is not AWS::AppSync::Resolver but has the same properties is changed', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - AppSyncResolver: { - Type: 'AWS::AppSync::NotAResolver', - Properties: { - RequestMappingTemplate: '## original template', - FieldName: 'oldField', - }, - Metadata: { - 'aws:asset:path': 'old-path', - }, - }, - }, + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateFunction).not.toHaveBeenCalled(); + expect(mockUpdateResolver).toHaveBeenCalledWith({ + apiId: 'apiId', + typeName: 'Query', + fieldName: 'oldField', + requestMappingTemplate: '## new request template', + responseMappingTemplate: '## original response template', + }); + } }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('does not call the updateResolver() API when a resource with type that is not AWS::AppSync::Resolver but has the same properties is changed', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { AppSyncResolver: { Type: 'AWS::AppSync::NotAResolver', Properties: { - RequestMappingTemplate: '## new template', - FieldName: 'newField', + RequestMappingTemplate: '## original template', + FieldName: 'oldField', + }, + Metadata: { + 'aws:asset:path': 'old-path', }, }, }, - }, - }); + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + AppSyncResolver: { + Type: 'AWS::AppSync::NotAResolver', + Properties: { + RequestMappingTemplate: '## new template', + FieldName: 'newField', + }, + }, + }, + }, + }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockUpdateResolver).not.toHaveBeenCalled(); -}); + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateFunction).not.toHaveBeenCalled(); + expect(mockUpdateResolver).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); -test('calls the updateFunction() API when it receives only a mapping template difference in a Function', async () => { - // GIVEN - const mockListFunctions = jest.fn().mockReturnValue({ functions: [{ name: 'my-function', functionId: 'functionId' }] }); - hotswapMockSdkProvider.stubAppSync({ listFunctions: mockListFunctions, updateFunction: mockUpdateFunction }); - - setup.setCurrentCfnStackTemplate({ - Resources: { - AppSyncFunction: { - Type: 'AWS::AppSync::FunctionConfiguration', - Properties: { - Name: 'my-function', - ApiId: 'apiId', - DataSourceName: 'my-datasource', - FunctionVersion: '2018-05-29', - RequestMappingTemplate: '## original request template', - ResponseMappingTemplate: '## original response template', - }, - Metadata: { - 'aws:asset:path': 'old-path', - }, - }, - }, + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(deployStackResult?.noOp).toEqual(true); + expect(mockUpdateFunction).not.toHaveBeenCalled(); + expect(mockUpdateResolver).not.toHaveBeenCalled(); + } }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('calls the updateFunction() API when it receives only a mapping template difference in a Function', async () => { + // GIVEN + const mockListFunctions = jest.fn().mockReturnValue({ functions: [{ name: 'my-function', functionId: 'functionId' }] }); + hotswapMockSdkProvider.stubAppSync({ listFunctions: mockListFunctions, updateFunction: mockUpdateFunction }); + + setup.setCurrentCfnStackTemplate({ Resources: { AppSyncFunction: { Type: 'AWS::AppSync::FunctionConfiguration', @@ -260,108 +314,167 @@ test('calls the updateFunction() API when it receives only a mapping template di DataSourceName: 'my-datasource', FunctionVersion: '2018-05-29', RequestMappingTemplate: '## original request template', - ResponseMappingTemplate: '## new response template', + ResponseMappingTemplate: '## original response template', }, Metadata: { - 'aws:asset:path': 'new-path', + 'aws:asset:path': 'old-path', }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateFunction).toHaveBeenCalledWith({ - apiId: 'apiId', - dataSourceName: 'my-datasource', - functionId: 'functionId', - functionVersion: '2018-05-29', - name: 'my-function', - requestMappingTemplate: '## original request template', - responseMappingTemplate: '## new response template', - }); -}); - -test('does not call the updateFunction() API when it receives a change that is not a mapping template difference in a Function', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - AppSyncFunction: { - Type: 'AWS::AppSync::FunctionConfiguration', - Properties: { - RequestMappingTemplate: '## original template', - Name: 'my-function', - DataSourceName: 'my-datasource', - }, - Metadata: { - 'aws:asset:path': 'old-path', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + AppSyncFunction: { + Type: 'AWS::AppSync::FunctionConfiguration', + Properties: { + Name: 'my-function', + ApiId: 'apiId', + DataSourceName: 'my-datasource', + FunctionVersion: '2018-05-29', + RequestMappingTemplate: '## original request template', + ResponseMappingTemplate: '## new response template', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, + }, }, }, - }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateFunction).toHaveBeenCalledWith({ + apiId: 'apiId', + dataSourceName: 'my-datasource', + functionId: 'functionId', + functionVersion: '2018-05-29', + name: 'my-function', + requestMappingTemplate: '## original request template', + responseMappingTemplate: '## new response template', + }); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test(`when it receives a change that is not a mapping template difference in a Function, it does not call the updateFunction() API in CLASSIC mode + but does in HOTSWAP_ONLY mode`, + async () => { + // GIVEN + const mockListFunctions = jest.fn().mockReturnValue({ functions: [{ name: 'my-function', functionId: 'functionId' }] }); + hotswapMockSdkProvider.stubAppSync({ listFunctions: mockListFunctions, updateFunction: mockUpdateFunction }); + + setup.setCurrentCfnStackTemplate({ Resources: { AppSyncFunction: { Type: 'AWS::AppSync::FunctionConfiguration', Properties: { - RequestMappingTemplate: '## new template', + RequestMappingTemplate: '## original request template', + ResponseMappingTemplate: '## original response template', Name: 'my-function', - DataSourceName: 'new-datasource', + ApiId: 'apiId', + DataSourceName: 'my-datasource', + }, + Metadata: { + 'aws:asset:path': 'old-path', }, }, }, - }, - }); + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + AppSyncFunction: { + Type: 'AWS::AppSync::FunctionConfiguration', + Properties: { + RequestMappingTemplate: '## new request template', + ResponseMappingTemplate: '## original response template', + ApiId: 'apiId', + Name: 'my-function', + DataSourceName: 'new-datasource', + }, + }, + }, + }, + }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockUpdateFunction).not.toHaveBeenCalled(); -}); + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateFunction).not.toHaveBeenCalled(); + expect(mockUpdateResolver).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); -test('does not call the updateFunction() API when a resource with type that is not AWS::AppSync::FunctionConfiguration but has the same properties is changed', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - AppSyncFunction: { - Type: 'AWS::AppSync::NotAFunctionConfiguration', - Properties: { - RequestMappingTemplate: '## original template', - Name: 'my-function', - DataSourceName: 'my-datasource', - }, - Metadata: { - 'aws:asset:path': 'old-path', - }, - }, - }, + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateFunction).toHaveBeenCalledWith({ + apiId: 'apiId', + dataSourceName: 'my-datasource', + functionId: 'functionId', + name: 'my-function', + requestMappingTemplate: '## new request template', + responseMappingTemplate: '## original response template', + }); + expect(mockUpdateResolver).not.toHaveBeenCalled(); + } }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('does not call the updateFunction() API when a resource with type that is not AWS::AppSync::FunctionConfiguration but has the same properties is changed', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { AppSyncFunction: { Type: 'AWS::AppSync::NotAFunctionConfiguration', Properties: { - RequestMappingTemplate: '## new template', - Name: 'my-resolver', + RequestMappingTemplate: '## original template', + Name: 'my-function', DataSourceName: 'my-datasource', }, + Metadata: { + 'aws:asset:path': 'old-path', + }, }, }, - }, - }); + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + AppSyncFunction: { + Type: 'AWS::AppSync::NotAFunctionConfiguration', + Properties: { + RequestMappingTemplate: '## new template', + Name: 'my-resolver', + DataSourceName: 'my-datasource', + }, + }, + }, + }, + }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockUpdateFunction).not.toHaveBeenCalled(); + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateFunction).not.toHaveBeenCalled(); + expect(mockUpdateResolver).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(deployStackResult?.noOp).toEqual(true); + expect(mockUpdateFunction).not.toHaveBeenCalled(); + expect(mockUpdateResolver).not.toHaveBeenCalled(); + } + }); }); diff --git a/packages/aws-cdk/test/api/hotswap/code-build-projects-hotswap-deployments.test.ts b/packages/aws-cdk/test/api/hotswap/code-build-projects-hotswap-deployments.test.ts index fa0de06780ccd..5ed1d9d2cdbf1 100644 --- a/packages/aws-cdk/test/api/hotswap/code-build-projects-hotswap-deployments.test.ts +++ b/packages/aws-cdk/test/api/hotswap/code-build-projects-hotswap-deployments.test.ts @@ -1,4 +1,5 @@ import { CodeBuild } from 'aws-sdk'; +import { HotswapMode } from '../../../lib/api/hotswap/common'; import * as setup from './hotswap-test-setup'; let hotswapMockSdkProvider: setup.HotswapMockSdkProvider; @@ -10,100 +11,93 @@ beforeEach(() => { hotswapMockSdkProvider.setUpdateProjectMock(mockUpdateProject); }); -test('returns undefined when a new CodeBuild Project is added to the Stack', async () => { - // GIVEN - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { - Resources: { - CodeBuildProject: { - Type: 'AWS::CodeBuild::Project', +describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hotswapMode) => { + test('returns undefined when a new CodeBuild Project is added to the Stack', async () => { + // GIVEN + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + CodeBuildProject: { + Type: 'AWS::CodeBuild::Project', + }, }, }, - }, + }); + + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateProject).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(deployStackResult?.noOp).toEqual(true); + expect(mockUpdateProject).not.toHaveBeenCalled(); + } }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).toBeUndefined(); -}); - -test('calls the updateProject() API when it receives only a source difference in a CodeBuild project', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - CodeBuildProject: { - Type: 'AWS::CodeBuild::Project', - Properties: { - Source: { - BuildSpec: 'current-spec', - Type: 'NO_SOURCE', - }, - Name: 'my-project', - }, - Metadata: { - 'aws:asset:path': 'old-path', - }, - }, - }, - }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + test('calls the updateProject() API when it receives only a source difference in a CodeBuild project', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { CodeBuildProject: { Type: 'AWS::CodeBuild::Project', Properties: { Source: { - BuildSpec: 'new-spec', + BuildSpec: 'current-spec', Type: 'NO_SOURCE', }, Name: 'my-project', }, Metadata: { - 'aws:asset:path': 'new-path', + 'aws:asset:path': 'old-path', }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateProject).toHaveBeenCalledWith({ - name: 'my-project', - source: { - type: 'NO_SOURCE', - buildspec: 'new-spec', - }, - }); -}); - -test('calls the updateProject() API when it receives only a source version difference in a CodeBuild project', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - CodeBuildProject: { - Type: 'AWS::CodeBuild::Project', - Properties: { - Source: { - BuildSpec: 'current-spec', - Type: 'NO_SOURCE', - }, - Name: 'my-project', - SourceVersion: 'v1', - }, - Metadata: { - 'aws:asset:path': 'old-path', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + CodeBuildProject: { + Type: 'AWS::CodeBuild::Project', + Properties: { + Source: { + BuildSpec: 'new-spec', + Type: 'NO_SOURCE', + }, + Name: 'my-project', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, + }, }, }, - }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateProject).toHaveBeenCalledWith({ + name: 'my-project', + source: { + type: 'NO_SOURCE', + buildspec: 'new-spec', + }, + }); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('calls the updateProject() API when it receives only a source version difference in a CodeBuild project', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { CodeBuildProject: { Type: 'AWS::CodeBuild::Project', @@ -113,67 +107,49 @@ test('calls the updateProject() API when it receives only a source version diffe Type: 'NO_SOURCE', }, Name: 'my-project', - SourceVersion: 'v2', + SourceVersion: 'v1', }, Metadata: { - 'aws:asset:path': 'new-path', + 'aws:asset:path': 'old-path', }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateProject).toHaveBeenCalledWith({ - name: 'my-project', - sourceVersion: 'v2', - }); -}); - -test('calls the updateProject() API when it receives only an environment difference in a CodeBuild project', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - CodeBuildProject: { - Type: 'AWS::CodeBuild::Project', - Properties: { - Source: { - BuildSpec: 'current-spec', - Type: 'NO_SOURCE', - }, - Name: 'my-project', - Environment: { - ComputeType: 'BUILD_GENERAL1_SMALL', - EnvironmentVariables: [ - { - Name: 'SUPER_IMPORTANT_ENV_VAR', - Type: 'PLAINTEXT', - Value: 'super cool value', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + CodeBuildProject: { + Type: 'AWS::CodeBuild::Project', + Properties: { + Source: { + BuildSpec: 'current-spec', + Type: 'NO_SOURCE', }, - { - Name: 'SECOND_IMPORTANT_ENV_VAR', - Type: 'PLAINTEXT', - Value: 'yet another super cool value', - }, - ], - Image: 'aws/codebuild/standard:1.0', - ImagePullCredentialsType: 'CODEBUILD', - PrivilegedMode: false, - Type: 'LINUX_CONTAINER', + Name: 'my-project', + SourceVersion: 'v2', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, }, }, - Metadata: { - 'aws:asset:path': 'old-path', - }, }, - }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateProject).toHaveBeenCalledWith({ + name: 'my-project', + sourceVersion: 'v2', + }); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('calls the updateProject() API when it receives only an environment difference in a CodeBuild project', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { CodeBuildProject: { Type: 'AWS::CodeBuild::Project', @@ -189,12 +165,12 @@ test('calls the updateProject() API when it receives only an environment differe { Name: 'SUPER_IMPORTANT_ENV_VAR', Type: 'PLAINTEXT', - Value: 'changed value', + Value: 'super cool value', }, { - Name: 'NEW_IMPORTANT_ENV_VAR', + Name: 'SECOND_IMPORTANT_ENV_VAR', Type: 'PLAINTEXT', - Value: 'new value', + Value: 'yet another super cool value', }, ], Image: 'aws/codebuild/standard:1.0', @@ -204,72 +180,82 @@ test('calls the updateProject() API when it receives only an environment differe }, }, Metadata: { - 'aws:asset:path': 'new-path', + 'aws:asset:path': 'old-path', }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateProject).toHaveBeenCalledWith({ - name: 'my-project', - environment: { - computeType: 'BUILD_GENERAL1_SMALL', - environmentVariables: [ - { - name: 'SUPER_IMPORTANT_ENV_VAR', - type: 'PLAINTEXT', - value: 'changed value', - }, - { - name: 'NEW_IMPORTANT_ENV_VAR', - type: 'PLAINTEXT', - value: 'new value', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + CodeBuildProject: { + Type: 'AWS::CodeBuild::Project', + Properties: { + Source: { + BuildSpec: 'current-spec', + Type: 'NO_SOURCE', + }, + Name: 'my-project', + Environment: { + ComputeType: 'BUILD_GENERAL1_SMALL', + EnvironmentVariables: [ + { + Name: 'SUPER_IMPORTANT_ENV_VAR', + Type: 'PLAINTEXT', + Value: 'changed value', + }, + { + Name: 'NEW_IMPORTANT_ENV_VAR', + Type: 'PLAINTEXT', + Value: 'new value', + }, + ], + Image: 'aws/codebuild/standard:1.0', + ImagePullCredentialsType: 'CODEBUILD', + PrivilegedMode: false, + Type: 'LINUX_CONTAINER', + }, + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, + }, }, - ], - image: 'aws/codebuild/standard:1.0', - imagePullCredentialsType: 'CODEBUILD', - privilegedMode: false, - type: 'LINUX_CONTAINER', - }, - }); -}); - -test("correctly evaluates the project's name when it references a different resource from the template", async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Bucket: { - Type: 'AWS::S3::Bucket', }, - CodeBuildProject: { - Type: 'AWS::CodeBuild::Project', - Properties: { - Source: { - BuildSpec: 'current-spec', - Type: 'NO_SOURCE', - }, - Name: { - 'Fn::Join': ['-', [ - { Ref: 'Bucket' }, - 'project', - ]], + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateProject).toHaveBeenCalledWith({ + name: 'my-project', + environment: { + computeType: 'BUILD_GENERAL1_SMALL', + environmentVariables: [ + { + name: 'SUPER_IMPORTANT_ENV_VAR', + type: 'PLAINTEXT', + value: 'changed value', }, - }, - Metadata: { - 'aws:asset:path': 'old-path', - }, + { + name: 'NEW_IMPORTANT_ENV_VAR', + type: 'PLAINTEXT', + value: 'new value', + }, + ], + image: 'aws/codebuild/standard:1.0', + imagePullCredentialsType: 'CODEBUILD', + privilegedMode: false, + type: 'LINUX_CONTAINER', }, - }, + }); }); - setup.pushStackResourceSummaries(setup.stackSummaryOf('Bucket', 'AWS::S3::Bucket', 'mybucket')); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test("correctly evaluates the project's name when it references a different resource from the template", async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Bucket: { Type: 'AWS::S3::Bucket', @@ -278,7 +264,7 @@ test("correctly evaluates the project's name when it references a different reso Type: 'AWS::CodeBuild::Project', Properties: { Source: { - BuildSpec: 'new-spec', + BuildSpec: 'current-spec', Type: 'NO_SOURCE', }, Name: { @@ -293,49 +279,53 @@ test("correctly evaluates the project's name when it references a different reso }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateProject).toHaveBeenCalledWith({ - name: 'mybucket-project', - source: { - type: 'NO_SOURCE', - buildspec: 'new-spec', - }, - }); -}); - -test("correctly falls back to taking the project's name from the current stack if it can't evaluate it in the template", async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Parameters: { - Param1: { Type: 'String' }, - AssetBucketParam: { Type: 'String' }, - }, - Resources: { - CodeBuildProject: { - Type: 'AWS::CodeBuild::Project', - Properties: { - Source: { - BuildSpec: 'current-spec', - Type: 'NO_SOURCE', - }, - Name: { Ref: 'Param1' }, - }, - Metadata: { - 'aws:asset:path': 'old-path', + }); + setup.pushStackResourceSummaries(setup.stackSummaryOf('Bucket', 'AWS::S3::Bucket', 'mybucket')); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Bucket: { + Type: 'AWS::S3::Bucket', + }, + CodeBuildProject: { + Type: 'AWS::CodeBuild::Project', + Properties: { + Source: { + BuildSpec: 'new-spec', + Type: 'NO_SOURCE', + }, + Name: { + 'Fn::Join': ['-', [ + { Ref: 'Bucket' }, + 'project', + ]], + }, + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, + }, }, }, - }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateProject).toHaveBeenCalledWith({ + name: 'mybucket-project', + source: { + type: 'NO_SOURCE', + buildspec: 'new-spec', + }, + }); }); - setup.pushStackResourceSummaries(setup.stackSummaryOf('CodeBuildProject', 'AWS::CodeBuild::Project', 'my-project')); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test("correctly falls back to taking the project's name from the current stack if it can't evaluate it in the template", async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Parameters: { Param1: { Type: 'String' }, AssetBucketParam: { Type: 'String' }, @@ -345,57 +335,59 @@ test("correctly falls back to taking the project's name from the current stack i Type: 'AWS::CodeBuild::Project', Properties: { Source: { - BuildSpec: 'new-spec', + BuildSpec: 'current-spec', Type: 'NO_SOURCE', }, Name: { Ref: 'Param1' }, }, Metadata: { - 'aws:asset:path': 'new-path', + 'aws:asset:path': 'old-path', }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact, { AssetBucketParam: 'asset-bucket' }); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateProject).toHaveBeenCalledWith({ - name: 'my-project', - source: { - type: 'NO_SOURCE', - buildspec: 'new-spec', - }, - }); -}); - -test("will not perform a hotswap deployment if it cannot find a Ref target (outside the project's name)", async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Parameters: { - Param1: { Type: 'String' }, - }, - Resources: { - CodeBuildProject: { - Type: 'AWS::CodeBuild::Project', - Properties: { - Source: { - BuildSpec: { 'Fn::Sub': '${Param1}' }, - Type: 'NO_SOURCE', + }); + setup.pushStackResourceSummaries(setup.stackSummaryOf('CodeBuildProject', 'AWS::CodeBuild::Project', 'my-project')); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Parameters: { + Param1: { Type: 'String' }, + AssetBucketParam: { Type: 'String' }, + }, + Resources: { + CodeBuildProject: { + Type: 'AWS::CodeBuild::Project', + Properties: { + Source: { + BuildSpec: 'new-spec', + Type: 'NO_SOURCE', + }, + Name: { Ref: 'Param1' }, + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, }, }, - Metadata: { - 'aws:asset:path': 'old-path', - }, }, - }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact, { AssetBucketParam: 'asset-bucket' }); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateProject).toHaveBeenCalledWith({ + name: 'my-project', + source: { + type: 'NO_SOURCE', + buildspec: 'new-spec', + }, + }); }); - setup.pushStackResourceSummaries(setup.stackSummaryOf('CodeBuildProject', 'AWS::CodeBuild::Project', 'my-project')); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test("will not perform a hotswap deployment if it cannot find a Ref target (outside the project's name)", async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Parameters: { Param1: { Type: 'String' }, }, @@ -405,50 +397,47 @@ test("will not perform a hotswap deployment if it cannot find a Ref target (outs Properties: { Source: { BuildSpec: { 'Fn::Sub': '${Param1}' }, - Type: 'CODEPIPELINE', + Type: 'NO_SOURCE', }, }, Metadata: { - 'aws:asset:path': 'new-path', + 'aws:asset:path': 'old-path', }, }, }, - }, - }); - - // THEN - await expect(() => - hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact), - ).rejects.toThrow(/Parameter or resource 'Param1' could not be found for evaluation/); -}); - -test("will not perform a hotswap deployment if it doesn't know how to handle a specific attribute (outside the project's name)", async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Bucket: { - Type: 'AWS::S3::Bucket', - }, - CodeBuildProject: { - Type: 'AWS::CodeBuild::Project', - Properties: { - Source: { - BuildSpec: { 'Fn::GetAtt': ['Bucket', 'UnknownAttribute'] }, - Type: 'NO_SOURCE', + }); + setup.pushStackResourceSummaries(setup.stackSummaryOf('CodeBuildProject', 'AWS::CodeBuild::Project', 'my-project')); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Parameters: { + Param1: { Type: 'String' }, + }, + Resources: { + CodeBuildProject: { + Type: 'AWS::CodeBuild::Project', + Properties: { + Source: { + BuildSpec: { 'Fn::Sub': '${Param1}' }, + Type: 'CODEPIPELINE', + }, + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, }, }, - Metadata: { - 'aws:asset:path': 'old-path', - }, }, - }, + }); + + // THEN + await expect(() => + hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact), + ).rejects.toThrow(/Parameter or resource 'Param1' could not be found for evaluation/); }); - setup.pushStackResourceSummaries( - setup.stackSummaryOf('CodeBuildProject', 'AWS::CodeBuild::Project', 'my-project'), - setup.stackSummaryOf('Bucket', 'AWS::S3::Bucket', 'my-bucket'), - ); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test("will not perform a hotswap deployment if it doesn't know how to handle a specific attribute (outside the project's name)", async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Bucket: { Type: 'AWS::S3::Bucket', @@ -458,49 +447,56 @@ test("will not perform a hotswap deployment if it doesn't know how to handle a s Properties: { Source: { BuildSpec: { 'Fn::GetAtt': ['Bucket', 'UnknownAttribute'] }, - Type: 'S3', + Type: 'NO_SOURCE', }, }, Metadata: { - 'aws:asset:path': 'new-path', + 'aws:asset:path': 'old-path', }, }, }, - }, - }); - - // THEN - await expect(() => - hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact), - ).rejects.toThrow("We don't support the 'UnknownAttribute' attribute of the 'AWS::S3::Bucket' resource. This is a CDK limitation. Please report it at https://github.com/aws/aws-cdk/issues/new/choose"); -}); - -test('calls the updateProject() API when it receives a difference in a CodeBuild project with no name', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - CodeBuildProject: { - Type: 'AWS::CodeBuild::Project', - Properties: { - Source: { - BuildSpec: 'current-spec', - Type: 'NO_SOURCE', + }); + setup.pushStackResourceSummaries( + setup.stackSummaryOf('CodeBuildProject', 'AWS::CodeBuild::Project', 'my-project'), + setup.stackSummaryOf('Bucket', 'AWS::S3::Bucket', 'my-bucket'), + ); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Bucket: { + Type: 'AWS::S3::Bucket', + }, + CodeBuildProject: { + Type: 'AWS::CodeBuild::Project', + Properties: { + Source: { + BuildSpec: { 'Fn::GetAtt': ['Bucket', 'UnknownAttribute'] }, + Type: 'S3', + }, + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, }, - }, - Metadata: { - 'aws:asset:path': 'current-path', }, }, - }, + }); + + // THEN + await expect(() => + hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact), + ).rejects.toThrow("We don't support the 'UnknownAttribute' attribute of the 'AWS::S3::Bucket' resource. This is a CDK limitation. Please report it at https://github.com/aws/aws-cdk/issues/new/choose"); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('calls the updateProject() API when it receives a difference in a CodeBuild project with no name', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { CodeBuildProject: { Type: 'AWS::CodeBuild::Project', Properties: { Source: { - BuildSpec: 'new-spec', + BuildSpec: 'current-spec', Type: 'NO_SOURCE', }, }, @@ -509,42 +505,44 @@ test('calls the updateProject() API when it receives a difference in a CodeBuild }, }, }, - }, - }); - - // WHEN - setup.pushStackResourceSummaries(setup.stackSummaryOf('CodeBuildProject', 'AWS::CodeBuild::Project', 'mock-project-resource-id')); - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateProject).toHaveBeenCalledWith({ - name: 'mock-project-resource-id', - source: { - type: 'NO_SOURCE', - buildspec: 'new-spec', - }, - }); -}); - -test('does not call the updateProject() API when it receives a change that is not Source, SourceVersion, or Environment difference in a CodeBuild project', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - CodeBuildProject: { - Type: 'AWS::CodeBuild::Project', - Properties: { - Source: { - BuildSpec: 'current-spec', - Type: 'NO_SOURCE', - }, - ConcurrentBuildLimit: 1, + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + CodeBuildProject: { + Type: 'AWS::CodeBuild::Project', + Properties: { + Source: { + BuildSpec: 'new-spec', + Type: 'NO_SOURCE', + }, + }, + Metadata: { + 'aws:asset:path': 'current-path', + }, + }, }, }, - }, + }); + + // WHEN + setup.pushStackResourceSummaries(setup.stackSummaryOf('CodeBuildProject', 'AWS::CodeBuild::Project', 'mock-project-resource-id')); + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateProject).toHaveBeenCalledWith({ + name: 'mock-project-resource-id', + source: { + type: 'NO_SOURCE', + buildspec: 'new-spec', + }, + }); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('does not call the updateProject() API when it receives a change that is not Source, SourceVersion, or Environment difference in a CodeBuild project', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { CodeBuildProject: { Type: 'AWS::CodeBuild::Project', @@ -553,47 +551,113 @@ test('does not call the updateProject() API when it receives a change that is no BuildSpec: 'current-spec', Type: 'NO_SOURCE', }, - ConcurrentBuildLimit: 2, + ConcurrentBuildLimit: 1, }, }, }, - }, + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + CodeBuildProject: { + Type: 'AWS::CodeBuild::Project', + Properties: { + Source: { + BuildSpec: 'current-spec', + Type: 'NO_SOURCE', + }, + ConcurrentBuildLimit: 2, + }, + }, + }, + }, + }); + + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateProject).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(deployStackResult?.noOp).toEqual(true); + expect(mockUpdateProject).not.toHaveBeenCalled(); + } }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockUpdateProject).not.toHaveBeenCalled(); -}); - -test('does not call the updateProject() API when a resource with type that is not AWS::CodeBuild::Project but has the same properties is changed', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - CodeBuildProject: { - Type: 'AWS::NotCodeBuild::NotAProject', - Properties: { - Source: { - BuildSpec: 'current-spec', - Type: 'NO_SOURCE', + test(`when it receives a change that is not Source, SourceVersion, or Environment difference in a CodeBuild project alongside a hotswappable change, + it does not call the updateProject() API in CLASSIC mode, but it does in HOTSWAP_ONLY mode`, + async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ + Resources: { + CodeBuildProject: { + Type: 'AWS::CodeBuild::Project', + Properties: { + Source: { + BuildSpec: 'current-spec', + Type: 'NO_SOURCE', + }, + ConcurrentBuildLimit: 1, }, }, - Metadata: { - 'aws:asset:path': 'old-path', + }, + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + CodeBuildProject: { + Type: 'AWS::CodeBuild::Project', + Properties: { + Source: { + BuildSpec: 'new-spec', + Type: 'NO_SOURCE', + }, + ConcurrentBuildLimit: 2, + }, + }, }, }, - }, + }); + + setup.pushStackResourceSummaries(setup.stackSummaryOf('CodeBuildProject', 'AWS::CodeBuild::Project', 'mock-project-resource-id')); + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateProject).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateProject).toHaveBeenCalledWith({ + name: 'mock-project-resource-id', + source: { + type: 'NO_SOURCE', + buildspec: 'new-spec', + }, + }); + } }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + test('does not call the updateProject() API when a resource with type that is not AWS::CodeBuild::Project but has the same properties is changed', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { CodeBuildProject: { Type: 'AWS::NotCodeBuild::NotAProject', Properties: { Source: { - BuildSpec: 'new-spec', + BuildSpec: 'current-spec', Type: 'NO_SOURCE', }, }, @@ -602,13 +666,41 @@ test('does not call the updateProject() API when a resource with type that is no }, }, }, - }, + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + CodeBuildProject: { + Type: 'AWS::NotCodeBuild::NotAProject', + Properties: { + Source: { + BuildSpec: 'new-spec', + Type: 'NO_SOURCE', + }, + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, + }, + }, + }, + }); + + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateProject).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(deployStackResult?.noOp).toEqual(true); + expect(mockUpdateProject).not.toHaveBeenCalled(); + } }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockUpdateProject).not.toHaveBeenCalled(); -}); \ No newline at end of file +}); diff --git a/packages/aws-cdk/test/api/hotswap/ecs-services-hotswap-deployments.test.ts b/packages/aws-cdk/test/api/hotswap/ecs-services-hotswap-deployments.test.ts index dda972f5a90b9..d2903cf5b5bd7 100644 --- a/packages/aws-cdk/test/api/hotswap/ecs-services-hotswap-deployments.test.ts +++ b/packages/aws-cdk/test/api/hotswap/ecs-services-hotswap-deployments.test.ts @@ -1,4 +1,5 @@ import * as AWS from 'aws-sdk'; +import { HotswapMode } from '../../../lib/api/hotswap/common'; import * as setup from './hotswap-test-setup'; let hotswapMockSdkProvider: setup.HotswapMockSdkProvider; @@ -28,45 +29,17 @@ beforeEach(() => { }); }); -test('should call registerTaskDefinition and updateService for a difference only in the TaskDefinition with a Family property', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - TaskDef: { - Type: 'AWS::ECS::TaskDefinition', - Properties: { - Family: 'my-task-def', - ContainerDefinitions: [ - { Image: 'image1' }, - ], - }, - }, - Service: { - Type: 'AWS::ECS::Service', - Properties: { - TaskDefinition: { Ref: 'TaskDef' }, - }, - }, - }, - }); - setup.pushStackResourceSummaries( - setup.stackSummaryOf('Service', 'AWS::ECS::Service', - 'arn:aws:ecs:region:account:service/my-cluster/my-service'), - ); - mockRegisterTaskDef.mockReturnValue({ - taskDefinition: { - taskDefinitionArn: 'arn:aws:ecs:region:account:task-definition/my-task-def:3', - }, - }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { +describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hotswapMode) => { + test('should call registerTaskDefinition and updateService for a difference only in the TaskDefinition with a Family property', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { TaskDef: { Type: 'AWS::ECS::TaskDefinition', Properties: { Family: 'my-task-def', ContainerDefinitions: [ - { Image: 'image2' }, + { Image: 'image1' }, ], }, }, @@ -77,73 +50,72 @@ test('should call registerTaskDefinition and updateService for a difference only }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockRegisterTaskDef).toBeCalledWith({ - family: 'my-task-def', - containerDefinitions: [ - { image: 'image2' }, - ], - }); - expect(mockUpdateService).toBeCalledWith({ - service: 'arn:aws:ecs:region:account:service/my-cluster/my-service', - cluster: 'my-cluster', - taskDefinition: 'arn:aws:ecs:region:account:task-definition/my-task-def:3', - deploymentConfiguration: { - minimumHealthyPercent: 0, - }, - forceNewDeployment: true, - }); -}); - -test('any other TaskDefinition property change besides ContainerDefinition cannot be hotswapped', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - TaskDef: { - Type: 'AWS::ECS::TaskDefinition', - Properties: { - Family: 'my-task-def', - ContainerDefinitions: [ - { Image: 'image1' }, - ], - Cpu: '256', - }, + }); + setup.pushStackResourceSummaries( + setup.stackSummaryOf('Service', 'AWS::ECS::Service', + 'arn:aws:ecs:region:account:service/my-cluster/my-service'), + ); + mockRegisterTaskDef.mockReturnValue({ + taskDefinition: { + taskDefinitionArn: 'arn:aws:ecs:region:account:task-definition/my-task-def:3', }, - Service: { - Type: 'AWS::ECS::Service', - Properties: { - TaskDefinition: { Ref: 'TaskDef' }, + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + TaskDef: { + Type: 'AWS::ECS::TaskDefinition', + Properties: { + Family: 'my-task-def', + ContainerDefinitions: [ + { Image: 'image2' }, + ], + }, + }, + Service: { + Type: 'AWS::ECS::Service', + Properties: { + TaskDefinition: { Ref: 'TaskDef' }, + }, + }, }, }, - }, - }); - setup.pushStackResourceSummaries( - setup.stackSummaryOf('Service', 'AWS::ECS::Service', - 'arn:aws:ecs:region:account:service/my-cluster/my-service'), - ); - mockRegisterTaskDef.mockReturnValue({ - taskDefinition: { - taskDefinitionArn: 'arn:aws:ecs:region:account:task-definition/my-task-def:3', - }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockRegisterTaskDef).toBeCalledWith({ + family: 'my-task-def', + containerDefinitions: [ + { image: 'image2' }, + ], + }); + expect(mockUpdateService).toBeCalledWith({ + service: 'arn:aws:ecs:region:account:service/my-cluster/my-service', + cluster: 'my-cluster', + taskDefinition: 'arn:aws:ecs:region:account:task-definition/my-task-def:3', + deploymentConfiguration: { + minimumHealthyPercent: 0, + }, + forceNewDeployment: true, + }); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('any other TaskDefinition property change besides ContainerDefinition cannot be hotswapped in CLASSIC mode but does not block HOTSWAP_ONLY mode deployments', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { TaskDef: { Type: 'AWS::ECS::TaskDefinition', Properties: { Family: 'my-task-def', ContainerDefinitions: [ - { Image: 'image2' }, + { Image: 'image1' }, ], - Cpu: '512', + Cpu: '256', }, }, Service: { @@ -153,56 +125,84 @@ test('any other TaskDefinition property change besides ContainerDefinition canno }, }, }, - }, - }); + }); + setup.pushStackResourceSummaries( + setup.stackSummaryOf('Service', 'AWS::ECS::Service', + 'arn:aws:ecs:region:account:service/my-cluster/my-service'), + ); + mockRegisterTaskDef.mockReturnValue({ + taskDefinition: { + taskDefinitionArn: 'arn:aws:ecs:region:account:task-definition/my-task-def:3', + }, + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + TaskDef: { + Type: 'AWS::ECS::TaskDefinition', + Properties: { + Family: 'my-task-def', + ContainerDefinitions: [ + { Image: 'image2' }, + ], + Cpu: '512', + }, + }, + Service: { + Type: 'AWS::ECS::Service', + Properties: { + TaskDefinition: { Ref: 'TaskDef' }, + }, + }, + }, + }, + }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN - expect(deployStackResult).toBeUndefined(); -}); + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockRegisterTaskDef).not.toHaveBeenCalled(); + expect(mockUpdateService).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); -test('should call registerTaskDefinition and updateService for a difference only in the TaskDefinition without a Family property', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - TaskDef: { - Type: 'AWS::ECS::TaskDefinition', - Properties: { - ContainerDefinitions: [ - { Image: 'image1' }, - ], - }, - }, - Service: { - Type: 'AWS::ECS::Service', - Properties: { - TaskDefinition: { Ref: 'TaskDef' }, + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockRegisterTaskDef).toBeCalledWith({ + family: 'my-task-def', + containerDefinitions: [ + { image: 'image2' }, + ], + cpu: '256', // this uses the old value because a new value could cause a service replacement + }); + expect(mockUpdateService).toBeCalledWith({ + service: 'arn:aws:ecs:region:account:service/my-cluster/my-service', + cluster: 'my-cluster', + taskDefinition: 'arn:aws:ecs:region:account:task-definition/my-task-def:3', + deploymentConfiguration: { + minimumHealthyPercent: 0, }, - }, - }, - }); - setup.pushStackResourceSummaries( - setup.stackSummaryOf('TaskDef', 'AWS::ECS::TaskDefinition', - 'arn:aws:ecs:region:account:task-definition/my-task-def:2'), - setup.stackSummaryOf('Service', 'AWS::ECS::Service', - 'arn:aws:ecs:region:account:service/my-cluster/my-service'), - ); - mockRegisterTaskDef.mockReturnValue({ - taskDefinition: { - taskDefinitionArn: 'arn:aws:ecs:region:account:task-definition/my-task-def:3', - }, + forceNewDeployment: true, + }); + } }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('deleting any other TaskDefinition property besides ContainerDefinition results in a full deployment in CLASSIC mode and a hotswap deployment in HOTSWAP_ONLY mode', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { TaskDef: { Type: 'AWS::ECS::TaskDefinition', Properties: { + Family: 'my-task-def', ContainerDefinitions: [ - { Image: 'image2' }, + { Image: 'image1' }, ], + Cpu: '256', }, }, Service: { @@ -212,126 +212,218 @@ test('should call registerTaskDefinition and updateService for a difference only }, }, }, - }, - }); + }); + setup.pushStackResourceSummaries( + setup.stackSummaryOf('Service', 'AWS::ECS::Service', + 'arn:aws:ecs:region:account:service/my-cluster/my-service'), + ); + mockRegisterTaskDef.mockReturnValue({ + taskDefinition: { + taskDefinitionArn: 'arn:aws:ecs:region:account:task-definition/my-task-def:3', + }, + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + TaskDef: { + Type: 'AWS::ECS::TaskDefinition', + Properties: { + Family: 'my-task-def', + ContainerDefinitions: [ + { Image: 'image2' }, + ], + }, + }, + Service: { + Type: 'AWS::ECS::Service', + Properties: { + TaskDefinition: { Ref: 'TaskDef' }, + }, + }, + }, + }, + }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockRegisterTaskDef).toBeCalledWith({ - family: 'my-task-def', - containerDefinitions: [ - { image: 'image2' }, - ], - }); - expect(mockUpdateService).toBeCalledWith({ - service: 'arn:aws:ecs:region:account:service/my-cluster/my-service', - cluster: 'my-cluster', - taskDefinition: 'arn:aws:ecs:region:account:task-definition/my-task-def:3', - deploymentConfiguration: { - minimumHealthyPercent: 0, - }, - forceNewDeployment: true, - }); -}); + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockRegisterTaskDef).not.toHaveBeenCalled(); + expect(mockUpdateService).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); -test('a difference just in a TaskDefinition, without any services using it, is not hotswappable', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - TaskDef: { - Type: 'AWS::ECS::TaskDefinition', - Properties: { - ContainerDefinitions: [ - { Image: 'image1' }, - ], + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockRegisterTaskDef).toBeCalledWith({ + family: 'my-task-def', + containerDefinitions: [ + { image: 'image2' }, + ], + cpu: '256', // this uses the old value because a new value could cause a service replacement + }); + expect(mockUpdateService).toBeCalledWith({ + service: 'arn:aws:ecs:region:account:service/my-cluster/my-service', + cluster: 'my-cluster', + taskDefinition: 'arn:aws:ecs:region:account:task-definition/my-task-def:3', + deploymentConfiguration: { + minimumHealthyPercent: 0, }, - }, - }, + forceNewDeployment: true, + }); + } }); - setup.pushStackResourceSummaries( - setup.stackSummaryOf('TaskDef', 'AWS::ECS::TaskDefinition', - 'arn:aws:ecs:region:account:task-definition/my-task-def:2'), - ); - mockRegisterTaskDef.mockReturnValue({ - taskDefinition: { - taskDefinitionArn: 'arn:aws:ecs:region:account:task-definition/my-task-def:3', - }, - }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('should call registerTaskDefinition and updateService for a difference only in the TaskDefinition without a Family property', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { TaskDef: { Type: 'AWS::ECS::TaskDefinition', Properties: { ContainerDefinitions: [ - { Image: 'image2' }, + { Image: 'image1' }, ], }, }, + Service: { + Type: 'AWS::ECS::Service', + Properties: { + TaskDefinition: { Ref: 'TaskDef' }, + }, + }, }, - }, - }); + }); + setup.pushStackResourceSummaries( + setup.stackSummaryOf('TaskDef', 'AWS::ECS::TaskDefinition', + 'arn:aws:ecs:region:account:task-definition/my-task-def:2'), + setup.stackSummaryOf('Service', 'AWS::ECS::Service', + 'arn:aws:ecs:region:account:service/my-cluster/my-service'), + ); + mockRegisterTaskDef.mockReturnValue({ + taskDefinition: { + taskDefinitionArn: 'arn:aws:ecs:region:account:task-definition/my-task-def:3', + }, + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + TaskDef: { + Type: 'AWS::ECS::TaskDefinition', + Properties: { + ContainerDefinitions: [ + { Image: 'image2' }, + ], + }, + }, + Service: { + Type: 'AWS::ECS::Service', + Properties: { + TaskDefinition: { Ref: 'TaskDef' }, + }, + }, + }, + }, + }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockRegisterTaskDef).not.toHaveBeenCalled(); -}); + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockRegisterTaskDef).toBeCalledWith({ + family: 'my-task-def', + containerDefinitions: [ + { image: 'image2' }, + ], + }); + expect(mockUpdateService).toBeCalledWith({ + service: 'arn:aws:ecs:region:account:service/my-cluster/my-service', + cluster: 'my-cluster', + taskDefinition: 'arn:aws:ecs:region:account:task-definition/my-task-def:3', + deploymentConfiguration: { + minimumHealthyPercent: 0, + }, + forceNewDeployment: true, + }); + }); -test('if anything besides an ECS Service references the changed TaskDefinition, hotswapping is not possible', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - TaskDef: { - Type: 'AWS::ECS::TaskDefinition', - Properties: { - Family: 'my-task-def', - ContainerDefinitions: [ - { Image: 'image1' }, - ], + test('a difference just in a TaskDefinition, without any services using it, is not hotswappable in FALL_BACK mode', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ + Resources: { + TaskDef: { + Type: 'AWS::ECS::TaskDefinition', + Properties: { + ContainerDefinitions: [ + { Image: 'image1' }, + ], + }, }, }, - Service: { - Type: 'AWS::ECS::Service', - Properties: { - TaskDefinition: { Ref: 'TaskDef' }, - }, + }); + setup.pushStackResourceSummaries( + setup.stackSummaryOf('TaskDef', 'AWS::ECS::TaskDefinition', + 'arn:aws:ecs:region:account:task-definition/my-task-def:2'), + ); + mockRegisterTaskDef.mockReturnValue({ + taskDefinition: { + taskDefinitionArn: 'arn:aws:ecs:region:account:task-definition/my-task-def:3', }, - Function: { - Type: 'AWS::Lambda::Function', - Properties: { - Environment: { - Variables: { - TaskDefRevArn: { Ref: 'TaskDef' }, + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + TaskDef: { + Type: 'AWS::ECS::TaskDefinition', + Properties: { + ContainerDefinitions: [ + { Image: 'image2' }, + ], }, }, }, }, - }, - }); - setup.pushStackResourceSummaries( - setup.stackSummaryOf('Service', 'AWS::ECS::Service', - 'arn:aws:ecs:region:account:service/my-cluster/my-service'), - ); - mockRegisterTaskDef.mockReturnValue({ - taskDefinition: { - taskDefinitionArn: 'arn:aws:ecs:region:account:task-definition/my-task-def:3', - }, + }); + + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockRegisterTaskDef).not.toHaveBeenCalled(); + expect(mockUpdateService).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockRegisterTaskDef).toBeCalledWith({ + family: 'my-task-def', + containerDefinitions: [ + { image: 'image2' }, + ], + }); + + expect(mockUpdateService).not.toHaveBeenCalledWith(); + } }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('if anything besides an ECS Service references the changed TaskDefinition, hotswapping is not possible in CLASSIC mode but is possible in HOTSWAP_ONLY', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { TaskDef: { Type: 'AWS::ECS::TaskDefinition', Properties: { Family: 'my-task-def', ContainerDefinitions: [ - { Image: 'image2' }, + { Image: 'image1' }, ], }, }, @@ -352,73 +444,90 @@ test('if anything besides an ECS Service references the changed TaskDefinition, }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockRegisterTaskDef).not.toHaveBeenCalled(); -}); - -test('should call registerTaskDefinition with certain properties not lowercased', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - TaskDef: { - Type: 'AWS::ECS::TaskDefinition', - Properties: { - Family: 'my-task-def', - ContainerDefinitions: [ - { Image: 'image1' }, - ], - Volumes: [ - { - DockerVolumeConfiguration: { - DriverOpts: { Option1: 'option1' }, - Labels: { Label1: 'label1' }, + }); + setup.pushStackResourceSummaries( + setup.stackSummaryOf('Service', 'AWS::ECS::Service', + 'arn:aws:ecs:region:account:service/my-cluster/my-service'), + ); + mockRegisterTaskDef.mockReturnValue({ + taskDefinition: { + taskDefinitionArn: 'arn:aws:ecs:region:account:task-definition/my-task-def:3', + }, + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + TaskDef: { + Type: 'AWS::ECS::TaskDefinition', + Properties: { + Family: 'my-task-def', + ContainerDefinitions: [ + { Image: 'image2' }, + ], + }, + }, + Service: { + Type: 'AWS::ECS::Service', + Properties: { + TaskDefinition: { Ref: 'TaskDef' }, + }, + }, + Function: { + Type: 'AWS::Lambda::Function', + Properties: { + Environment: { + Variables: { + TaskDefRevArn: { Ref: 'TaskDef' }, + }, }, }, - ], + }, }, }, - Service: { - Type: 'AWS::ECS::Service', - Properties: { - TaskDefinition: { Ref: 'TaskDef' }, + }); + + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockRegisterTaskDef).not.toHaveBeenCalled(); + expect(mockUpdateService).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockRegisterTaskDef).toBeCalledWith({ + family: 'my-task-def', + containerDefinitions: [ + { image: 'image2' }, + ], + }); + expect(mockUpdateService).toBeCalledWith({ + service: 'arn:aws:ecs:region:account:service/my-cluster/my-service', + cluster: 'my-cluster', + taskDefinition: 'arn:aws:ecs:region:account:task-definition/my-task-def:3', + deploymentConfiguration: { + minimumHealthyPercent: 0, }, - }, - }, - }); - setup.pushStackResourceSummaries( - setup.stackSummaryOf('Service', 'AWS::ECS::Service', - 'arn:aws:ecs:region:account:service/my-cluster/my-service'), - ); - mockRegisterTaskDef.mockReturnValue({ - taskDefinition: { - taskDefinitionArn: 'arn:aws:ecs:region:account:task-definition/my-task-def:3', - }, + forceNewDeployment: true, + }); + } }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('should call registerTaskDefinition with certain properties not lowercased', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { TaskDef: { Type: 'AWS::ECS::TaskDefinition', Properties: { Family: 'my-task-def', ContainerDefinitions: [ - { - Image: 'image2', - DockerLabels: { Label1: 'label1' }, - FirelensConfiguration: { - Options: { Name: 'cloudwatch' }, - }, - LogConfiguration: { - Options: { Option1: 'option1' }, - }, - }, + { Image: 'image1' }, ], Volumes: [ { @@ -437,46 +546,93 @@ test('should call registerTaskDefinition with certain properties not lowercased' }, }, }, - }, - }); + }); + setup.pushStackResourceSummaries( + setup.stackSummaryOf('Service', 'AWS::ECS::Service', + 'arn:aws:ecs:region:account:service/my-cluster/my-service'), + ); + mockRegisterTaskDef.mockReturnValue({ + taskDefinition: { + taskDefinitionArn: 'arn:aws:ecs:region:account:task-definition/my-task-def:3', + }, + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + TaskDef: { + Type: 'AWS::ECS::TaskDefinition', + Properties: { + Family: 'my-task-def', + ContainerDefinitions: [ + { + Image: 'image2', + DockerLabels: { Label1: 'label1' }, + FirelensConfiguration: { + Options: { Name: 'cloudwatch' }, + }, + LogConfiguration: { + Options: { Option1: 'option1' }, + }, + }, + ], + Volumes: [ + { + DockerVolumeConfiguration: { + DriverOpts: { Option1: 'option1' }, + Labels: { Label1: 'label1' }, + }, + }, + ], + }, + }, + Service: { + Type: 'AWS::ECS::Service', + Properties: { + TaskDefinition: { Ref: 'TaskDef' }, + }, + }, + }, + }, + }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockRegisterTaskDef).toBeCalledWith({ - family: 'my-task-def', - containerDefinitions: [ - { - image: 'image2', - dockerLabels: { Label1: 'label1' }, - firelensConfiguration: { - options: { - Name: 'cloudwatch', + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockRegisterTaskDef).toBeCalledWith({ + family: 'my-task-def', + containerDefinitions: [ + { + image: 'image2', + dockerLabels: { Label1: 'label1' }, + firelensConfiguration: { + options: { + Name: 'cloudwatch', + }, + }, + logConfiguration: { + options: { Option1: 'option1' }, }, }, - logConfiguration: { - options: { Option1: 'option1' }, - }, - }, - ], - volumes: [ - { - dockerVolumeConfiguration: { - driverOpts: { Option1: 'option1' }, - labels: { Label1: 'label1' }, + ], + volumes: [ + { + dockerVolumeConfiguration: { + driverOpts: { Option1: 'option1' }, + labels: { Label1: 'label1' }, + }, }, + ], + }); + expect(mockUpdateService).toBeCalledWith({ + service: 'arn:aws:ecs:region:account:service/my-cluster/my-service', + cluster: 'my-cluster', + taskDefinition: 'arn:aws:ecs:region:account:task-definition/my-task-def:3', + deploymentConfiguration: { + minimumHealthyPercent: 0, }, - ], - }); - expect(mockUpdateService).toBeCalledWith({ - service: 'arn:aws:ecs:region:account:service/my-cluster/my-service', - cluster: 'my-cluster', - taskDefinition: 'arn:aws:ecs:region:account:task-definition/my-task-def:3', - deploymentConfiguration: { - minimumHealthyPercent: 0, - }, - forceNewDeployment: true, + forceNewDeployment: true, + }); }); }); diff --git a/packages/aws-cdk/test/api/hotswap/hotswap-deployments.test.ts b/packages/aws-cdk/test/api/hotswap/hotswap-deployments.test.ts index cb2e2a77e8f65..f97df8facd1c7 100644 --- a/packages/aws-cdk/test/api/hotswap/hotswap-deployments.test.ts +++ b/packages/aws-cdk/test/api/hotswap/hotswap-deployments.test.ts @@ -1,5 +1,6 @@ import { Lambda, StepFunctions } from 'aws-sdk'; import { CfnEvaluationException } from '../../../lib/api/evaluate-cloudformation-template'; +import { HotswapMode } from '../../../lib/api/hotswap/common'; import * as setup from './hotswap-test-setup'; let hotswapMockSdkProvider: setup.HotswapMockSdkProvider; @@ -19,202 +20,212 @@ beforeEach(() => { hotswapMockSdkProvider.stubGetEndpointSuffix(mockGetEndpointSuffix); }); -test('returns a deployStackResult with noOp=true when it receives an empty set of changes', async () => { - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(setup.cdkStackArtifactOf()); +describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hotswapMode) => { + test('returns a deployStackResult with noOp=true when it receives an empty set of changes', async () => { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, setup.cdkStackArtifactOf()); - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(deployStackResult?.noOp).toBeTruthy(); - expect(deployStackResult?.stackArn).toEqual(setup.STACK_ID); -}); - -test('A change to only a non-hotswappable resource results in a full deployment', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - SomethingElse: { - Type: 'AWS::CloudFormation::SomethingElse', - Properties: { - Prop: 'old-value', - }, - }, - }, + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(deployStackResult?.noOp).toBeTruthy(); + expect(deployStackResult?.stackArn).toEqual(setup.STACK_ID); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('A change to only a non-hotswappable resource results in a full deployment for HOTSWAP and a noOp for HOTSWAP_ONLY', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { SomethingElse: { Type: 'AWS::CloudFormation::SomethingElse', Properties: { - Prop: 'new-value', + Prop: 'old-value', }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); - expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); -}); - -test('A change to both a hotswappable resource and a non-hotswappable resource results in a full deployment', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + SomethingElse: { + Type: 'AWS::CloudFormation::SomethingElse', + Properties: { + Prop: 'new-value', + }, }, - FunctionName: 'my-function', - }, - Metadata: { - 'aws:asset:path': 'old-path', - }, - }, - SomethingElse: { - Type: 'AWS::CloudFormation::SomethingElse', - Properties: { - Prop: 'old-value', }, }, - }, + }); + + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); + expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(deployStackResult?.noOp).toEqual(true); + expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); + expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); + } }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('A change to both a hotswappable resource and a non-hotswappable resource results in a full deployment for HOTSWAP and a noOp for HOTSWAP_ONLY', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', - S3Key: 'new-key', + S3Key: 'current-key', }, FunctionName: 'my-function', }, Metadata: { - 'aws:asset:path': 'new-path', + 'aws:asset:path': 'old-path', }, }, SomethingElse: { Type: 'AWS::CloudFormation::SomethingElse', Properties: { - Prop: 'new-value', + Prop: 'old-value', }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); - expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); -}); - -test('changes only to CDK::Metadata result in a noOp', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - MetaData: { - Type: 'AWS::CDK::Metadata', - Properties: { - Prop: 'old-value', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, + }, + SomethingElse: { + Type: 'AWS::CloudFormation::SomethingElse', + Properties: { + Prop: 'new-value', + }, + }, }, }, - }, + }); + + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); + expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'my-function', + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }); + expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); + } }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('changes only to CDK::Metadata result in a noOp', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { MetaData: { Type: 'AWS::CDK::Metadata', Properties: { - Prop: 'new-value', + Prop: 'old-value', }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(deployStackResult?.noOp).toEqual(true); - expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); - expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); -}); - -test('resource deletions require full deployments', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Machine: { - Type: 'AWS::StepFunctions::StateMachine', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + MetaData: { + Type: 'AWS::CDK::Metadata', + Properties: { + Prop: 'new-value', + }, + }, + }, }, - }, - }); - const cdkStackArtifact = setup.cdkStackArtifactOf(); + }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); - expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); -}); + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(deployStackResult?.noOp).toEqual(true); + expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); + expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); + }); -test('can correctly reference AWS::Partition in hotswappable changes', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', - }, - FunctionName: { - 'Fn::Join': [ - '', - [ - { Ref: 'AWS::Partition' }, - '-', - 'my-function', - ], - ], - }, - }, - Metadata: { - 'aws:asset:path': 'new-path', + test('resource deletions require full deployments for HOTSWAP and a noOp for HOTSWAP_ONLY', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ + Resources: { + Machine: { + Type: 'AWS::StepFunctions::StateMachine', }, }, - }, + }); + const cdkStackArtifact = setup.cdkStackArtifactOf(); + + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); + expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(deployStackResult?.noOp).toEqual(true); + expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); + expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); + } }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('can correctly reference AWS::Partition in hotswappable changes', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', - S3Key: 'new-key', + S3Key: 'current-key', }, FunctionName: { 'Fn::Join': [ @@ -232,56 +243,58 @@ test('can correctly reference AWS::Partition in hotswappable changes', async () }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ - FunctionName: 'aws-my-function', - S3Bucket: 'current-bucket', - S3Key: 'new-key', - }); -}); - -test('can correctly reference AWS::URLSuffix in hotswappable changes', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', - }, - FunctionName: { - 'Fn::Join': ['', [ - 'my-function-', - { Ref: 'AWS::URLSuffix' }, - '-', - { Ref: 'AWS::URLSuffix' }, - ]], + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }, + FunctionName: { + 'Fn::Join': [ + '', + [ + { Ref: 'AWS::Partition' }, + '-', + 'my-function', + ], + ], + }, + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, }, }, - Metadata: { - 'aws:asset:path': 'old-path', - }, }, - }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'aws-my-function', + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('can correctly reference AWS::URLSuffix in hotswappable changes', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', - S3Key: 'new-key', + S3Key: 'current-key', }, FunctionName: { 'Fn::Join': ['', [ @@ -293,169 +306,182 @@ test('can correctly reference AWS::URLSuffix in hotswappable changes', async () }, }, Metadata: { - 'aws:asset:path': 'new-path', + 'aws:asset:path': 'old-path', }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ - FunctionName: 'my-function-amazonaws.com-amazonaws.com', - S3Bucket: 'current-bucket', - S3Key: 'new-key', - }); - expect(mockGetEndpointSuffix).toHaveBeenCalledTimes(1); - - // the User-Agent is set correctly - expect(hotswapMockSdkProvider.mockSdkProvider.sdk.appendCustomUserAgent) - .toHaveBeenCalledWith('cdk-hotswap/success-lambda-function'); - expect(hotswapMockSdkProvider.mockSdkProvider.sdk.removeCustomUserAgent) - .toHaveBeenCalledWith('cdk-hotswap/success-lambda-function'); -}); - -test('changing the type of a deployed resource always results in a full deployment', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - SharedLogicalId: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'new-key', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }, + FunctionName: { + 'Fn::Join': ['', [ + 'my-function-', + { Ref: 'AWS::URLSuffix' }, + '-', + { Ref: 'AWS::URLSuffix' }, + ]], + }, + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, }, - FunctionName: 'my-function', }, }, - }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'my-function-amazonaws.com-amazonaws.com', + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }); + expect(mockGetEndpointSuffix).toHaveBeenCalledTimes(1); + + // the User-Agent is set correctly + expect(hotswapMockSdkProvider.mockSdkProvider.sdk.appendCustomUserAgent) + .toHaveBeenCalledWith('cdk-hotswap/success-lambda'); + expect(hotswapMockSdkProvider.mockSdkProvider.sdk.removeCustomUserAgent) + .toHaveBeenCalledWith('cdk-hotswap/success-lambda'); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('changing the type of a deployed resource always results in a full deployment for HOTSWAP and a noOp for HOTSWAP_ONLY', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { SharedLogicalId: { - Type: 'AWS::StepFunctions::StateMachine', + Type: 'AWS::Lambda::Function', Properties: { - DefinitionString: '{ Prop: "new-value" }', - StateMachineName: 'my-machine', + Code: { + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }, + FunctionName: 'my-function', }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); - expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); -}); - -test('A change to both a hotswappable resource and a stack output results in a full deployment', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + SharedLogicalId: { + Type: 'AWS::StepFunctions::StateMachine', + Properties: { + DefinitionString: '{ Prop: "new-value" }', + StateMachineName: 'my-machine', + }, }, - FunctionName: 'my-function', - }, - Metadata: { - 'aws:asset:path': 'old-path', }, }, - }, - Outputs: { - SomeOutput: { - Value: 'old-value', - }, - }, + }); + + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); + expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(deployStackResult?.noOp).toEqual(true); + expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); + expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); + } }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('A change to both a hotswappable resource and a stack output results in a full deployment for HOTSWAP and a hotswap deployment for HOTSWAP_ONLY', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', - S3Key: 'new-key', + S3Key: 'current-key', }, FunctionName: 'my-function', }, Metadata: { - 'aws:asset:path': 'new-path', + 'aws:asset:path': 'old-path', }, }, }, Outputs: { SomeOutput: { - Value: 'new-value', + Value: 'old-value', }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); - expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); -}); - -test('Multiple CfnEvaluationException will not cause unhandled rejections', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Func1: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', - }, - Environment: { - key: 'old', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, }, - FunctionName: 'my-function', - }, - Metadata: { - 'aws:asset:path': 'old-path', }, - }, - Func2: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', - }, - Environment: { - key: 'old', + Outputs: { + SomeOutput: { + Value: 'new-value', }, - FunctionName: 'my-function', - }, - Metadata: { - 'aws:asset:path': 'old-path', }, }, - }, + }); + + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); + expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'my-function', + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }); + expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); + } }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('Multiple CfnEvaluationException will not cause unhandled rejections', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Func1: { Type: 'AWS::Lambda::Function', @@ -465,12 +491,12 @@ test('Multiple CfnEvaluationException will not cause unhandled rejections', asyn S3Key: 'current-key', }, Environment: { - key: { Ref: 'ErrorResource' }, + key: 'old', }, FunctionName: 'my-function', }, Metadata: { - 'aws:asset:path': 'new-path', + 'aws:asset:path': 'old-path', }, }, Func2: { @@ -481,23 +507,126 @@ test('Multiple CfnEvaluationException will not cause unhandled rejections', asyn S3Key: 'current-key', }, Environment: { - key: { Ref: 'ErrorResource' }, + key: 'old', }, FunctionName: 'my-function', }, Metadata: { - 'aws:asset:path': 'new-path', + 'aws:asset:path': 'old-path', }, }, }, - }, - }); + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func1: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', + }, + Environment: { + key: { Ref: 'ErrorResource' }, + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, + }, + Func2: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', + }, + Environment: { + key: { Ref: 'ErrorResource' }, + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, + }, + }, + }, + }); - // WHEN - const deployStackResult = hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + // WHEN + const deployStackResult = hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN - await expect(deployStackResult).rejects.toThrowError(CfnEvaluationException); - expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); - expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); + // THEN + await expect(deployStackResult).rejects.toThrowError(CfnEvaluationException); + expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); + expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); + }); + + test('deleting a resource and making a hotswappable change results in full deployments for HOTSWAP and a hotswap deployment for HOTSWAP_ONLY', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ + Resources: { + Machine: { + Type: 'AWS::StepFunctions::StateMachine', + }, + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, + }, + }, + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, + }, + }, + }, + }); + + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); + expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'my-function', + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }); + } + }); }); diff --git a/packages/aws-cdk/test/api/hotswap/hotswap-test-setup.ts b/packages/aws-cdk/test/api/hotswap/hotswap-test-setup.ts index a693564fae056..00591e156d3bc 100644 --- a/packages/aws-cdk/test/api/hotswap/hotswap-test-setup.ts +++ b/packages/aws-cdk/test/api/hotswap/hotswap-test-setup.ts @@ -5,6 +5,7 @@ import * as lambda from 'aws-sdk/clients/lambda'; import * as stepfunctions from 'aws-sdk/clients/stepfunctions'; import { DeployStackResult } from '../../../lib/api'; import * as deployments from '../../../lib/api/hotswap-deployments'; +import { HotswapMode } from '../../../lib/api/hotswap/common'; import { CloudFormationStack, Template } from '../../../lib/api/util/cloudformation'; import { testStack, TestStackArtifact } from '../../util'; import { MockSdkProvider, SyncHandlerSubsetOf } from '../../util/mock-sdk'; @@ -171,9 +172,10 @@ export class HotswapMockSdkProvider { } public tryHotswapDeployment( + hotswapMode: HotswapMode, stackArtifact: cxapi.CloudFormationStackArtifact, assetParams: { [key: string]: string } = {}, ): Promise { - return deployments.tryHotswapDeployment(this.mockSdkProvider, assetParams, currentCfnStack, stackArtifact); + return deployments.tryHotswapDeployment(this.mockSdkProvider, assetParams, currentCfnStack, stackArtifact, hotswapMode); } } diff --git a/packages/aws-cdk/test/api/hotswap/lambda-functions-docker-hotswap-deployments.test.ts b/packages/aws-cdk/test/api/hotswap/lambda-functions-docker-hotswap-deployments.test.ts index 9685b3ef1c0b4..c0784099bc401 100644 --- a/packages/aws-cdk/test/api/hotswap/lambda-functions-docker-hotswap-deployments.test.ts +++ b/packages/aws-cdk/test/api/hotswap/lambda-functions-docker-hotswap-deployments.test.ts @@ -1,4 +1,5 @@ import { Lambda } from 'aws-sdk'; +import { HotswapMode } from '../../../lib/api/hotswap/common'; import * as setup from './hotswap-test-setup'; let mockUpdateLambdaCode: (params: Lambda.Types.UpdateFunctionCodeRequest) => Lambda.Types.FunctionConfiguration; @@ -28,100 +29,102 @@ beforeEach(() => { }); }); -test('calls the updateLambdaCode() API when it receives only a code difference in a Lambda function', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - ImageUri: 'current-image', - }, - FunctionName: 'my-function', - }, - Metadata: { - 'aws:asset:path': 'old-path', - }, - }, - }, - }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { +describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hotswapMode) => { + test('calls the updateLambdaCode() API when it receives only a code difference in a Lambda function', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { - ImageUri: 'new-image', + ImageUri: 'current-image', }, FunctionName: 'my-function', }, Metadata: { - 'aws:asset:path': 'new-path', + 'aws:asset:path': 'old-path', }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ - FunctionName: 'my-function', - ImageUri: 'new-image', - }); -}); - -test('calls the getFunction() API with a delay of 5', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - ImageUri: 'current-image', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + ImageUri: 'new-image', + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, }, - FunctionName: 'my-function', - }, - Metadata: { - 'aws:asset:path': 'old-path', }, }, - }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'my-function', + ImageUri: 'new-image', + }); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('calls the getFunction() API with a delay of 5', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { - ImageUri: 'new-image', + ImageUri: 'current-image', }, FunctionName: 'my-function', }, Metadata: { - 'aws:asset:path': 'new-path', + 'aws:asset:path': 'old-path', }, }, }, - }, - }); + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + ImageUri: 'new-image', + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, + }, + }, + }, + }); - // WHEN - await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + // WHEN + await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN - expect(mockMakeRequest).toHaveBeenCalledWith('getFunction', { FunctionName: 'my-function' }); - expect(hotswapMockSdkProvider.getLambdaApiWaiters()).toEqual(expect.objectContaining({ - updateFunctionPropertiesToFinish: expect.objectContaining({ - name: 'UpdateFunctionPropertiesToFinish', - delay: 5, - }), - })); + // THEN + expect(mockMakeRequest).toHaveBeenCalledWith('getFunction', { FunctionName: 'my-function' }); + expect(hotswapMockSdkProvider.getLambdaApiWaiters()).toEqual(expect.objectContaining({ + updateFunctionPropertiesToFinish: expect.objectContaining({ + name: 'UpdateFunctionPropertiesToFinish', + delay: 5, + }), + })); + }); }); diff --git a/packages/aws-cdk/test/api/hotswap/lambda-functions-hotswap-deployments.test.ts b/packages/aws-cdk/test/api/hotswap/lambda-functions-hotswap-deployments.test.ts index 8f26c154dfbcb..15056d956497e 100644 --- a/packages/aws-cdk/test/api/hotswap/lambda-functions-hotswap-deployments.test.ts +++ b/packages/aws-cdk/test/api/hotswap/lambda-functions-hotswap-deployments.test.ts @@ -1,12 +1,11 @@ import { Lambda } from 'aws-sdk'; +import { HotswapMode } from '../../../lib/api/hotswap/common'; import * as setup from './hotswap-test-setup'; let mockUpdateLambdaCode: (params: Lambda.Types.UpdateFunctionCodeRequest) => Lambda.Types.FunctionConfiguration; let mockUpdateLambdaConfiguration: ( params: Lambda.Types.UpdateFunctionConfigurationRequest ) => Lambda.Types.FunctionConfiguration; -let mockTagResource: (params: Lambda.Types.TagResourceRequest) => {}; -let mockUntagResource: (params: Lambda.Types.UntagResourceRequest) => {}; let mockMakeRequest: (operation: string, params: any) => AWS.Request; let hotswapMockSdkProvider: setup.HotswapMockSdkProvider; @@ -14,8 +13,6 @@ beforeEach(() => { hotswapMockSdkProvider = setup.setupHotswapTests(); mockUpdateLambdaCode = jest.fn().mockReturnValue({}); mockUpdateLambdaConfiguration = jest.fn().mockReturnValue({}); - mockTagResource = jest.fn(); - mockUntagResource = jest.fn(); mockMakeRequest = jest.fn().mockReturnValue({ promise: () => Promise.resolve({}), response: {}, @@ -24,197 +21,95 @@ beforeEach(() => { hotswapMockSdkProvider.stubLambda({ updateFunctionCode: mockUpdateLambdaCode, updateFunctionConfiguration: mockUpdateLambdaConfiguration, - tagResource: mockTagResource, - untagResource: mockUntagResource, }, { makeRequest: mockMakeRequest, }); }); -test('returns undefined when a new Lambda function is added to the Stack', async () => { - // GIVEN - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { - Resources: { - Func: { - Type: 'AWS::Lambda::Function', +describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hotswapMode) => { + test('returns undefined when a new Lambda function is added to the Stack', async () => { + // GIVEN + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + }, }, }, - }, - }); + }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN - expect(deployStackResult).toBeUndefined(); -}); + // THEN + expect(deployStackResult).toBeUndefined(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); -test('calls the updateLambdaCode() API when it receives only a code difference in a Lambda function', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', - }, - FunctionName: 'my-function', - }, - Metadata: { - 'aws:asset:path': 'old-path', - }, - }, - }, + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(deployStackResult?.noOp).toEqual(true); + expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); + } }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('calls the updateLambdaCode() API when it receives only a code difference in a Lambda function', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', - S3Key: 'new-key', + S3Key: 'current-key', }, FunctionName: 'my-function', }, Metadata: { - 'aws:asset:path': 'new-path', + 'aws:asset:path': 'old-path', }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ - FunctionName: 'my-function', - S3Bucket: 'current-bucket', - S3Key: 'new-key', - }); -}); - -test('calls the tagResource() API when it receives only a tag difference in a Lambda function', async () => { - // GIVEN - const currentTemplate = { - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', - }, - FunctionName: 'my-function', - Tags: [ - { - Key: 'to-be-deleted', - Value: 'a-value', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }, + FunctionName: 'my-function', }, - { - Key: 'to-be-changed', - Value: 'current-tag-value', + Metadata: { + 'aws:asset:path': 'new-path', }, - ], - }, - Metadata: { - 'aws:asset:path': 'old-path', - }, - }, - }, - }; - - setup.setCurrentCfnStackTemplate(currentTemplate); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', - }, - FunctionName: 'my-function', - Tags: [ - { - Key: 'to-be-changed', - Value: 'new-tag-value', - }, - { - Key: 'to-be-added', - Value: 'added-tag-value', - }, - ], - }, - Metadata: { - 'aws:asset:path': 'old-path', }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + }); - // THEN - expect(deployStackResult).not.toBeUndefined(); - - expect(mockUntagResource).toHaveBeenCalledWith({ - Resource: 'arn:aws:lambda:here:123456789012:function:my-function', - TagKeys: ['to-be-deleted'], - }); + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - expect(mockTagResource).toHaveBeenCalledWith({ - Resource: 'arn:aws:lambda:here:123456789012:function:my-function', - Tags: { - 'to-be-changed': 'new-tag-value', - 'to-be-added': 'added-tag-value', - }, + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'my-function', + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }); }); - expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); -}); - -test("correctly evaluates the function's name when it references a different resource from the template", async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Bucket: { - Type: 'AWS::S3::Bucket', - }, - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', - }, - FunctionName: { - 'Fn::Join': ['-', [ - 'lambda', - { Ref: 'Bucket' }, - 'function', - ]], - }, - }, - Metadata: { - 'aws:asset:path': 'old-path', - }, - }, - }, - }); - setup.pushStackResourceSummaries(setup.stackSummaryOf('Bucket', 'AWS::S3::Bucket', 'mybucket')); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + test("correctly evaluates the function's name when it references a different resource from the template", async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Bucket: { Type: 'AWS::S3::Bucket', @@ -224,7 +119,7 @@ test("correctly evaluates the function's name when it references a different res Properties: { Code: { S3Bucket: 'current-bucket', - S3Key: 'new-key', + S3Key: 'current-key', }, FunctionName: { 'Fn::Join': ['-', [ @@ -239,47 +134,52 @@ test("correctly evaluates the function's name when it references a different res }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ - FunctionName: 'lambda-mybucket-function', - S3Bucket: 'current-bucket', - S3Key: 'new-key', - }); -}); - -test("correctly falls back to taking the function's name from the current stack if it can't evaluate it in the template", async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Parameters: { - Param1: { Type: 'String' }, - AssetBucketParam: { Type: 'String' }, - }, - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: { Ref: 'AssetBucketParam' }, - S3Key: 'current-key', + }); + setup.pushStackResourceSummaries(setup.stackSummaryOf('Bucket', 'AWS::S3::Bucket', 'mybucket')); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Bucket: { + Type: 'AWS::S3::Bucket', + }, + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }, + FunctionName: { + 'Fn::Join': ['-', [ + 'lambda', + { Ref: 'Bucket' }, + 'function', + ]], + }, + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, }, - FunctionName: { Ref: 'Param1' }, - }, - Metadata: { - 'aws:asset:path': 'old-path', }, }, - }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'lambda-mybucket-function', + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }); }); - setup.pushStackResourceSummaries(setup.stackSummaryOf('Func', 'AWS::Lambda::Function', 'my-function')); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test("correctly falls back to taking the function's name from the current stack if it can't evaluate it in the template", async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Parameters: { Param1: { Type: 'String' }, AssetBucketParam: { Type: 'String' }, @@ -290,54 +190,56 @@ test("correctly falls back to taking the function's name from the current stack Properties: { Code: { S3Bucket: { Ref: 'AssetBucketParam' }, - S3Key: 'new-key', + S3Key: 'current-key', }, FunctionName: { Ref: 'Param1' }, }, Metadata: { - 'aws:asset:path': 'new-path', + 'aws:asset:path': 'old-path', }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact, { AssetBucketParam: 'asset-bucket' }); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ - FunctionName: 'my-function', - S3Bucket: 'asset-bucket', - S3Key: 'new-key', - }); -}); - -test("will not perform a hotswap deployment if it cannot find a Ref target (outside the function's name)", async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Parameters: { - Param1: { Type: 'String' }, - }, - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: { 'Fn::Sub': '${Param1}' }, - S3Key: 'current-key', - }, + }); + setup.pushStackResourceSummaries(setup.stackSummaryOf('Func', 'AWS::Lambda::Function', 'my-function')); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Parameters: { + Param1: { Type: 'String' }, + AssetBucketParam: { Type: 'String' }, }, - Metadata: { - 'aws:asset:path': 'old-path', + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: { Ref: 'AssetBucketParam' }, + S3Key: 'new-key', + }, + FunctionName: { Ref: 'Param1' }, + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, + }, }, }, - }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact, { AssetBucketParam: 'asset-bucket' }); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'my-function', + S3Bucket: 'asset-bucket', + S3Key: 'new-key', + }); }); - setup.pushStackResourceSummaries(setup.stackSummaryOf('Func', 'AWS::Lambda::Function', 'my-func')); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test("will not perform a hotswap deployment if it cannot find a Ref target (outside the function's name)", async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Parameters: { Param1: { Type: 'String' }, }, @@ -347,50 +249,47 @@ test("will not perform a hotswap deployment if it cannot find a Ref target (outs Properties: { Code: { S3Bucket: { 'Fn::Sub': '${Param1}' }, - S3Key: 'new-key', + S3Key: 'current-key', }, }, Metadata: { - 'aws:asset:path': 'new-path', + 'aws:asset:path': 'old-path', }, }, }, - }, - }); - - // THEN - await expect(() => - hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact), - ).rejects.toThrow(/Parameter or resource 'Param1' could not be found for evaluation/); -}); - -test("will not perform a hotswap deployment if it doesn't know how to handle a specific attribute (outside the function's name)", async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Bucket: { - Type: 'AWS::S3::Bucket', - }, - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: { 'Fn::GetAtt': ['Bucket', 'UnknownAttribute'] }, - S3Key: 'current-key', - }, + }); + setup.pushStackResourceSummaries(setup.stackSummaryOf('Func', 'AWS::Lambda::Function', 'my-func')); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Parameters: { + Param1: { Type: 'String' }, }, - Metadata: { - 'aws:asset:path': 'old-path', + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: { 'Fn::Sub': '${Param1}' }, + S3Key: 'new-key', + }, + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, + }, }, }, - }, + }); + + // THEN + await expect(() => + hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact), + ).rejects.toThrow(/Parameter or resource 'Param1' could not be found for evaluation/); }); - setup.pushStackResourceSummaries( - setup.stackSummaryOf('Func', 'AWS::Lambda::Function', 'my-func'), - setup.stackSummaryOf('Bucket', 'AWS::S3::Bucket', 'my-bucket'), - ); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test("will not perform a hotswap deployment if it doesn't know how to handle a specific attribute (outside the function's name)", async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Bucket: { Type: 'AWS::S3::Bucket', @@ -400,50 +299,57 @@ test("will not perform a hotswap deployment if it doesn't know how to handle a s Properties: { Code: { S3Bucket: { 'Fn::GetAtt': ['Bucket', 'UnknownAttribute'] }, - S3Key: 'new-key', + S3Key: 'current-key', }, }, Metadata: { - 'aws:asset:path': 'new-path', + 'aws:asset:path': 'old-path', }, }, }, - }, - }); - - // THEN - await expect(() => - hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact), - ).rejects.toThrow("We don't support the 'UnknownAttribute' attribute of the 'AWS::S3::Bucket' resource. This is a CDK limitation. Please report it at https://github.com/aws/aws-cdk/issues/new/choose"); -}); - -test('calls the updateLambdaCode() API when it receives a code difference in a Lambda function with no name', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', + }); + setup.pushStackResourceSummaries( + setup.stackSummaryOf('Func', 'AWS::Lambda::Function', 'my-func'), + setup.stackSummaryOf('Bucket', 'AWS::S3::Bucket', 'my-bucket'), + ); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Bucket: { + Type: 'AWS::S3::Bucket', + }, + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: { 'Fn::GetAtt': ['Bucket', 'UnknownAttribute'] }, + S3Key: 'new-key', + }, + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, }, - }, - Metadata: { - 'aws:asset:path': 'current-path', }, }, - }, + }); + + // THEN + await expect(() => + hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact), + ).rejects.toThrow("We don't support the 'UnknownAttribute' attribute of the 'AWS::S3::Bucket' resource. This is a CDK limitation. Please report it at https://github.com/aws/aws-cdk/issues/new/choose"); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('calls the updateLambdaCode() API when it receives a code difference in a Lambda function with no name', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', - S3Key: 'new-key', + S3Key: 'current-key', }, }, Metadata: { @@ -451,40 +357,42 @@ test('calls the updateLambdaCode() API when it receives a code difference in a L }, }, }, - }, - }); - - // WHEN - setup.pushStackResourceSummaries(setup.stackSummaryOf('Func', 'AWS::Lambda::Function', 'mock-function-resource-id')); - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ - FunctionName: 'mock-function-resource-id', - S3Bucket: 'current-bucket', - S3Key: 'new-key', - }); -}); - -test('does not call the updateLambdaCode() API when it receives a change that is not a code difference in a Lambda function', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }, + }, + Metadata: { + 'aws:asset:path': 'current-path', + }, }, - PackageType: 'Zip', }, }, - }, + }); + + // WHEN + setup.pushStackResourceSummaries(setup.stackSummaryOf('Func', 'AWS::Lambda::Function', 'mock-function-resource-id')); + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'mock-function-resource-id', + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('does not call the updateLambdaCode() API when it receives a change that is not a code difference in a Lambda function', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Func: { Type: 'AWS::Lambda::Function', @@ -493,48 +401,114 @@ test('does not call the updateLambdaCode() API when it receives a change that is S3Bucket: 'current-bucket', S3Key: 'current-key', }, - PackageType: 'Image', + PackageType: 'Zip', }, }, }, - }, - }); + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', + }, + PackageType: 'Image', + }, + }, + }, + }, + }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); -}); + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(deployStackResult?.noOp).toEqual(true); + expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); + } + }); -test('does not call the updateLambdaCode() API when a resource with type that is not AWS::Lambda::Function but has the same properties is changed', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Func: { - Type: 'AWS::NotLambda::NotAFunction', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', + test(`when it receives a non-hotswappable change that includes a code difference in a Lambda function, it does not call the updateLambdaCode() + API in CLASSIC mode but does in HOTSWAP_ONLY mode`, + async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', + }, + FunctionName: 'my-function', + PackageType: 'Zip', }, }, - Metadata: { - 'aws:asset:path': 'old-path', + }, + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }, + FunctionName: 'my-function', + PackageType: 'Image', + }, + }, }, }, - }, + }); + + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'my-function', + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }); + } }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('does not call the updateLambdaCode() API when a resource with type that is not AWS::Lambda::Function but has the same properties is changed', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Func: { Type: 'AWS::NotLambda::NotAFunction', Properties: { Code: { S3Bucket: 'current-bucket', - S3Key: 'new-key', + S3Key: 'current-key', }, }, Metadata: { @@ -542,214 +516,217 @@ test('does not call the updateLambdaCode() API when a resource with type that is }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); -}); - -test('calls getFunction() after function code is updated with delay 1', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::NotLambda::NotAFunction', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }, + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, }, - FunctionName: 'my-function', - }, - Metadata: { - 'aws:asset:path': 'old-path', }, }, - }, + }); + + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(deployStackResult?.noOp).toEqual(true); + expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); + } }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('calls getFunction() after function code is updated with delay 1', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', - S3Key: 'new-key', + S3Key: 'current-key', }, FunctionName: 'my-function', }, Metadata: { - 'aws:asset:path': 'new-path', + 'aws:asset:path': 'old-path', }, }, }, - }, - }); - - // WHEN - await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(mockMakeRequest).toHaveBeenCalledWith('getFunction', { FunctionName: 'my-function' }); - expect(hotswapMockSdkProvider.getLambdaApiWaiters()).toEqual(expect.objectContaining({ - updateFunctionPropertiesToFinish: expect.objectContaining({ - name: 'UpdateFunctionPropertiesToFinish', - delay: 1, - }), - })); -}); - -test('calls getFunction() after function code is updated and VpcId is empty string with delay 1', async () => { - // GIVEN - mockUpdateLambdaCode = jest.fn().mockReturnValue({ - VpcConfig: { - VpcId: '', - }, - }); - hotswapMockSdkProvider.stubLambda({ - updateFunctionCode: mockUpdateLambdaCode, - tagResource: mockTagResource, - untagResource: mockUntagResource, - }); - setup.setCurrentCfnStackTemplate({ - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, }, - FunctionName: 'my-function', - }, - Metadata: { - 'aws:asset:path': 'old-path', }, }, - }, + }); + + // WHEN + await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(mockMakeRequest).toHaveBeenCalledWith('getFunction', { FunctionName: 'my-function' }); + expect(hotswapMockSdkProvider.getLambdaApiWaiters()).toEqual(expect.objectContaining({ + updateFunctionPropertiesToFinish: expect.objectContaining({ + name: 'UpdateFunctionPropertiesToFinish', + delay: 1, + }), + })); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('calls getFunction() after function code is updated and VpcId is empty string with delay 1', async () => { + // GIVEN + mockUpdateLambdaCode = jest.fn().mockReturnValue({ + VpcConfig: { + VpcId: '', + }, + }); + hotswapMockSdkProvider.stubLambda({ + updateFunctionCode: mockUpdateLambdaCode, + }); + setup.setCurrentCfnStackTemplate({ Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', - S3Key: 'new-key', + S3Key: 'current-key', }, FunctionName: 'my-function', }, Metadata: { - 'aws:asset:path': 'new-path', + 'aws:asset:path': 'old-path', }, }, }, - }, - }); - - // WHEN - await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(hotswapMockSdkProvider.getLambdaApiWaiters()).toEqual(expect.objectContaining({ - updateFunctionPropertiesToFinish: expect.objectContaining({ - name: 'UpdateFunctionPropertiesToFinish', - delay: 1, - }), - })); -}); - -test('calls getFunction() after function code is updated on a VPC function with delay 5', async () => { - // GIVEN - mockUpdateLambdaCode = jest.fn().mockReturnValue({ - VpcConfig: { - VpcId: 'abc', - }, - }); - hotswapMockSdkProvider.stubLambda({ - updateFunctionCode: mockUpdateLambdaCode, - tagResource: mockTagResource, - untagResource: mockUntagResource, - }); - setup.setCurrentCfnStackTemplate({ - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, }, - FunctionName: 'my-function', - }, - Metadata: { - 'aws:asset:path': 'old-path', }, }, - }, + }); + + // WHEN + await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(hotswapMockSdkProvider.getLambdaApiWaiters()).toEqual(expect.objectContaining({ + updateFunctionPropertiesToFinish: expect.objectContaining({ + name: 'UpdateFunctionPropertiesToFinish', + delay: 1, + }), + })); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('calls getFunction() after function code is updated on a VPC function with delay 5', async () => { + // GIVEN + mockUpdateLambdaCode = jest.fn().mockReturnValue({ + VpcConfig: { + VpcId: 'abc', + }, + }); + hotswapMockSdkProvider.stubLambda({ + updateFunctionCode: mockUpdateLambdaCode, + }); + setup.setCurrentCfnStackTemplate({ Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', - S3Key: 'new-key', + S3Key: 'current-key', }, FunctionName: 'my-function', }, Metadata: { - 'aws:asset:path': 'new-path', + 'aws:asset:path': 'old-path', }, }, }, - }, - }); - - // WHEN - await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(hotswapMockSdkProvider.getLambdaApiWaiters()).toEqual(expect.objectContaining({ - updateFunctionPropertiesToFinish: expect.objectContaining({ - name: 'UpdateFunctionPropertiesToFinish', - delay: 5, - }), - })); -}); - - -test('calls the updateLambdaConfiguration() API when it receives difference in Description field of a Lambda function', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 's3-bucket', - S3Key: 's3-key', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, }, - FunctionName: 'my-function', - Description: 'Old Description', - }, - Metadata: { - 'aws:asset:path': 'asset-path', }, }, - }, + }); + + // WHEN + await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(hotswapMockSdkProvider.getLambdaApiWaiters()).toEqual(expect.objectContaining({ + updateFunctionPropertiesToFinish: expect.objectContaining({ + name: 'UpdateFunctionPropertiesToFinish', + delay: 5, + }), + })); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('calls the updateLambdaConfiguration() API when it receives difference in Description field of a Lambda function', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Func: { Type: 'AWS::Lambda::Function', @@ -759,54 +736,49 @@ test('calls the updateLambdaConfiguration() API when it receives difference in D S3Key: 's3-key', }, FunctionName: 'my-function', - Description: 'New Description', + Description: 'Old Description', }, Metadata: { 'aws:asset:path': 'asset-path', }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateLambdaConfiguration).toHaveBeenCalledWith({ - FunctionName: 'my-function', - Description: 'New Description', - }); -}); - -test('calls the updateLambdaConfiguration() API when it receives difference in Environment field of a Lambda function', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 's3-bucket', - S3Key: 's3-key', - }, - FunctionName: 'my-function', - Environment: { - Variables: { - Key1: 'Value1', - Key2: 'Value2', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 's3-bucket', + S3Key: 's3-key', + }, + FunctionName: 'my-function', + Description: 'New Description', + }, + Metadata: { + 'aws:asset:path': 'asset-path', }, }, }, - Metadata: { - 'aws:asset:path': 'asset-path', - }, }, - }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaConfiguration).toHaveBeenCalledWith({ + FunctionName: 'my-function', + Description: 'New Description', + }); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('calls the updateLambdaConfiguration() API when it receives difference in Environment field of a Lambda function', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Func: { Type: 'AWS::Lambda::Function', @@ -820,7 +792,6 @@ test('calls the updateLambdaConfiguration() API when it receives difference in E Variables: { Key1: 'Value1', Key2: 'Value2', - NewKey: 'NewValue', }, }, }, @@ -829,137 +800,111 @@ test('calls the updateLambdaConfiguration() API when it receives difference in E }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateLambdaConfiguration).toHaveBeenCalledWith({ - FunctionName: 'my-function', - Environment: { - Variables: { - Key1: 'Value1', - Key2: 'Value2', - NewKey: 'NewValue', - }, - }, - }); -}); - -test('calls both updateLambdaCode() and updateLambdaConfiguration() API when it receives both code and configuration change', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 's3-bucket', + S3Key: 's3-key', + }, + FunctionName: 'my-function', + Environment: { + Variables: { + Key1: 'Value1', + Key2: 'Value2', + NewKey: 'NewValue', + }, + }, + }, + Metadata: { + 'aws:asset:path': 'asset-path', + }, }, - FunctionName: 'my-function', - Description: 'Old Description', }, - Metadata: { - 'aws:asset:path': 'asset-path', + }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaConfiguration).toHaveBeenCalledWith({ + FunctionName: 'my-function', + Environment: { + Variables: { + Key1: 'Value1', + Key2: 'Value2', + NewKey: 'NewValue', }, }, - }, + }); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('calls both updateLambdaCode() and updateLambdaConfiguration() API when it receives both code and configuration change', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { - S3Bucket: 'new-bucket', - S3Key: 'new-key', + S3Bucket: 'current-bucket', + S3Key: 'current-key', }, FunctionName: 'my-function', - Description: 'New Description', + Description: 'Old Description', }, Metadata: { 'aws:asset:path': 'asset-path', }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateLambdaConfiguration).toHaveBeenCalledWith({ - FunctionName: 'my-function', - Description: 'New Description', - }); - expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ - FunctionName: 'my-function', - S3Bucket: 'new-bucket', - S3Key: 'new-key', - }); -}); - -test('Lambda hotswap works properly with changes of environment variables, description, tags with tokens', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - EventBus: { - Type: 'AWS::Events::EventBus', - Properties: { - Name: 'my-event-bus', - }, - }, - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 's3-bucket', - S3Key: 's3-key', - }, - FunctionName: 'my-function', - Environment: { - Variables: { - token: { 'Fn::GetAtt': ['EventBus', 'Arn'] }, - literal: 'oldValue', - }, - }, - Description: { - 'Fn::Join': ['', [ - 'oldValue', - { 'Fn::GetAtt': ['EventBus', 'Arn'] }, - ]], - }, - Tags: [ - { - Key: 'token', - Value: { 'Fn::GetAtt': ['EventBus', 'Arn'] }, + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'new-bucket', + S3Key: 'new-key', + }, + FunctionName: 'my-function', + Description: 'New Description', }, - { - Key: 'literal', - Value: 'oldValue', + Metadata: { + 'aws:asset:path': 'asset-path', }, - ], - }, - Metadata: { - 'aws:asset:path': 'asset-path', + }, }, }, - }, - }); + }); - setup.pushStackResourceSummaries( - setup.stackSummaryOf('EventBus', 'AWS::Events::EventBus', 'my-event-bus'), - ); + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaConfiguration).toHaveBeenCalledWith({ + FunctionName: 'my-function', + Description: 'New Description', + }); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'my-function', + S3Bucket: 'new-bucket', + S3Key: 'new-key', + }); + }); + + test('Lambda hotswap works properly with changes of environment variables and description with tokens', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { EventBus: { Type: 'AWS::Events::EventBus', @@ -978,54 +923,124 @@ test('Lambda hotswap works properly with changes of environment variables, descr Environment: { Variables: { token: { 'Fn::GetAtt': ['EventBus', 'Arn'] }, - literal: 'newValue', + literal: 'oldValue', }, }, Description: { 'Fn::Join': ['', [ - 'newValue', + 'oldValue', { 'Fn::GetAtt': ['EventBus', 'Arn'] }, ]], }, - Tags: [ - { - Key: 'token', - Value: { 'Fn::GetAtt': ['EventBus', 'Arn'] }, - }, - { - Key: 'literal', - Value: 'newValue', - }, - ], }, Metadata: { 'aws:asset:path': 'asset-path', }, }, }, - }, - }); + }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + setup.pushStackResourceSummaries( + setup.stackSummaryOf('EventBus', 'AWS::Events::EventBus', 'my-event-bus'), + ); - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateLambdaConfiguration).toHaveBeenCalledWith({ - FunctionName: 'my-function', - Environment: { - Variables: { - token: 'arn:aws:events:here:123456789012:event-bus/my-event-bus', - literal: 'newValue', + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + EventBus: { + Type: 'AWS::Events::EventBus', + Properties: { + Name: 'my-event-bus', + }, + }, + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 's3-bucket', + S3Key: 's3-key', + }, + FunctionName: 'my-function', + Environment: { + Variables: { + token: { 'Fn::GetAtt': ['EventBus', 'Arn'] }, + literal: 'newValue', + }, + }, + Description: { + 'Fn::Join': ['', [ + 'newValue', + { 'Fn::GetAtt': ['EventBus', 'Arn'] }, + ]], + }, + }, + Metadata: { + 'aws:asset:path': 'asset-path', + }, + }, + }, }, - }, - Description: 'newValuearn:aws:events:here:123456789012:event-bus/my-event-bus', + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaConfiguration).toHaveBeenCalledWith({ + FunctionName: 'my-function', + Environment: { + Variables: { + token: 'arn:aws:events:here:123456789012:event-bus/my-event-bus', + literal: 'newValue', + }, + }, + Description: 'newValuearn:aws:events:here:123456789012:event-bus/my-event-bus', + }); }); - expect(mockTagResource).toHaveBeenCalledWith({ - Resource: 'arn:aws:lambda:here:123456789012:function:my-function', - Tags: { - token: 'arn:aws:events:here:123456789012:event-bus/my-event-bus', - literal: 'newValue', - }, + + test('S3ObjectVersion is hotswappable', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Key: 'current-key', + S3ObjectVersion: 'current-obj', + }, + FunctionName: 'my-function', + }, + }, + }, + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Key: 'new-key', + S3ObjectVersion: 'new-obj', + }, + FunctionName: 'my-function', + }, + }, + }, + }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'my-function', + S3Key: 'new-key', + S3ObjectVersion: 'new-obj', + }); }); }); diff --git a/packages/aws-cdk/test/api/hotswap/lambda-functions-inline-hotswap-deployments.test.ts b/packages/aws-cdk/test/api/hotswap/lambda-functions-inline-hotswap-deployments.test.ts index 478fd80b538bf..861f930635cdf 100644 --- a/packages/aws-cdk/test/api/hotswap/lambda-functions-inline-hotswap-deployments.test.ts +++ b/packages/aws-cdk/test/api/hotswap/lambda-functions-inline-hotswap-deployments.test.ts @@ -1,4 +1,5 @@ import { Lambda } from 'aws-sdk'; +import { HotswapMode } from '../../../lib/api/hotswap/common'; import * as setup from './hotswap-test-setup'; let mockUpdateLambdaCode: (params: Lambda.Types.UpdateFunctionCodeRequest) => Lambda.Types.FunctionConfiguration; @@ -18,131 +19,133 @@ beforeEach(() => { }); }); -test('calls the updateLambdaCode() API when it receives only a code difference in a Lambda function (Inline Node.js code)', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - ZipFile: 'exports.handler = () => {return true}', - }, - Runtime: 'nodejs14.x', - FunctionName: 'my-function', - }, - }, - }, - }); - const newCode = 'exports.handler = () => {return false}'; - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { +describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('these tests do not depend on the hotswap type', (hotswapMode) => { + test('calls the updateLambdaCode() API when it receives only a code difference in a Lambda function (Inline Node.js code)', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { - ZipFile: newCode, + ZipFile: 'exports.handler = () => {return true}', }, Runtime: 'nodejs14.x', FunctionName: 'my-function', }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ - FunctionName: 'my-function', - ZipFile: expect.any(Buffer), - }); -}); - -test('calls the updateLambdaCode() API when it receives only a code difference in a Lambda function (Inline Python code)', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - ZipFile: 'def handler(event, context):\n return True', + }); + const newCode = 'exports.handler = () => {return false}'; + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + ZipFile: newCode, + }, + Runtime: 'nodejs14.x', + FunctionName: 'my-function', + }, }, - Runtime: 'python3.9', - FunctionName: 'my-function', }, }, - }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'my-function', + ZipFile: expect.any(Buffer), + }); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('calls the updateLambdaCode() API when it receives only a code difference in a Lambda function (Inline Python code)', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { - ZipFile: 'def handler(event, context):\n return False', + ZipFile: 'def handler(event, context):\n return True', }, Runtime: 'python3.9', FunctionName: 'my-function', }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ - FunctionName: 'my-function', - ZipFile: expect.any(Buffer), - }); -}); - -test('throw a CfnEvaluationException when it receives an unsupported function runtime', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - ZipFile: 'def handler(event:, context:) true end', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + ZipFile: 'def handler(event, context):\n return False', + }, + Runtime: 'python3.9', + FunctionName: 'my-function', + }, }, - Runtime: 'ruby2.7', - FunctionName: 'my-function', }, }, - }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'my-function', + ZipFile: expect.any(Buffer), + }); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('throw a CfnEvaluationException when it receives an unsupported function runtime', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { - ZipFile: 'def handler(event:, context:) false end', + ZipFile: 'def handler(event:, context:) true end', }, Runtime: 'ruby2.7', FunctionName: 'my-function', }, }, }, - }, - }); + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + ZipFile: 'def handler(event:, context:) false end', + }, + Runtime: 'ruby2.7', + FunctionName: 'my-function', + }, + }, + }, + }, + }); - // WHEN - const tryHotswap = hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + // WHEN + const tryHotswap = hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN - await expect(tryHotswap).rejects.toThrow('runtime ruby2.7 is unsupported, only node.js and python runtimes are currently supported.'); + // THEN + await expect(tryHotswap).rejects.toThrow('runtime ruby2.7 is unsupported, only node.js and python runtimes are currently supported.'); + }); }); diff --git a/packages/aws-cdk/test/api/hotswap/lambda-versions-aliases-hotswap-deployments.test.ts b/packages/aws-cdk/test/api/hotswap/lambda-versions-aliases-hotswap-deployments.test.ts index f937be7c5a5a0..92cc806a398cd 100644 --- a/packages/aws-cdk/test/api/hotswap/lambda-versions-aliases-hotswap-deployments.test.ts +++ b/packages/aws-cdk/test/api/hotswap/lambda-versions-aliases-hotswap-deployments.test.ts @@ -1,4 +1,5 @@ import { Lambda } from 'aws-sdk'; +import { HotswapMode } from '../../../lib/api/hotswap/common'; import * as setup from './hotswap-test-setup'; let mockUpdateLambdaCode: (params: Lambda.Types.UpdateFunctionCodeRequest) => Lambda.Types.FunctionConfiguration; @@ -19,37 +20,17 @@ beforeEach(() => { }); }); -test('hotswaps a Version if it points to a changed Function, even if it itself is unchanged', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', - }, - FunctionName: 'my-function', - }, - }, - Version: { - Type: 'AWS::Lambda::Version', - Properties: { - FunctionName: { Ref: 'Func' }, - }, - }, - }, - }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { +describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hotswapMode) => { + test('hotswaps a Version if it points to a changed Function, even if it itself is unchanged', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', - S3Key: 'new-key', + S3Key: 'current-key', }, FunctionName: 'my-function', }, @@ -61,118 +42,110 @@ test('hotswaps a Version if it points to a changed Function, even if it itself i }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockPublishVersion).toHaveBeenCalledWith({ - FunctionName: 'my-function', - }); -}); - -test('hotswaps a Version if it points to a changed Function, even if it itself is replaced', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }, + FunctionName: 'my-function', + }, + }, + Version: { + Type: 'AWS::Lambda::Version', + Properties: { + FunctionName: { Ref: 'Func' }, + }, }, - FunctionName: 'my-function', - }, - }, - Version1: { - Type: 'AWS::Lambda::Version', - Properties: { - FunctionName: { Ref: 'Func' }, }, }, - }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockPublishVersion).toHaveBeenCalledWith({ + FunctionName: 'my-function', + }); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('hotswaps a Version if it points to a changed Function, even if it itself is replaced', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', - S3Key: 'new-key', + S3Key: 'current-key', }, FunctionName: 'my-function', }, }, - Version2: { + Version1: { Type: 'AWS::Lambda::Version', Properties: { FunctionName: { Ref: 'Func' }, }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockPublishVersion).toHaveBeenCalledWith({ - FunctionName: 'my-function', - }); -}); - -test('hotswaps a Version and an Alias if the Function they point to changed', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }, + FunctionName: 'my-function', + }, + }, + Version2: { + Type: 'AWS::Lambda::Version', + Properties: { + FunctionName: { Ref: 'Func' }, + }, }, - FunctionName: 'my-function', - }, - }, - Version1: { - Type: 'AWS::Lambda::Version', - Properties: { - FunctionName: { Ref: 'Func' }, - }, - }, - Alias: { - Type: 'AWS::Lambda::Alias', - Properties: { - FunctionName: { Ref: 'Func' }, - FunctionVersion: { 'Fn::GetAtt': ['Version1', 'Version'] }, - Name: 'dev', }, }, - }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockPublishVersion).toHaveBeenCalledWith({ + FunctionName: 'my-function', + }); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('hotswaps a Version and an Alias if the Function they point to changed', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', - S3Key: 'new-key', + S3Key: 'current-key', }, FunctionName: 'my-function', }, }, - Version2: { + Version1: { Type: 'AWS::Lambda::Version', Properties: { FunctionName: { Ref: 'Func' }, @@ -182,25 +155,55 @@ test('hotswaps a Version and an Alias if the Function they point to changed', as Type: 'AWS::Lambda::Alias', Properties: { FunctionName: { Ref: 'Func' }, - FunctionVersion: { 'Fn::GetAtt': ['Version2', 'Version'] }, + FunctionVersion: { 'Fn::GetAtt': ['Version1', 'Version'] }, Name: 'dev', }, }, }, - }, - }); - mockPublishVersion.mockReturnValue({ - Version: 'v2', - }); + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }, + FunctionName: 'my-function', + }, + }, + Version2: { + Type: 'AWS::Lambda::Version', + Properties: { + FunctionName: { Ref: 'Func' }, + }, + }, + Alias: { + Type: 'AWS::Lambda::Alias', + Properties: { + FunctionName: { Ref: 'Func' }, + FunctionVersion: { 'Fn::GetAtt': ['Version2', 'Version'] }, + Name: 'dev', + }, + }, + }, + }, + }); + mockPublishVersion.mockReturnValue({ + Version: 'v2', + }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateAlias).toHaveBeenCalledWith({ - FunctionName: 'my-function', - FunctionVersion: 'v2', - Name: 'dev', + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateAlias).toHaveBeenCalledWith({ + FunctionName: 'my-function', + FunctionVersion: 'v2', + Name: 'dev', + }); }); }); diff --git a/packages/aws-cdk/test/api/hotswap/nested-stacks-hotswap.test.ts b/packages/aws-cdk/test/api/hotswap/nested-stacks-hotswap.test.ts index b01e94ae4715b..47841366baf1e 100644 --- a/packages/aws-cdk/test/api/hotswap/nested-stacks-hotswap.test.ts +++ b/packages/aws-cdk/test/api/hotswap/nested-stacks-hotswap.test.ts @@ -1,4 +1,5 @@ import { Lambda } from 'aws-sdk'; +import { HotswapMode } from '../../../lib/api/hotswap/common'; import { testStack } from '../../util'; import * as setup from './hotswap-test-setup'; @@ -6,969 +7,1033 @@ let mockUpdateLambdaCode: (params: Lambda.Types.UpdateFunctionCodeRequest) => La let mockPublishVersion: jest.Mock; let hotswapMockSdkProvider: setup.HotswapMockSdkProvider; -test('can hotswap a lambda function in a 1-level nested stack', async () => { - // GIVEN - hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('LambdaRoot'); - mockUpdateLambdaCode = jest.fn().mockReturnValue({}); - hotswapMockSdkProvider.stubLambda({ - updateFunctionCode: mockUpdateLambdaCode, - }); - - const rootStack = testStack({ - stackName: 'LambdaRoot', - template: { - Resources: { - NestedStack: { - Type: 'AWS::CloudFormation::Stack', - Properties: { - TemplateURL: 'https://www.magic-url.com', - }, - Metadata: { - 'aws:asset:path': 'one-lambda-stack.nested.template.json', +// TODO: more tests for parent vs child containing hotswappable changes +describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hotswapMode) => { + test('can hotswap a lambda function in a 1-level nested stack', async () => { + // GIVEN + hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('LambdaRoot'); + mockUpdateLambdaCode = jest.fn().mockReturnValue({}); + hotswapMockSdkProvider.stubLambda({ + updateFunctionCode: mockUpdateLambdaCode, + }); + + const rootStack = testStack({ + stackName: 'LambdaRoot', + template: { + Resources: { + NestedStack: { + Type: 'AWS::CloudFormation::Stack', + Properties: { + TemplateURL: 'https://www.magic-url.com', + }, + Metadata: { + 'aws:asset:path': 'one-lambda-stack.nested.template.json', + }, }, }, }, - }, - }); - - setup.addTemplateToCloudFormationLookupMock(rootStack); - setup.addTemplateToCloudFormationLookupMock(testStack({ - stackName: 'NestedStack', - template: { - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', + }); + + setup.addTemplateToCloudFormationLookupMock(rootStack); + setup.addTemplateToCloudFormationLookupMock(testStack({ + stackName: 'NestedStack', + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'old-path', }, - FunctionName: 'my-function', - }, - Metadata: { - 'aws:asset:path': 'old-path', }, }, }, - }, - })); - - setup.pushNestedStackResourceSummaries('LambdaRoot', - setup.stackSummaryOf('NestedStack', 'AWS::CloudFormation::Stack', - 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedStack/abcd', - ), - ); - - const cdkStackArtifact = testStack({ stackName: 'LambdaRoot', template: rootStack.template }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ - FunctionName: 'my-function', - S3Bucket: 'current-bucket', - S3Key: 'new-key', - }); -}); - -test('hotswappable changes do not override hotswappable changes in their ancestors', async () => { - // GIVEN - hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('TwoLevelLambdaRoot'); - mockUpdateLambdaCode = jest.fn().mockReturnValue({}); - hotswapMockSdkProvider.stubLambda({ - updateFunctionCode: mockUpdateLambdaCode, + })); + + setup.pushNestedStackResourceSummaries('LambdaRoot', + setup.stackSummaryOf('NestedStack', 'AWS::CloudFormation::Stack', + 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedStack/abcd', + ), + ); + + const cdkStackArtifact = testStack({ stackName: 'LambdaRoot', template: rootStack.template }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'my-function', + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }); }); - const rootStack = testStack({ - stackName: 'TwoLevelLambdaRoot', - template: { - Resources: { - ChildStack: { - Type: 'AWS::CloudFormation::Stack', - Properties: { - TemplateURL: 'https://www.magic-url.com', - }, - Metadata: { - 'aws:asset:path': 'one-lambda-one-stack-stack.nested.template.json', + test('hotswappable changes do not override hotswappable changes in their ancestors', async () => { + // GIVEN + hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('TwoLevelLambdaRoot'); + mockUpdateLambdaCode = jest.fn().mockReturnValue({}); + hotswapMockSdkProvider.stubLambda({ + updateFunctionCode: mockUpdateLambdaCode, + }); + + const rootStack = testStack({ + stackName: 'TwoLevelLambdaRoot', + template: { + Resources: { + ChildStack: { + Type: 'AWS::CloudFormation::Stack', + Properties: { + TemplateURL: 'https://www.magic-url.com', + }, + Metadata: { + 'aws:asset:path': 'one-lambda-one-stack-stack.nested.template.json', + }, }, }, }, - }, - }); - - setup.addTemplateToCloudFormationLookupMock(rootStack); - setup.addTemplateToCloudFormationLookupMock(testStack({ - stackName: 'ChildStack', - template: { - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', + }); + + setup.addTemplateToCloudFormationLookupMock(rootStack); + setup.addTemplateToCloudFormationLookupMock(testStack({ + stackName: 'ChildStack', + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', + }, + FunctionName: 'child-function', + }, + Metadata: { + 'aws:asset:path': 'old-path', }, - FunctionName: 'child-function', - }, - Metadata: { - 'aws:asset:path': 'old-path', - }, - }, - GrandChildStack: { - Type: 'AWS::CloudFormation::Stack', - Properties: { - TemplateURL: 'https://www.magic-url.com', }, - Metadata: { - 'aws:asset:path': 'one-lambda-stack.nested.template.json', + GrandChildStack: { + Type: 'AWS::CloudFormation::Stack', + Properties: { + TemplateURL: 'https://www.magic-url.com', + }, + Metadata: { + 'aws:asset:path': 'one-lambda-stack.nested.template.json', + }, }, }, }, - }, - })); - setup.addTemplateToCloudFormationLookupMock(testStack({ - stackName: 'GrandChildStack', - template: { - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', - }, - FunctionName: 'my-function', - }, - Metadata: { - 'aws:asset:path': 'old-path', + })); + setup.addTemplateToCloudFormationLookupMock(testStack({ + stackName: 'GrandChildStack', + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, }, }, }, - }, - })); - - setup.pushNestedStackResourceSummaries('TwoLevelLambdaRoot', - setup.stackSummaryOf('ChildStack', 'AWS::CloudFormation::Stack', - 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/ChildStack/abcd', - ), - ); - setup.pushNestedStackResourceSummaries('ChildStack', - setup.stackSummaryOf('GrandChildStack', 'AWS::CloudFormation::Stack', - 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/GrandChildStack/abcd', - ), - ); - - const cdkStackArtifact = testStack({ stackName: 'TwoLevelLambdaRoot', template: rootStack.template }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ - FunctionName: 'child-function', - S3Bucket: 'new-bucket', - S3Key: 'current-key', - }); - expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ - FunctionName: 'my-function', - S3Bucket: 'current-bucket', - S3Key: 'new-key', - }); -}); - -test('hotswappable changes in nested stacks do not override hotswappable changes in their parent stack', async () => { - // GIVEN - hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('SiblingLambdaRoot'); - mockUpdateLambdaCode = jest.fn().mockReturnValue({}); - hotswapMockSdkProvider.stubLambda({ - updateFunctionCode: mockUpdateLambdaCode, + })); + + setup.pushNestedStackResourceSummaries('TwoLevelLambdaRoot', + setup.stackSummaryOf('ChildStack', 'AWS::CloudFormation::Stack', + 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/ChildStack/abcd', + ), + ); + setup.pushNestedStackResourceSummaries('ChildStack', + setup.stackSummaryOf('GrandChildStack', 'AWS::CloudFormation::Stack', + 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/GrandChildStack/abcd', + ), + ); + + const cdkStackArtifact = testStack({ stackName: 'TwoLevelLambdaRoot', template: rootStack.template }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'child-function', + S3Bucket: 'new-bucket', + S3Key: 'current-key', + }); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'my-function', + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }); }); - const rootStack = testStack({ - stackName: 'SiblingLambdaRoot', - template: { - Resources: { - NestedStack: { - Type: 'AWS::CloudFormation::Stack', - Properties: { - TemplateURL: 'https://www.magic-url.com', - }, - Metadata: { - 'aws:asset:path': 'one-lambda-stack.nested.template.json', - }, - }, - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', + test('hotswappable changes in nested stacks do not override hotswappable changes in their parent stack', async () => { + // GIVEN + hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('SiblingLambdaRoot'); + mockUpdateLambdaCode = jest.fn().mockReturnValue({}); + hotswapMockSdkProvider.stubLambda({ + updateFunctionCode: mockUpdateLambdaCode, + }); + + const rootStack = testStack({ + stackName: 'SiblingLambdaRoot', + template: { + Resources: { + NestedStack: { + Type: 'AWS::CloudFormation::Stack', + Properties: { + TemplateURL: 'https://www.magic-url.com', + }, + Metadata: { + 'aws:asset:path': 'one-lambda-stack.nested.template.json', }, - FunctionName: 'root-function', }, - Metadata: { - 'aws:asset:path': 'old-path', + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', + }, + FunctionName: 'root-function', + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, }, }, }, - }, - }); - - setup.addTemplateToCloudFormationLookupMock(rootStack); - setup.addTemplateToCloudFormationLookupMock(testStack({ - stackName: 'NestedStack', - template: { - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', + }); + + setup.addTemplateToCloudFormationLookupMock(rootStack); + setup.addTemplateToCloudFormationLookupMock(testStack({ + stackName: 'NestedStack', + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'old-path', }, - FunctionName: 'my-function', - }, - Metadata: { - 'aws:asset:path': 'old-path', }, }, }, - }, - })); - - setup.pushNestedStackResourceSummaries('SiblingLambdaRoot', - setup.stackSummaryOf('NestedStack', 'AWS::CloudFormation::Stack', - 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedStack/abcd', - ), - ); - - rootStack.template.Resources.Func.Properties.Code.S3Bucket = 'new-bucket'; - const cdkStackArtifact = testStack({ stackName: 'SiblingLambdaRoot', template: rootStack.template }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ - FunctionName: 'root-function', - S3Bucket: 'new-bucket', - S3Key: 'current-key', - }); - expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ - FunctionName: 'my-function', - S3Bucket: 'current-bucket', - S3Key: 'new-key', + })); + + setup.pushNestedStackResourceSummaries('SiblingLambdaRoot', + setup.stackSummaryOf('NestedStack', 'AWS::CloudFormation::Stack', + 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedStack/abcd', + ), + ); + + rootStack.template.Resources.Func.Properties.Code.S3Bucket = 'new-bucket'; + const cdkStackArtifact = testStack({ stackName: 'SiblingLambdaRoot', template: rootStack.template }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'root-function', + S3Bucket: 'new-bucket', + S3Key: 'current-key', + }); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'my-function', + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }); }); -}); -test('non-hotswappable changes in nested stacks result in a full deployment, even if their parent contains a hotswappable change', async () => { - // GIVEN - hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('NonHotswappableRoot'); - mockUpdateLambdaCode = jest.fn().mockReturnValue({}); - hotswapMockSdkProvider.stubLambda({ - updateFunctionCode: mockUpdateLambdaCode, - }); - - const rootStack = testStack({ - stackName: 'NonHotswappableRoot', - template: { - Resources: { - NestedStack: { - Type: 'AWS::CloudFormation::Stack', - Properties: { - TemplateURL: 'https://www.magic-url.com', - }, - Metadata: { - 'aws:asset:path': 'one-lambda-stack.nested.template.json', - }, - }, - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', + test(`non-hotswappable changes in nested stacks result in a full deployment, even if their parent contains a hotswappable change in CLASSIC mode, + but perform a hotswap deployment in HOTSWAP_ONLY`, + async () => { + // GIVEN + hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('NonHotswappableRoot'); + mockUpdateLambdaCode = jest.fn().mockReturnValue({}); + hotswapMockSdkProvider.stubLambda({ + updateFunctionCode: mockUpdateLambdaCode, + }); + + const rootStack = testStack({ + stackName: 'NonHotswappableRoot', + template: { + Resources: { + NestedStack: { + Type: 'AWS::CloudFormation::Stack', + Properties: { + TemplateURL: 'https://www.magic-url.com', + }, + Metadata: { + 'aws:asset:path': 'one-lambda-stack.nested.template.json', }, - FunctionName: 'root-function', }, - Metadata: { - 'aws:asset:path': 'old-path', + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', + }, + FunctionName: 'root-function', + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, }, }, }, - }, - }); - - setup.addTemplateToCloudFormationLookupMock(rootStack); - setup.addTemplateToCloudFormationLookupMock(testStack({ - stackName: 'NestedStack', - template: { - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', + }); + + setup.addTemplateToCloudFormationLookupMock(rootStack); + setup.addTemplateToCloudFormationLookupMock(testStack({ + stackName: 'NestedStack', + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', + }, + PackageType: 'Image', + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'old-path', }, - PackageType: 'Image', - FunctionName: 'my-function', - }, - Metadata: { - 'aws:asset:path': 'old-path', }, }, }, - }, - })); - - setup.pushNestedStackResourceSummaries('NonHotswappableRoot', - setup.stackSummaryOf('NestedStack', 'AWS::CloudFormation::Stack', - 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedStack/abcd', - ), - ); - - rootStack.template.Resources.Func.Properties.Code.S3Bucket = 'new-bucket'; - const cdkStackArtifact = testStack({ stackName: 'NonHotswappableRoot', template: rootStack.template }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); -}); - -test('deleting a nested stack results in a full deployment, even if their parent contains a hotswappable change', async () => { - // GIVEN - hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('NestedStackDeletionRoot'); - mockUpdateLambdaCode = jest.fn().mockReturnValue({}); - hotswapMockSdkProvider.stubLambda({ - updateFunctionCode: mockUpdateLambdaCode, + })); + + setup.pushNestedStackResourceSummaries('NonHotswappableRoot', + setup.stackSummaryOf('NestedStack', 'AWS::CloudFormation::Stack', + 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedStack/abcd', + ), + ); + + rootStack.template.Resources.Func.Properties.Code.S3Bucket = 'new-bucket'; + const cdkStackArtifact = testStack({ stackName: 'NonHotswappableRoot', template: rootStack.template }); + + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); + + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'root-function', + S3Bucket: 'new-bucket', + S3Key: 'current-key', + }); + } }); - const rootStack = testStack({ - stackName: 'NestedStackDeletionRoot', - template: { - Resources: { - NestedStack: { - Type: 'AWS::CloudFormation::Stack', - Properties: { - TemplateURL: 'https://www.magic-url.com', - }, - Metadata: { - 'aws:asset:path': 'one-lambda-stack.nested.template.json', - }, - }, - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', + test(`deleting a nested stack results in a full deployment in CLASSIC mode, even if their parent contains a hotswappable change, + but results in a hotswap deployment in HOTSWAP_ONLY mode`, + async () => { + // GIVEN + hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('NestedStackDeletionRoot'); + mockUpdateLambdaCode = jest.fn().mockReturnValue({}); + hotswapMockSdkProvider.stubLambda({ + updateFunctionCode: mockUpdateLambdaCode, + }); + + const rootStack = testStack({ + stackName: 'NestedStackDeletionRoot', + template: { + Resources: { + NestedStack: { + Type: 'AWS::CloudFormation::Stack', + Properties: { + TemplateURL: 'https://www.magic-url.com', + }, + Metadata: { + 'aws:asset:path': 'one-lambda-stack.nested.template.json', }, - FunctionName: 'root-function', }, - Metadata: { - 'aws:asset:path': 'old-path', + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', + }, + FunctionName: 'root-function', + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, }, }, }, - }, - }); - - setup.addTemplateToCloudFormationLookupMock(rootStack); - setup.addTemplateToCloudFormationLookupMock(testStack({ - stackName: 'NestedStack', - template: { - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', + }); + + setup.addTemplateToCloudFormationLookupMock(rootStack); + setup.addTemplateToCloudFormationLookupMock(testStack({ + stackName: 'NestedStack', + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'old-path', }, - FunctionName: 'my-function', - }, - Metadata: { - 'aws:asset:path': 'old-path', }, }, }, - }, - })); - - setup.pushNestedStackResourceSummaries('NestedStackDeletionRoot', - setup.stackSummaryOf('NestedStack', 'AWS::CloudFormation::Stack', - 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedStack/abcd', - ), - ); - - rootStack.template.Resources.Func.Properties.Code.S3Bucket = 'new-bucket'; - delete rootStack.template.Resources.NestedStack; - const cdkStackArtifact = testStack({ stackName: 'NestedStackDeletionRoot', template: rootStack.template }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); -}); - -test('creating a nested stack results in a full deployment, even if their parent contains a hotswappable change', async () => { - // GIVEN - hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('NestedStackCreationRoot'); - mockUpdateLambdaCode = jest.fn().mockReturnValue({}); - hotswapMockSdkProvider.stubLambda({ - updateFunctionCode: mockUpdateLambdaCode, + })); + + setup.pushNestedStackResourceSummaries('NestedStackDeletionRoot', + setup.stackSummaryOf('NestedStack', 'AWS::CloudFormation::Stack', + 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedStack/abcd', + ), + ); + + rootStack.template.Resources.Func.Properties.Code.S3Bucket = 'new-bucket'; + delete rootStack.template.Resources.NestedStack; + const cdkStackArtifact = testStack({ stackName: 'NestedStackDeletionRoot', template: rootStack.template }); + + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'root-function', + S3Bucket: 'new-bucket', + S3Key: 'current-key', + }); + } }); - const rootStack = testStack({ - stackName: 'NestedStackCreationRoot', - template: { - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', + test(`creating a nested stack results in a full deployment in CLASSIC mode, even if their parent contains a hotswappable change, + but results in a hotswap deployment in HOTSWAP_ONLY mode`, + async () => { + // GIVEN + hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('NestedStackCreationRoot'); + mockUpdateLambdaCode = jest.fn().mockReturnValue({}); + hotswapMockSdkProvider.stubLambda({ + updateFunctionCode: mockUpdateLambdaCode, + }); + + const rootStack = testStack({ + stackName: 'NestedStackCreationRoot', + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', + }, + FunctionName: 'root-function', + }, + Metadata: { + 'aws:asset:path': 'old-path', }, - FunctionName: 'root-function', - }, - Metadata: { - 'aws:asset:path': 'old-path', }, }, }, - }, - }); + }); - setup.addTemplateToCloudFormationLookupMock(rootStack); - - rootStack.template.Resources.Func.Properties.Code.S3Bucket = 'new-bucket'; - rootStack.template.Resources.NestedStack = { - Type: 'AWS::CloudFormation::Stack', - Properties: { - TemplateURL: 'https://www.magic-url.com', - }, - Metadata: { - 'aws:asset:path': 'one-lambda-stack.nested.template.json', - }, - }; - const cdkStackArtifact = testStack({ stackName: 'NestedStackCreationRoot', template: rootStack.template }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); -}); + setup.addTemplateToCloudFormationLookupMock(rootStack); -test('attempting to hotswap a newly created nested stack with the same logical ID as a resource with a different type results in a full deployment', async () => { - // GIVEN - hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('NestedStackTypeChangeRoot'); - mockUpdateLambdaCode = jest.fn().mockReturnValue({}); - hotswapMockSdkProvider.stubLambda({ - updateFunctionCode: mockUpdateLambdaCode, + rootStack.template.Resources.Func.Properties.Code.S3Bucket = 'new-bucket'; + rootStack.template.Resources.NestedStack = { + Type: 'AWS::CloudFormation::Stack', + Properties: { + TemplateURL: 'https://www.magic-url.com', + }, + Metadata: { + 'aws:asset:path': 'one-lambda-stack.nested.template.json', + }, + }; + const cdkStackArtifact = testStack({ stackName: 'NestedStackCreationRoot', template: rootStack.template }); + + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); + + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'root-function', + S3Bucket: 'new-bucket', + S3Key: 'current-key', + }); + } }); - const rootStack = testStack({ - stackName: 'NestedStackTypeChangeRoot', - template: { - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', + test(`attempting to hotswap a newly created nested stack with the same logical ID as a resource with a different type results in a full deployment in CLASSIC mode + and a hotswap deployment in HOTSWAP_ONLY mode`, + async () => { + // GIVEN + hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('NestedStackTypeChangeRoot'); + mockUpdateLambdaCode = jest.fn().mockReturnValue({}); + hotswapMockSdkProvider.stubLambda({ + updateFunctionCode: mockUpdateLambdaCode, + }); + + const rootStack = testStack({ + stackName: 'NestedStackTypeChangeRoot', + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', + }, + FunctionName: 'root-function', }, - FunctionName: 'root-function', - }, - Metadata: { - 'aws:asset:path': 'old-path', - }, - }, - FutureNestedStack: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'new-key', + Metadata: { + 'aws:asset:path': 'old-path', }, - FunctionName: 'spooky-function', }, - Metadata: { - 'aws:asset:path': 'old-path', + FutureNestedStack: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }, + FunctionName: 'spooky-function', + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, }, }, }, - }, - }); - - setup.addTemplateToCloudFormationLookupMock(rootStack); - - rootStack.template.Resources.Func.Properties.Code.S3Bucket = 'new-bucket'; - rootStack.template.Resources.FutureNestedStack = { - Type: 'AWS::CloudFormation::Stack', - Properties: { - TemplateURL: 'https://www.magic-url.com', - }, - Metadata: { - 'aws:asset:path': 'one-lambda-stack.nested.template.json', - }, - }; - const cdkStackArtifact = testStack({ stackName: 'NestedStackTypeChangeRoot', template: rootStack.template }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); -}); - + }); -test('multi-sibling + 3-layer nested stack structure is hotswappable', async () => { - // GIVEN - hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('MultiLayerRoot'); - mockUpdateLambdaCode = jest.fn().mockReturnValue({}); - hotswapMockSdkProvider.stubLambda({ - updateFunctionCode: mockUpdateLambdaCode, - }); + setup.addTemplateToCloudFormationLookupMock(rootStack); - const lambdaFunctionResource = { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', + rootStack.template.Resources.Func.Properties.Code.S3Bucket = 'new-bucket'; + rootStack.template.Resources.FutureNestedStack = { + Type: 'AWS::CloudFormation::Stack', + Properties: { + TemplateURL: 'https://www.magic-url.com', }, - }, - Metadata: { - 'aws:asset:path': 'old-path', - }, - }; - - const rootStack = testStack({ - stackName: 'MultiLayerRoot', - template: { - Resources: { - ChildStack: { - Type: 'AWS::CloudFormation::Stack', - Properties: { - TemplateURL: 'https://www.magic-url.com', - }, - Metadata: { - 'aws:asset:path': 'one-unnamed-lambda-two-stacks-stack.nested.template.json', - }, - }, - Func: lambdaFunctionResource, + Metadata: { + 'aws:asset:path': 'one-lambda-stack.nested.template.json', }, - }, + }; + const cdkStackArtifact = testStack({ stackName: 'NestedStackTypeChangeRoot', template: rootStack.template }); + + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); + + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'root-function', + S3Bucket: 'new-bucket', + S3Key: 'current-key', + }); + } }); - setup.addTemplateToCloudFormationLookupMock(rootStack); - setup.addTemplateToCloudFormationLookupMock(testStack({ - stackName: 'ChildStack', - template: { - Resources: { - GrandChildStackA: { - Type: 'AWS::CloudFormation::Stack', - Properties: { - TemplateURL: 'https://www.magic-url.com', - }, - Metadata: { - 'aws:asset:path': 'one-unnamed-lambda-stack.nested.template.json', - }, - }, - GrandChildStackB: { - Type: 'AWS::CloudFormation::Stack', - Properties: { - TemplateURL: 'https://www.magic-url.com', - }, - Metadata: { - 'aws:asset:path': 'one-unnamed-lambda-stack.nested.template.json', - }, + test('multi-sibling + 3-layer nested stack structure is hotswappable', async () => { + // GIVEN + hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('MultiLayerRoot'); + mockUpdateLambdaCode = jest.fn().mockReturnValue({}); + hotswapMockSdkProvider.stubLambda({ + updateFunctionCode: mockUpdateLambdaCode, + }); + + const lambdaFunctionResource = { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', }, - Func: lambdaFunctionResource, }, - }, - })); - setup.addTemplateToCloudFormationLookupMock(testStack({ - stackName: 'GrandChildStackA', - template: { - Resources: { - Func: lambdaFunctionResource, + Metadata: { + 'aws:asset:path': 'old-path', }, - }, - })); - setup.addTemplateToCloudFormationLookupMock(testStack({ - stackName: 'GrandChildStackB', - template: { - Resources: { - Func: lambdaFunctionResource, + }; + + const rootStack = testStack({ + stackName: 'MultiLayerRoot', + template: { + Resources: { + ChildStack: { + Type: 'AWS::CloudFormation::Stack', + Properties: { + TemplateURL: 'https://www.magic-url.com', + }, + Metadata: { + 'aws:asset:path': 'one-unnamed-lambda-two-stacks-stack.nested.template.json', + }, + }, + Func: lambdaFunctionResource, + }, }, - }, - })); - - setup.pushNestedStackResourceSummaries('MultiLayerRoot', - setup.stackSummaryOf('ChildStack', 'AWS::CloudFormation::Stack', - 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/ChildStack/abcd', - ), - setup.stackSummaryOf('Func', 'AWS::Lambda::Function', 'root-function'), - ); - setup.pushNestedStackResourceSummaries('ChildStack', - setup.stackSummaryOf('GrandChildStackA', 'AWS::CloudFormation::Stack', - 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/GrandChildStackA/abcd', - ), - setup.stackSummaryOf('GrandChildStackB', 'AWS::CloudFormation::Stack', - 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/GrandChildStackB/abcd', - ), - setup.stackSummaryOf('Func', 'AWS::Lambda::Function', 'child-function'), - ); - setup.pushNestedStackResourceSummaries('GrandChildStackA', - setup.stackSummaryOf('Func', 'AWS::Lambda::Function', 'grandchild-A-function'), - ); - setup.pushNestedStackResourceSummaries('GrandChildStackB', - setup.stackSummaryOf('Func', 'AWS::Lambda::Function', 'grandchild-B-function'), - ); - - rootStack.template.Resources.Func.Properties.Code.S3Key = 'new-key'; - const cdkStackArtifact = testStack({ stackName: 'MultiLayerRoot', template: rootStack.template }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ - FunctionName: 'root-function', - S3Bucket: 'current-bucket', - S3Key: 'new-key', - }); - expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ - FunctionName: 'child-function', - S3Bucket: 'current-bucket', - S3Key: 'new-key', - }); - expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ - FunctionName: 'grandchild-A-function', - S3Bucket: 'current-bucket', - S3Key: 'new-key', - }); - expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ - FunctionName: 'grandchild-B-function', - S3Bucket: 'current-bucket', - S3Key: 'new-key', - }); -}); - -test('can hotswap a lambda function in a 1-level nested stack with asset parameters', async () => { - // GIVEN - hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('LambdaRoot'); - mockUpdateLambdaCode = jest.fn().mockReturnValue({}); - hotswapMockSdkProvider.stubLambda({ - updateFunctionCode: mockUpdateLambdaCode, - }); - - const rootStack = testStack({ - stackName: 'LambdaRoot', - template: { - Resources: { - NestedStack: { - Type: 'AWS::CloudFormation::Stack', - Properties: { - TemplateURL: 'https://www.magic-url.com', - Parameters: { - referencetoS3BucketParam: { - Ref: 'S3BucketParam', - }, - referencetoS3KeyParam: { - Ref: 'S3KeyParam', - }, + }); + + setup.addTemplateToCloudFormationLookupMock(rootStack); + setup.addTemplateToCloudFormationLookupMock(testStack({ + stackName: 'ChildStack', + template: { + Resources: { + GrandChildStackA: { + Type: 'AWS::CloudFormation::Stack', + Properties: { + TemplateURL: 'https://www.magic-url.com', + }, + Metadata: { + 'aws:asset:path': 'one-unnamed-lambda-stack.nested.template.json', }, }, - Metadata: { - 'aws:asset:path': 'one-lambda-stack-with-asset-parameters.nested.template.json', + GrandChildStackB: { + Type: 'AWS::CloudFormation::Stack', + Properties: { + TemplateURL: 'https://www.magic-url.com', + }, + Metadata: { + 'aws:asset:path': 'one-unnamed-lambda-stack.nested.template.json', + }, }, + Func: lambdaFunctionResource, }, }, - Parameters: { - S3BucketParam: { - Type: 'String', - Description: 'S3 bucket for asset', + })); + setup.addTemplateToCloudFormationLookupMock(testStack({ + stackName: 'GrandChildStackA', + template: { + Resources: { + Func: lambdaFunctionResource, }, - S3KeyParam: { - Type: 'String', - Description: 'S3 bucket for asset', + }, + })); + setup.addTemplateToCloudFormationLookupMock(testStack({ + stackName: 'GrandChildStackB', + template: { + Resources: { + Func: lambdaFunctionResource, }, }, - }, + })); + + setup.pushNestedStackResourceSummaries('MultiLayerRoot', + setup.stackSummaryOf('ChildStack', 'AWS::CloudFormation::Stack', + 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/ChildStack/abcd', + ), + setup.stackSummaryOf('Func', 'AWS::Lambda::Function', 'root-function'), + ); + setup.pushNestedStackResourceSummaries('ChildStack', + setup.stackSummaryOf('GrandChildStackA', 'AWS::CloudFormation::Stack', + 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/GrandChildStackA/abcd', + ), + setup.stackSummaryOf('GrandChildStackB', 'AWS::CloudFormation::Stack', + 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/GrandChildStackB/abcd', + ), + setup.stackSummaryOf('Func', 'AWS::Lambda::Function', 'child-function'), + ); + setup.pushNestedStackResourceSummaries('GrandChildStackA', + setup.stackSummaryOf('Func', 'AWS::Lambda::Function', 'grandchild-A-function'), + ); + setup.pushNestedStackResourceSummaries('GrandChildStackB', + setup.stackSummaryOf('Func', 'AWS::Lambda::Function', 'grandchild-B-function'), + ); + + rootStack.template.Resources.Func.Properties.Code.S3Key = 'new-key'; + const cdkStackArtifact = testStack({ stackName: 'MultiLayerRoot', template: rootStack.template }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'root-function', + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'child-function', + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'grandchild-A-function', + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'grandchild-B-function', + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }); }); - setup.addTemplateToCloudFormationLookupMock(rootStack); - setup.addTemplateToCloudFormationLookupMock(testStack({ - stackName: 'NestedStack', - template: { - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', + test('can hotswap a lambda function in a 1-level nested stack with asset parameters', async () => { + // GIVEN + hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('LambdaRoot'); + mockUpdateLambdaCode = jest.fn().mockReturnValue({}); + hotswapMockSdkProvider.stubLambda({ + updateFunctionCode: mockUpdateLambdaCode, + }); + + const rootStack = testStack({ + stackName: 'LambdaRoot', + template: { + Resources: { + NestedStack: { + Type: 'AWS::CloudFormation::Stack', + Properties: { + TemplateURL: 'https://www.magic-url.com', + Parameters: { + referencetoS3BucketParam: { + Ref: 'S3BucketParam', + }, + referencetoS3KeyParam: { + Ref: 'S3KeyParam', + }, + }, + }, + Metadata: { + 'aws:asset:path': 'one-lambda-stack-with-asset-parameters.nested.template.json', }, - FunctionName: 'my-function', }, - Metadata: { - 'aws:asset:path': 'old-path', + }, + Parameters: { + S3BucketParam: { + Type: 'String', + Description: 'S3 bucket for asset', + }, + S3KeyParam: { + Type: 'String', + Description: 'S3 bucket for asset', }, }, }, - }, - })); - - setup.pushNestedStackResourceSummaries('LambdaRoot', - setup.stackSummaryOf('NestedStack', 'AWS::CloudFormation::Stack', - 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedStack/abcd', - ), - ); - - const cdkStackArtifact = testStack({ stackName: 'LambdaRoot', template: rootStack.template }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact, { - S3BucketParam: 'bucket-param-value', - S3KeyParam: 'key-param-value', - }); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ - FunctionName: 'my-function', - S3Bucket: 'bucket-param-value', - S3Key: 'key-param-value', - }); -}); - -test('can hotswap a lambda function in a 2-level nested stack with asset parameters', async () => { - // GIVEN - hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('LambdaRoot'); - mockUpdateLambdaCode = jest.fn().mockReturnValue({}); - hotswapMockSdkProvider.stubLambda({ - updateFunctionCode: mockUpdateLambdaCode, - }); - - const rootStack = testStack({ - stackName: 'LambdaRoot', - template: { - Resources: { - ChildStack: { - Type: 'AWS::CloudFormation::Stack', - Properties: { - TemplateURL: 'https://www.magic-url.com', - Parameters: { - referencetoGrandChildS3BucketParam: { - Ref: 'GrandChildS3BucketParam', - }, - referencetoGrandChildS3KeyParam: { - Ref: 'GrandChildS3KeyParam', - }, - referencetoChildS3BucketParam: { - Ref: 'ChildS3BucketParam', - }, - referencetoChildS3KeyParam: { - Ref: 'ChildS3KeyParam', + }); + + setup.addTemplateToCloudFormationLookupMock(rootStack); + setup.addTemplateToCloudFormationLookupMock(testStack({ + stackName: 'NestedStack', + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'old-path', }, - }, - Metadata: { - 'aws:asset:path': 'one-lambda-one-stack-stack-with-asset-parameters.nested.template.json', }, }, }, - Parameters: { - GrandChildS3BucketParam: { - Type: 'String', - Description: 'S3 bucket for asset', - }, - GrandChildS3KeyParam: { - Type: 'String', - Description: 'S3 bucket for asset', - }, - ChildS3BucketParam: { - Type: 'String', - Description: 'S3 bucket for asset', - }, - ChildS3KeyParam: { - Type: 'String', - Description: 'S3 bucket for asset', - }, - }, - }, + })); + + setup.pushNestedStackResourceSummaries('LambdaRoot', + setup.stackSummaryOf('NestedStack', 'AWS::CloudFormation::Stack', + 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedStack/abcd', + ), + ); + + const cdkStackArtifact = testStack({ stackName: 'LambdaRoot', template: rootStack.template }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact, { + S3BucketParam: 'bucket-param-value', + S3KeyParam: 'key-param-value', + }); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'my-function', + S3Bucket: 'bucket-param-value', + S3Key: 'key-param-value', + }); }); - setup.addTemplateToCloudFormationLookupMock(rootStack); - setup.addTemplateToCloudFormationLookupMock(testStack({ - stackName: 'ChildStack', - template: { - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', + test('can hotswap a lambda function in a 2-level nested stack with asset parameters', async () => { + // GIVEN + hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('LambdaRoot'); + mockUpdateLambdaCode = jest.fn().mockReturnValue({}); + hotswapMockSdkProvider.stubLambda({ + updateFunctionCode: mockUpdateLambdaCode, + }); + + const rootStack = testStack({ + stackName: 'LambdaRoot', + template: { + Resources: { + ChildStack: { + Type: 'AWS::CloudFormation::Stack', + Properties: { + TemplateURL: 'https://www.magic-url.com', + Parameters: { + referencetoGrandChildS3BucketParam: { + Ref: 'GrandChildS3BucketParam', + }, + referencetoGrandChildS3KeyParam: { + Ref: 'GrandChildS3KeyParam', + }, + referencetoChildS3BucketParam: { + Ref: 'ChildS3BucketParam', + }, + referencetoChildS3KeyParam: { + Ref: 'ChildS3KeyParam', + }, + }, + }, + Metadata: { + 'aws:asset:path': 'one-lambda-one-stack-stack-with-asset-parameters.nested.template.json', }, - FunctionName: 'my-function', - }, - Metadata: { - 'aws:asset:path': 'old-path', }, }, - GrandChildStack: { - Type: 'AWS::CloudFormation::Stack', - Properties: { - TemplateURL: 'https://www.magic-url.com', + Parameters: { + GrandChildS3BucketParam: { + Type: 'String', + Description: 'S3 bucket for asset', + }, + GrandChildS3KeyParam: { + Type: 'String', + Description: 'S3 bucket for asset', }, - Metadata: { - 'aws:asset:path': 'one-lambda-stack-with-asset-parameters.nested.template.json', + ChildS3BucketParam: { + Type: 'String', + Description: 'S3 bucket for asset', + }, + ChildS3KeyParam: { + Type: 'String', + Description: 'S3 bucket for asset', }, }, }, - }, - })); - setup.addTemplateToCloudFormationLookupMock(testStack({ - stackName: 'GrandChildStack', - template: { - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', - }, - FunctionName: 'my-function', - }, - Metadata: { - 'aws:asset:path': 'old-path', + }); + + setup.addTemplateToCloudFormationLookupMock(rootStack); + setup.addTemplateToCloudFormationLookupMock(testStack({ + stackName: 'ChildStack', + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, + }, + GrandChildStack: { + Type: 'AWS::CloudFormation::Stack', + Properties: { + TemplateURL: 'https://www.magic-url.com', + }, + Metadata: { + 'aws:asset:path': 'one-lambda-stack-with-asset-parameters.nested.template.json', + }, }, }, }, - }, - })); - - - setup.pushNestedStackResourceSummaries('LambdaRoot', - setup.stackSummaryOf('ChildStack', 'AWS::CloudFormation::Stack', - 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/ChildStack/abcd', - ), - ); - - setup.pushNestedStackResourceSummaries('ChildStack', - setup.stackSummaryOf('GrandChildStack', 'AWS::CloudFormation::Stack', - 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/GrandChildStack/abcd', - ), - ); - const cdkStackArtifact = testStack({ stackName: 'LambdaRoot', template: rootStack.template }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact, { - GrandChildS3BucketParam: 'child-bucket-param-value', - GrandChildS3KeyParam: 'child-key-param-value', - ChildS3BucketParam: 'bucket-param-value', - ChildS3KeyParam: 'key-param-value', - }); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ - FunctionName: 'my-function', - S3Bucket: 'bucket-param-value', - S3Key: 'key-param-value', - }); - expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ - FunctionName: 'my-function', - S3Bucket: 'child-bucket-param-value', - S3Key: 'child-key-param-value', - }); -}); - -test('looking up objects in nested stacks works', async () => { - hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('LambdaRoot'); - mockUpdateLambdaCode = jest.fn().mockReturnValue({}); - mockPublishVersion = jest.fn(); - hotswapMockSdkProvider.stubLambda({ - updateFunctionCode: mockUpdateLambdaCode, - publishVersion: mockPublishVersion, - }); - - const rootStack = testStack({ - stackName: 'LambdaRoot', - template: { - Resources: { - NestedStack: { - Type: 'AWS::CloudFormation::Stack', - Properties: { - TemplateURL: 'https://www.magic-url.com', - }, - Metadata: { - 'aws:asset:path': 'one-lambda-version-stack.nested.template.json', + })); + setup.addTemplateToCloudFormationLookupMock(testStack({ + stackName: 'GrandChildStack', + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, }, }, }, - }, + })); + + setup.pushNestedStackResourceSummaries('LambdaRoot', + setup.stackSummaryOf('ChildStack', 'AWS::CloudFormation::Stack', + 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/ChildStack/abcd', + ), + ); + + setup.pushNestedStackResourceSummaries('ChildStack', + setup.stackSummaryOf('GrandChildStack', 'AWS::CloudFormation::Stack', + 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/GrandChildStack/abcd', + ), + ); + const cdkStackArtifact = testStack({ stackName: 'LambdaRoot', template: rootStack.template }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact, { + GrandChildS3BucketParam: 'child-bucket-param-value', + GrandChildS3KeyParam: 'child-key-param-value', + ChildS3BucketParam: 'bucket-param-value', + ChildS3KeyParam: 'key-param-value', + }); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'my-function', + S3Bucket: 'bucket-param-value', + S3Key: 'key-param-value', + }); + expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ + FunctionName: 'my-function', + S3Bucket: 'child-bucket-param-value', + S3Key: 'child-key-param-value', + }); }); - setup.addTemplateToCloudFormationLookupMock(rootStack); - setup.addTemplateToCloudFormationLookupMock(testStack({ - stackName: 'NestedStack', - template: { - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Code: { - S3Bucket: 'current-bucket', - S3Key: 'current-key', + test('looking up objects in nested stacks works', async () => { + hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('LambdaRoot'); + mockUpdateLambdaCode = jest.fn().mockReturnValue({}); + mockPublishVersion = jest.fn(); + hotswapMockSdkProvider.stubLambda({ + updateFunctionCode: mockUpdateLambdaCode, + publishVersion: mockPublishVersion, + }); + + const rootStack = testStack({ + stackName: 'LambdaRoot', + template: { + Resources: { + NestedStack: { + Type: 'AWS::CloudFormation::Stack', + Properties: { + TemplateURL: 'https://www.magic-url.com', + }, + Metadata: { + 'aws:asset:path': 'one-lambda-version-stack.nested.template.json', }, - FunctionName: 'my-function', }, }, - Version: { - Type: 'AWS::Lambda::Version', - Properties: { - FunctionName: { Ref: 'Func' }, + }, + }); + + setup.addTemplateToCloudFormationLookupMock(rootStack); + setup.addTemplateToCloudFormationLookupMock(testStack({ + stackName: 'NestedStack', + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', + }, + FunctionName: 'my-function', + }, + }, + Version: { + Type: 'AWS::Lambda::Version', + Properties: { + FunctionName: { Ref: 'Func' }, + }, }, }, }, - }, - })); + })); - setup.pushNestedStackResourceSummaries('LambdaRoot', - setup.stackSummaryOf('NestedStack', 'AWS::CloudFormation::Stack', - 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedStack/abcd', - ), - ); + setup.pushNestedStackResourceSummaries('LambdaRoot', + setup.stackSummaryOf('NestedStack', 'AWS::CloudFormation::Stack', + 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedStack/abcd', + ), + ); - const cdkStackArtifact = testStack({ stackName: 'LambdaRoot', template: rootStack.template }); + const cdkStackArtifact = testStack({ stackName: 'LambdaRoot', template: rootStack.template }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockPublishVersion).toHaveBeenCalledWith({ - FunctionName: 'my-function', + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockPublishVersion).toHaveBeenCalledWith({ + FunctionName: 'my-function', + }); }); }); diff --git a/packages/aws-cdk/test/api/hotswap/s3-bucket-hotswap-deployments.test.ts b/packages/aws-cdk/test/api/hotswap/s3-bucket-hotswap-deployments.test.ts index 3a015308fcadc..c9547c300443b 100644 --- a/packages/aws-cdk/test/api/hotswap/s3-bucket-hotswap-deployments.test.ts +++ b/packages/aws-cdk/test/api/hotswap/s3-bucket-hotswap-deployments.test.ts @@ -1,4 +1,5 @@ import { Lambda } from 'aws-sdk'; +import { HotswapMode } from '../../../lib/api/hotswap/common'; import { REQUIRED_BY_CFN } from '../../../lib/api/hotswap/s3-bucket-deployments'; import * as setup from './hotswap-test-setup'; @@ -20,133 +21,111 @@ beforeEach(() => { hotswapMockSdkProvider.setInvokeLambdaMock(mockLambdaInvoke); }); -test('calls the lambdaInvoke() API when it receives only an asset difference in an S3 bucket deployment and evaluates CFN expressions in S3 Deployment Properties', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - S3Deployment: { - Type: 'Custom::CDKBucketDeployment', - Properties: { - ServiceToken: 'a-lambda-arn', - SourceBucketNames: ['src-bucket'], - SourceObjectKeys: ['src-key-old'], - DestinationBucketName: 'dest-bucket', - DestinationBucketKeyPrefix: 'my-key/some-old-prefix', - }, - }, - }, - }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { +describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hotswapMode) => { + test('calls the lambdaInvoke() API when it receives only an asset difference in an S3 bucket deployment and evaluates CFN expressions in S3 Deployment Properties', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { S3Deployment: { Type: 'Custom::CDKBucketDeployment', Properties: { ServiceToken: 'a-lambda-arn', SourceBucketNames: ['src-bucket'], - SourceObjectKeys: { - 'Fn::Split': [ - '-', - 'key1-key2-key3', - ], - }, + SourceObjectKeys: ['src-key-old'], DestinationBucketName: 'dest-bucket', - DestinationBucketKeyPrefix: 'my-key/some-new-prefix', + DestinationBucketKeyPrefix: 'my-key/some-old-prefix', }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - - expect(mockLambdaInvoke).toHaveBeenCalledWith({ - FunctionName: 'a-lambda-arn', - Payload: JSON.stringify({ - ...payloadWithoutCustomResProps, - ResourceProperties: { - SourceBucketNames: ['src-bucket'], - SourceObjectKeys: ['key1', 'key2', 'key3'], - DestinationBucketName: 'dest-bucket', - DestinationBucketKeyPrefix: 'my-key/some-new-prefix', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + S3Deployment: { + Type: 'Custom::CDKBucketDeployment', + Properties: { + ServiceToken: 'a-lambda-arn', + SourceBucketNames: ['src-bucket'], + SourceObjectKeys: { + 'Fn::Split': [ + '-', + 'key1-key2-key3', + ], + }, + DestinationBucketName: 'dest-bucket', + DestinationBucketKeyPrefix: 'my-key/some-new-prefix', + }, + }, + }, }, - }), - }); -}); + }); -test('does not call the invoke() API when a resource with type that is not Custom::CDKBucketDeployment but has the same properties is changed', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - S3Deployment: { - Type: 'Custom::NotCDKBucketDeployment', - Properties: { - SourceObjectKeys: ['src-key-old'], + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + + expect(mockLambdaInvoke).toHaveBeenCalledWith({ + FunctionName: 'a-lambda-arn', + Payload: JSON.stringify({ + ...payloadWithoutCustomResProps, + ResourceProperties: { + SourceBucketNames: ['src-bucket'], + SourceObjectKeys: ['key1', 'key2', 'key3'], + DestinationBucketName: 'dest-bucket', + DestinationBucketKeyPrefix: 'my-key/some-new-prefix', }, - }, - }, + }), + }); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('does not call the invoke() API when a resource with type that is not Custom::CDKBucketDeployment but has the same properties is changed', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { S3Deployment: { Type: 'Custom::NotCDKBucketDeployment', Properties: { - SourceObjectKeys: ['src-key-new'], + SourceObjectKeys: ['src-key-old'], }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockLambdaInvoke).not.toHaveBeenCalled(); -}); - -test('does not call the invokeLambda() api if the updated Policy has no Roles', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Parameters: { - WebsiteBucketParamOld: { Type: 'String' }, - WebsiteBucketParamNew: { Type: 'String' }, - }, - Resources: { - S3Deployment: { - Type: 'Custom::CDKBucketDeployment', - Properties: { - ServiceToken: 'a-lambda-arn', - SourceObjectKeys: ['src-key-old'], - }, - }, - Policy: { - Type: 'AWS::IAM::Policy', - Properties: { - PolicyName: 'my-policy', - PolicyDocument: { - Statement: [ - { - Action: ['s3:GetObject*'], - Effect: 'Allow', - Resource: { - Ref: 'WebsiteBucketParamOld', - }, - }, - ], + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + S3Deployment: { + Type: 'Custom::NotCDKBucketDeployment', + Properties: { + SourceObjectKeys: ['src-key-new'], + }, }, }, }, - }, + }); + + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockLambdaInvoke).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(deployStackResult?.noOp).toEqual(true); + expect(mockLambdaInvoke).not.toHaveBeenCalled(); + } }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('does not call the invokeLambda() api if the updated Policy has no Roles in CLASSIC mode but does in HOTSWAP_ONLY mode', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Parameters: { WebsiteBucketParamOld: { Type: 'String' }, WebsiteBucketParamNew: { Type: 'String' }, @@ -156,7 +135,9 @@ test('does not call the invokeLambda() api if the updated Policy has no Roles', Type: 'Custom::CDKBucketDeployment', Properties: { ServiceToken: 'a-lambda-arn', - SourceObjectKeys: ['src-key-new'], + SourceObjectKeys: ['src-key-old'], + SourceBucketNames: ['src-bucket'], + DestinationBucketName: 'dest-bucket', }, }, Policy: { @@ -169,7 +150,7 @@ test('does not call the invokeLambda() api if the updated Policy has no Roles', Action: ['s3:GetObject*'], Effect: 'Allow', Resource: { - Ref: 'WebsiteBucketParamNew', + Ref: 'WebsiteBucketParamOld', }, }, ], @@ -177,353 +158,100 @@ test('does not call the invokeLambda() api if the updated Policy has no Roles', }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockLambdaInvoke).not.toHaveBeenCalled(); -}); - -test('throws an error when the serviceToken fails evaluation in the template', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - S3Deployment: { - Type: 'Custom::CDKBucketDeployment', - Properties: { - ServiceToken: { - Ref: 'BadLamba', - }, - SourceBucketNames: ['src-bucket'], - SourceObjectKeys: ['src-key-old'], - DestinationBucketName: 'dest-bucket', - }, - }, - }, - }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { - Resources: { - S3Deployment: { - Type: 'Custom::CDKBucketDeployment', - Properties: { - ServiceToken: { - Ref: 'BadLamba', - }, - SourceBucketNames: ['src-bucket'], - SourceObjectKeys: ['src-key-new'], - DestinationBucketName: 'dest-bucket', - }, - }, - }, - }, - }); - - // WHEN - await expect(() => - hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact), - ).rejects.toThrow(/Parameter or resource 'BadLamba' could not be found for evaluation/); - - expect(mockLambdaInvoke).not.toHaveBeenCalled(); -}); - -describe('old-style synthesis', () => { - const parameters = { - WebsiteBucketParamOld: { Type: 'String' }, - WebsiteBucketParamNew: { Type: 'String' }, - DifferentBucketParamNew: { Type: 'String' }, - }; - - const serviceRole = { - Type: 'AWS::IAM::Role', - Properties: { - AssumeRolePolicyDocument: { - Statement: [ - { - Action: 'sts:AssumeRole', - Effect: 'Allow', - Principal: { - Service: 'lambda.amazonaws.com', - }, - }, - ], - Version: '2012-10-17', - }, - }, - }; - - const policyOld = { - Type: 'AWS::IAM::Policy', - Properties: { - PolicyName: 'my-policy-old', - Roles: [ - { Ref: 'ServiceRole' }, - ], - PolicyDocument: { - Statement: [ - { - Action: ['s3:GetObject*'], - Effect: 'Allow', - Resource: { - Ref: 'WebsiteBucketParamOld', - }, - }, - ], - }, - }, - }; - - const policyNew = { - Type: 'AWS::IAM::Policy', - Properties: { - PolicyName: 'my-policy-new', - Roles: [ - { Ref: 'ServiceRole' }, - ], - PolicyDocument: { - Statement: [ - { - Action: ['s3:GetObject*'], - Effect: 'Allow', - Resource: { - Ref: 'WebsiteBucketParamNew', - }, - }, - ], - }, - }, - }; - - const policy2Old = { - Type: 'AWS::IAM::Policy', - Properties: { - PolicyName: 'my-policy-old-2', - Roles: [ - { Ref: 'ServiceRole' }, - ], - PolicyDocument: { - Statement: [ - { - Action: ['s3:GetObject*'], - Effect: 'Allow', - Resource: { - Ref: 'WebsiteBucketParamOld', - }, - }, - ], - }, - }, - }; - - const policy2New = { - Type: 'AWS::IAM::Policy', - Properties: { - PolicyName: 'my-policy-new-2', - Roles: [ - { Ref: 'ServiceRole2' }, - ], - PolicyDocument: { - Statement: [ - { - Action: ['s3:GetObject*'], - Effect: 'Allow', - Resource: { - Ref: 'DifferentBucketParamOld', - }, - }, - ], - }, - }, - }; - - const deploymentLambda = { - Type: 'AWS::Lambda::Function', - Role: { - 'Fn::GetAtt': [ - 'ServiceRole', - 'Arn', - ], - }, - }; - - const s3DeploymentOld = { - Type: 'Custom::CDKBucketDeployment', - Properties: { - ServiceToken: { - 'Fn::GetAtt': [ - 'S3DeploymentLambda', - 'Arn', - ], - }, - SourceBucketNames: ['src-bucket-old'], - SourceObjectKeys: ['src-key-old'], - DestinationBucketName: 'WebsiteBucketOld', - }, - }; - - const s3DeploymentNew = { - Type: 'Custom::CDKBucketDeployment', - Properties: { - ServiceToken: { - 'Fn::GetAtt': [ - 'S3DeploymentLambda', - 'Arn', - ], - }, - SourceBucketNames: ['src-bucket-new'], - SourceObjectKeys: ['src-key-new'], - DestinationBucketName: 'WebsiteBucketNew', - }, - }; - - beforeEach(() => { - setup.pushStackResourceSummaries( - setup.stackSummaryOf('S3DeploymentLambda', 'AWS::Lambda::Function', 'my-deployment-lambda'), - setup.stackSummaryOf('ServiceRole', 'AWS::IAM::Role', 'my-service-role'), - ); - }); - - test('calls the lambdaInvoke() API when it receives an asset difference in an S3 bucket deployment and an IAM Policy difference using old-style synthesis', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Parameters: parameters, - ServiceRole: serviceRole, - Policy: policyOld, - S3DeploymentLambda: deploymentLambda, - S3Deployment: s3DeploymentOld, - }, }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ template: { - Resources: { - Parameters: parameters, - ServiceRole: serviceRole, - Policy: policyNew, - S3DeploymentLambda: deploymentLambda, - S3Deployment: s3DeploymentNew, + Parameters: { + WebsiteBucketParamOld: { Type: 'String' }, + WebsiteBucketParamNew: { Type: 'String' }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact, { WebsiteBucketParamOld: 'WebsiteBucketOld', WebsiteBucketParamNew: 'WebsiteBucketNew' }); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockLambdaInvoke).toHaveBeenCalledWith({ - FunctionName: 'arn:aws:lambda:here:123456789012:function:my-deployment-lambda', - Payload: JSON.stringify({ - ...payloadWithoutCustomResProps, - ResourceProperties: { - SourceBucketNames: ['src-bucket-new'], - SourceObjectKeys: ['src-key-new'], - DestinationBucketName: 'WebsiteBucketNew', - }, - }), - }); - }); - - test('does not call the lambdaInvoke() API when the difference in the S3 deployment is referred to in one IAM policy change but not another', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - ServiceRole: serviceRole, - Policy1: policyOld, - Policy2: policy2Old, - S3DeploymentLambda: deploymentLambda, - S3Deployment: s3DeploymentOld, - }, - }); - - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { Resources: { - ServiceRole: serviceRole, - Policy1: policyNew, - Policy2: { + S3Deployment: { + Type: 'Custom::CDKBucketDeployment', Properties: { - Roles: [ - { Ref: 'ServiceRole' }, - 'different-role', - ], + ServiceToken: 'a-lambda-arn', + SourceObjectKeys: ['src-key-new'], + SourceBucketNames: ['src-bucket'], + DestinationBucketName: 'dest-bucket', + }, + }, + Policy: { + Type: 'AWS::IAM::Policy', + Properties: { + PolicyName: 'my-policy', PolicyDocument: { Statement: [ { Action: ['s3:GetObject*'], Effect: 'Allow', Resource: { - 'Fn::GetAtt': [ - 'DifferentBucketNew', - 'Arn', - ], + Ref: 'WebsiteBucketParamNew', }, }, ], }, }, }, - S3DeploymentLambda: deploymentLambda, - S3Deployment: s3DeploymentNew, }, }, }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockLambdaInvoke).not.toHaveBeenCalled(); + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockLambdaInvoke).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockLambdaInvoke).toHaveBeenCalledWith({ + FunctionName: 'a-lambda-arn', + Payload: JSON.stringify({ + ...payloadWithoutCustomResProps, + ResourceProperties: { + SourceObjectKeys: ['src-key-new'], + SourceBucketNames: ['src-bucket'], + DestinationBucketName: 'dest-bucket', + }, + }), + }); + } }); - test('does not call the lambdaInvoke() API when the lambda that references the role is referred to by something other than an S3 deployment', async () => { + test('throws an error when the serviceToken fails evaluation in the template', async () => { // GIVEN setup.setCurrentCfnStackTemplate({ Resources: { - ServiceRole: serviceRole, - Policy: policyOld, - S3DeploymentLambda: deploymentLambda, - S3Deployment: s3DeploymentOld, - Endpoint: { - Type: 'AWS::Lambda::Permission', + S3Deployment: { + Type: 'Custom::CDKBucketDeployment', Properties: { - Action: 'lambda:InvokeFunction', - FunctionName: { - 'Fn::GetAtt': [ - 'S3DeploymentLambda', - 'Arn', - ], + ServiceToken: { + Ref: 'BadLamba', }, - Principal: 'apigateway.amazonaws.com', + SourceBucketNames: ['src-bucket'], + SourceObjectKeys: ['src-key-old'], + DestinationBucketName: 'dest-bucket', }, }, }, }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ template: { Resources: { - ServiceRole: serviceRole, - Policy: policyNew, - S3DeploymentLambda: deploymentLambda, - S3Deployment: s3DeploymentNew, - Endpoint: { - Type: 'AWS::Lambda::Permission', + S3Deployment: { + Type: 'Custom::CDKBucketDeployment', Properties: { - Action: 'lambda:InvokeFunction', - FunctionName: { - 'Fn::GetAtt': [ - 'S3DeploymentLambda', - 'Arn', - ], + ServiceToken: { + Ref: 'BadLamba', }, - Principal: 'apigateway.amazonaws.com', + SourceBucketNames: ['src-bucket'], + SourceObjectKeys: ['src-key-new'], + DestinationBucketName: 'dest-bucket', }, }, }, @@ -531,191 +259,545 @@ describe('old-style synthesis', () => { }); // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + await expect(() => + hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact), + ).rejects.toThrow(/Parameter or resource 'BadLamba' could not be found for evaluation/); - // THEN - expect(deployStackResult).toBeUndefined(); expect(mockLambdaInvoke).not.toHaveBeenCalled(); }); - test('calls the lambdaInvoke() API when it receives an asset difference in two S3 bucket deployments and IAM Policy differences using old-style synthesis', async () => { - // GIVEN - const s3Deployment2Old = { + describe('old-style synthesis', () => { + const parameters = { + WebsiteBucketParamOld: { Type: 'String' }, + WebsiteBucketParamNew: { Type: 'String' }, + DifferentBucketParamNew: { Type: 'String' }, + }; + + const serviceRole = { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }, + }; + + const policyOld = { + Type: 'AWS::IAM::Policy', + Properties: { + PolicyName: 'my-policy-old', + Roles: [ + { Ref: 'ServiceRole' }, + ], + PolicyDocument: { + Statement: [ + { + Action: ['s3:GetObject*'], + Effect: 'Allow', + Resource: { + Ref: 'WebsiteBucketParamOld', + }, + }, + ], + }, + }, + }; + + const policyNew = { + Type: 'AWS::IAM::Policy', + Properties: { + PolicyName: 'my-policy-new', + Roles: [ + { Ref: 'ServiceRole' }, + ], + PolicyDocument: { + Statement: [ + { + Action: ['s3:GetObject*'], + Effect: 'Allow', + Resource: { + Ref: 'WebsiteBucketParamNew', + }, + }, + ], + }, + }, + }; + + const policy2Old = { + Type: 'AWS::IAM::Policy', + Properties: { + PolicyName: 'my-policy-old-2', + Roles: [ + { Ref: 'ServiceRole' }, + ], + PolicyDocument: { + Statement: [ + { + Action: ['s3:GetObject*'], + Effect: 'Allow', + Resource: { + Ref: 'WebsiteBucketParamOld', + }, + }, + ], + }, + }, + }; + + const policy2New = { + Type: 'AWS::IAM::Policy', + Properties: { + PolicyName: 'my-policy-new-2', + Roles: [ + { Ref: 'ServiceRole2' }, + ], + PolicyDocument: { + Statement: [ + { + Action: ['s3:GetObject*'], + Effect: 'Allow', + Resource: { + Ref: 'DifferentBucketParamOld', + }, + }, + ], + }, + }, + }; + + const deploymentLambda = { + Type: 'AWS::Lambda::Function', + Role: { + 'Fn::GetAtt': [ + 'ServiceRole', + 'Arn', + ], + }, + }; + + const s3DeploymentOld = { Type: 'Custom::CDKBucketDeployment', Properties: { ServiceToken: { 'Fn::GetAtt': [ - 'S3DeploymentLambda2', + 'S3DeploymentLambda', 'Arn', ], }, SourceBucketNames: ['src-bucket-old'], SourceObjectKeys: ['src-key-old'], - DestinationBucketName: 'DifferentBucketOld', + DestinationBucketName: 'WebsiteBucketOld', }, }; - const s3Deployment2New = { + const s3DeploymentNew = { Type: 'Custom::CDKBucketDeployment', Properties: { ServiceToken: { 'Fn::GetAtt': [ - 'S3DeploymentLambda2', + 'S3DeploymentLambda', 'Arn', ], }, SourceBucketNames: ['src-bucket-new'], SourceObjectKeys: ['src-key-new'], - DestinationBucketName: 'DifferentBucketNew', + DestinationBucketName: 'WebsiteBucketNew', }, }; - setup.setCurrentCfnStackTemplate({ - Resources: { - ServiceRole: serviceRole, - ServiceRole2: serviceRole, - Policy1: policyOld, - Policy2: policy2Old, - S3DeploymentLambda: deploymentLambda, - S3DeploymentLambda2: deploymentLambda, - S3Deployment: s3DeploymentOld, - S3Deployment2: s3Deployment2Old, - }, + beforeEach(() => { + setup.pushStackResourceSummaries( + setup.stackSummaryOf('S3DeploymentLambda', 'AWS::Lambda::Function', 'my-deployment-lambda'), + setup.stackSummaryOf('ServiceRole', 'AWS::IAM::Role', 'my-service-role'), + ); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + test('calls the lambdaInvoke() API when it receives an asset difference in an S3 bucket deployment and an IAM Policy difference using old-style synthesis', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Parameters: parameters, ServiceRole: serviceRole, - ServiceRole2: serviceRole, - Policy1: policyNew, - Policy2: policy2New, + Policy: policyOld, S3DeploymentLambda: deploymentLambda, - S3DeploymentLambda2: deploymentLambda, - S3Deployment: s3DeploymentNew, - S3Deployment2: s3Deployment2New, + S3Deployment: s3DeploymentOld, }, - }, + }); + + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Parameters: parameters, + ServiceRole: serviceRole, + Policy: policyNew, + S3DeploymentLambda: deploymentLambda, + S3Deployment: s3DeploymentNew, + }, + }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact, { WebsiteBucketParamOld: 'WebsiteBucketOld', WebsiteBucketParamNew: 'WebsiteBucketNew' }); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockLambdaInvoke).toHaveBeenCalledWith({ + FunctionName: 'arn:aws:lambda:here:123456789012:function:my-deployment-lambda', + Payload: JSON.stringify({ + ...payloadWithoutCustomResProps, + ResourceProperties: { + SourceBucketNames: ['src-bucket-new'], + SourceObjectKeys: ['src-key-new'], + DestinationBucketName: 'WebsiteBucketNew', + }, + }), + }); }); - // WHEN - setup.pushStackResourceSummaries( - setup.stackSummaryOf('S3DeploymentLambda2', 'AWS::Lambda::Function', 'my-deployment-lambda-2'), - setup.stackSummaryOf('ServiceRole2', 'AWS::IAM::Role', 'my-service-role-2'), - ); - - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact, { - WebsiteBucketParamOld: 'WebsiteBucketOld', - WebsiteBucketParamNew: 'WebsiteBucketNew', - DifferentBucketParamNew: 'WebsiteBucketNew', + test(`does not call the lambdaInvoke() API when the difference in the S3 deployment is referred to in one IAM policy change but not another + in CLASSIC mode but does in HOTSWAP_ONLY`, + async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ + Resources: { + ServiceRole: serviceRole, + Policy1: policyOld, + Policy2: policy2Old, + S3DeploymentLambda: deploymentLambda, + S3Deployment: s3DeploymentOld, + }, + }); + + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + ServiceRole: serviceRole, + Policy1: policyNew, + Policy2: { + Properties: { + Roles: [ + { Ref: 'ServiceRole' }, + 'different-role', + ], + PolicyDocument: { + Statement: [ + { + Action: ['s3:GetObject*'], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'DifferentBucketNew', + 'Arn', + ], + }, + }, + ], + }, + }, + }, + S3DeploymentLambda: deploymentLambda, + S3Deployment: s3DeploymentNew, + }, + }, + }); + + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockLambdaInvoke).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockLambdaInvoke).toHaveBeenCalledWith({ + FunctionName: 'arn:aws:lambda:here:123456789012:function:my-deployment-lambda', + Payload: JSON.stringify({ + ...payloadWithoutCustomResProps, + ResourceProperties: { + SourceBucketNames: ['src-bucket-new'], + SourceObjectKeys: ['src-key-new'], + DestinationBucketName: 'WebsiteBucketNew', + }, + }), + }); + } }); - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockLambdaInvoke).toHaveBeenCalledWith({ - FunctionName: 'arn:aws:lambda:here:123456789012:function:my-deployment-lambda', - Payload: JSON.stringify({ - ...payloadWithoutCustomResProps, - ResourceProperties: { - SourceBucketNames: ['src-bucket-new'], - SourceObjectKeys: ['src-key-new'], - DestinationBucketName: 'WebsiteBucketNew', + test(`does not call the lambdaInvoke() API when the lambda that references the role is referred to by something other than an S3 deployment + in CLASSIC mode but does in HOTSWAP_ONLY mode`, + async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ + Resources: { + ServiceRole: serviceRole, + Policy: policyOld, + S3DeploymentLambda: deploymentLambda, + S3Deployment: s3DeploymentOld, + Endpoint: { + Type: 'AWS::Lambda::Permission', + Properties: { + Action: 'lambda:InvokeFunction', + FunctionName: { + 'Fn::GetAtt': [ + 'S3DeploymentLambda', + 'Arn', + ], + }, + Principal: 'apigateway.amazonaws.com', + }, + }, }, - }), + }); + + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + ServiceRole: serviceRole, + Policy: policyNew, + S3DeploymentLambda: deploymentLambda, + S3Deployment: s3DeploymentNew, + Endpoint: { + Type: 'AWS::Lambda::Permission', + Properties: { + Action: 'lambda:InvokeFunction', + FunctionName: { + 'Fn::GetAtt': [ + 'S3DeploymentLambda', + 'Arn', + ], + }, + Principal: 'apigateway.amazonaws.com', + }, + }, + }, + }, + }); + + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockLambdaInvoke).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockLambdaInvoke).toHaveBeenCalledWith({ + FunctionName: 'arn:aws:lambda:here:123456789012:function:my-deployment-lambda', + Payload: JSON.stringify({ + ...payloadWithoutCustomResProps, + ResourceProperties: { + SourceBucketNames: ['src-bucket-new'], + SourceObjectKeys: ['src-key-new'], + DestinationBucketName: 'WebsiteBucketNew', + }, + }), + }); + } }); - expect(mockLambdaInvoke).toHaveBeenCalledWith({ - FunctionName: 'arn:aws:lambda:here:123456789012:function:my-deployment-lambda-2', - Payload: JSON.stringify({ - ...payloadWithoutCustomResProps, - ResourceProperties: { + test('calls the lambdaInvoke() API when it receives an asset difference in two S3 bucket deployments and IAM Policy differences using old-style synthesis', async () => { + // GIVEN + const s3Deployment2Old = { + Type: 'Custom::CDKBucketDeployment', + Properties: { + ServiceToken: { + 'Fn::GetAtt': [ + 'S3DeploymentLambda2', + 'Arn', + ], + }, + SourceBucketNames: ['src-bucket-old'], + SourceObjectKeys: ['src-key-old'], + DestinationBucketName: 'DifferentBucketOld', + }, + }; + + const s3Deployment2New = { + Type: 'Custom::CDKBucketDeployment', + Properties: { + ServiceToken: { + 'Fn::GetAtt': [ + 'S3DeploymentLambda2', + 'Arn', + ], + }, SourceBucketNames: ['src-bucket-new'], SourceObjectKeys: ['src-key-new'], DestinationBucketName: 'DifferentBucketNew', }, - }), - }); - }); + }; - test('does not call the lambdaInvoke() API when it receives an asset difference in an S3 bucket deployment that references two different policies', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - ServiceRole: serviceRole, - Policy1: policyOld, - Policy2: policy2Old, - S3DeploymentLambda: deploymentLambda, - S3Deployment: s3DeploymentOld, - }, + setup.setCurrentCfnStackTemplate({ + Resources: { + ServiceRole: serviceRole, + ServiceRole2: serviceRole, + Policy1: policyOld, + Policy2: policy2Old, + S3DeploymentLambda: deploymentLambda, + S3DeploymentLambda2: deploymentLambda, + S3Deployment: s3DeploymentOld, + S3Deployment2: s3Deployment2Old, + }, + }); + + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Parameters: parameters, + ServiceRole: serviceRole, + ServiceRole2: serviceRole, + Policy1: policyNew, + Policy2: policy2New, + S3DeploymentLambda: deploymentLambda, + S3DeploymentLambda2: deploymentLambda, + S3Deployment: s3DeploymentNew, + S3Deployment2: s3Deployment2New, + }, + }, + }); + + // WHEN + setup.pushStackResourceSummaries( + setup.stackSummaryOf('S3DeploymentLambda2', 'AWS::Lambda::Function', 'my-deployment-lambda-2'), + setup.stackSummaryOf('ServiceRole2', 'AWS::IAM::Role', 'my-service-role-2'), + ); + + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact, { + WebsiteBucketParamOld: 'WebsiteBucketOld', + WebsiteBucketParamNew: 'WebsiteBucketNew', + DifferentBucketParamNew: 'WebsiteBucketNew', + }); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockLambdaInvoke).toHaveBeenCalledWith({ + FunctionName: 'arn:aws:lambda:here:123456789012:function:my-deployment-lambda', + Payload: JSON.stringify({ + ...payloadWithoutCustomResProps, + ResourceProperties: { + SourceBucketNames: ['src-bucket-new'], + SourceObjectKeys: ['src-key-new'], + DestinationBucketName: 'WebsiteBucketNew', + }, + }), + }); + + expect(mockLambdaInvoke).toHaveBeenCalledWith({ + FunctionName: 'arn:aws:lambda:here:123456789012:function:my-deployment-lambda-2', + Payload: JSON.stringify({ + ...payloadWithoutCustomResProps, + ResourceProperties: { + SourceBucketNames: ['src-bucket-new'], + SourceObjectKeys: ['src-key-new'], + DestinationBucketName: 'DifferentBucketNew', + }, + }), + }); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + test(`does not call the lambdaInvoke() API when it receives an asset difference in an S3 bucket deployment that references two different policies + in CLASSIC mode but does in HOTSWAP_ONLY mode`, + async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { ServiceRole: serviceRole, - Policy1: policyNew, - Policy2: { - Properties: { - Roles: [ - { Ref: 'ServiceRole' }, - ], - PolicyDocument: { - Statement: [ - { - Action: ['s3:GetObject*'], - Effect: 'Allow', - Resource: { - 'Fn::GetAtt': [ - 'DifferentBucketNew', - 'Arn', - ], - }, - }, + Policy1: policyOld, + Policy2: policy2Old, + S3DeploymentLambda: deploymentLambda, + S3Deployment: s3DeploymentOld, + }, + }); + + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + ServiceRole: serviceRole, + Policy1: policyNew, + Policy2: { + Properties: { + Roles: [ + { Ref: 'ServiceRole' }, ], + PolicyDocument: { + Statement: [ + { + Action: ['s3:GetObject*'], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'DifferentBucketNew', + 'Arn', + ], + }, + }, + ], + }, }, }, + S3DeploymentLambda: deploymentLambda, + S3Deployment: s3DeploymentNew, }, - S3DeploymentLambda: deploymentLambda, - S3Deployment: s3DeploymentNew, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockLambdaInvoke).not.toHaveBeenCalled(); - }); - - test('does not call the lambdaInvoke() API when a policy is referenced by a resource that is not an S3 deployment', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - ServiceRole: serviceRole, - Policy1: policyOld, - S3DeploymentLambda: deploymentLambda, - S3Deployment: s3DeploymentOld, - NotADeployment: { - Type: 'AWS::Not::S3Deployment', - Properties: { - Prop: { - Ref: 'ServiceRole', + }); + + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockLambdaInvoke).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockLambdaInvoke).toHaveBeenCalledWith({ + FunctionName: 'arn:aws:lambda:here:123456789012:function:my-deployment-lambda', + Payload: JSON.stringify({ + ...payloadWithoutCustomResProps, + ResourceProperties: { + SourceBucketNames: ['src-bucket-new'], + SourceObjectKeys: ['src-key-new'], + DestinationBucketName: 'WebsiteBucketNew', }, - }, - }, - }, + }), + }); + } }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + test(`does not call the lambdaInvoke() API when a policy is referenced by a resource that is not an S3 deployment + in CLASSIC mode but does in HOTSWAP_ONLY mode`, + async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { ServiceRole: serviceRole, - Policy1: policyNew, + Policy1: policyOld, S3DeploymentLambda: deploymentLambda, - S3Deployment: s3DeploymentNew, + S3Deployment: s3DeploymentOld, NotADeployment: { Type: 'AWS::Not::S3Deployment', Properties: { @@ -725,14 +807,52 @@ describe('old-style synthesis', () => { }, }, }, - }, + }); + + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + ServiceRole: serviceRole, + Policy1: policyNew, + S3DeploymentLambda: deploymentLambda, + S3Deployment: s3DeploymentNew, + NotADeployment: { + Type: 'AWS::Not::S3Deployment', + Properties: { + Prop: { + Ref: 'ServiceRole', + }, + }, + }, + }, + }, + }); + + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockLambdaInvoke).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockLambdaInvoke).toHaveBeenCalledWith({ + FunctionName: 'arn:aws:lambda:here:123456789012:function:my-deployment-lambda', + Payload: JSON.stringify({ + ...payloadWithoutCustomResProps, + ResourceProperties: { + SourceBucketNames: ['src-bucket-new'], + SourceObjectKeys: ['src-key-new'], + DestinationBucketName: 'WebsiteBucketNew', + }, + }), + }); + } }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockLambdaInvoke).not.toHaveBeenCalled(); }); -}); \ No newline at end of file +}); diff --git a/packages/aws-cdk/test/api/hotswap/state-machine-hotswap-deployments.test.ts b/packages/aws-cdk/test/api/hotswap/state-machine-hotswap-deployments.test.ts index 63ebb1deb8e06..a42dfb0487847 100644 --- a/packages/aws-cdk/test/api/hotswap/state-machine-hotswap-deployments.test.ts +++ b/packages/aws-cdk/test/api/hotswap/state-machine-hotswap-deployments.test.ts @@ -1,4 +1,5 @@ import { StepFunctions } from 'aws-sdk'; +import { HotswapMode } from '../../../lib/api/hotswap/common'; import * as setup from './hotswap-test-setup'; let mockUpdateMachineDefinition: (params: StepFunctions.Types.UpdateStateMachineInput) => StepFunctions.Types.UpdateStateMachineOutput; @@ -10,94 +11,78 @@ beforeEach(() => { hotswapMockSdkProvider.setUpdateStateMachineMock(mockUpdateMachineDefinition); }); -test('returns undefined when a new StateMachine is added to the Stack', async () => { - // GIVEN - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { - Resources: { - Machine: { - Type: 'AWS::StepFunctions::StateMachine', +describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hotswapMode) => { + test('returns undefined when a new StateMachine is added to the Stack', async () => { + // GIVEN + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Machine: { + Type: 'AWS::StepFunctions::StateMachine', + }, }, }, - }, - }); + }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN - expect(deployStackResult).toBeUndefined(); -}); + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); -test('calls the updateStateMachine() API when it receives only a definitionString change without Fn::Join in a state machine', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Machine: { - Type: 'AWS::StepFunctions::StateMachine', - Properties: { - DefinitionString: '{ Prop: "old-value" }', - StateMachineName: 'my-machine', - }, - }, - }, + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(deployStackResult?.noOp).toEqual(true); + expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); + } }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('calls the updateStateMachine() API when it receives only a definitionString change without Fn::Join in a state machine', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Machine: { Type: 'AWS::StepFunctions::StateMachine', Properties: { - DefinitionString: '{ Prop: "new-value" }', + DefinitionString: '{ Prop: "old-value" }', StateMachineName: 'my-machine', }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateMachineDefinition).toHaveBeenCalledWith({ - definition: '{ Prop: "new-value" }', - stateMachineArn: 'arn:aws:states:here:123456789012:stateMachine:my-machine', - }); -}); - -test('calls the updateStateMachine() API when it receives only a definitionString change with Fn::Join in a state machine', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Machine: { - Type: 'AWS::StepFunctions::StateMachine', - Properties: { - DefinitionString: { - 'Fn::Join': [ - '\n', - [ - '{', - ' "StartAt" : "SuccessState"', - ' "States" : {', - ' "SuccessState": {', - ' "Type": "Pass"', - ' "Result": "Success"', - ' "End": true', - ' }', - ' }', - '}', - ], - ], + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Machine: { + Type: 'AWS::StepFunctions::StateMachine', + Properties: { + DefinitionString: '{ Prop: "new-value" }', + StateMachineName: 'my-machine', + }, }, - StateMachineName: 'my-machine', }, }, - }, + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateMachineDefinition).toHaveBeenCalledWith({ + definition: '{ Prop: "new-value" }', + stateMachineArn: 'arn:aws:states:here:123456789012:stateMachine:my-machine', + }); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('calls the updateStateMachine() API when it receives only a definitionString change with Fn::Join in a state machine', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Machine: { Type: 'AWS::StepFunctions::StateMachine', @@ -107,10 +92,12 @@ test('calls the updateStateMachine() API when it receives only a definitionStrin '\n', [ '{', - ' "StartAt": "SuccessState",', - ' "States": {', + ' "StartAt" : "SuccessState"', + ' "States" : {', ' "SuccessState": {', - ' "Type": "Succeed"', + ' "Type": "Pass"', + ' "Result": "Success"', + ' "End": true', ' }', ' }', '}', @@ -121,209 +108,231 @@ test('calls the updateStateMachine() API when it receives only a definitionStrin }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateMachineDefinition).toHaveBeenCalledWith({ - definition: JSON.stringify({ - StartAt: 'SuccessState', - States: { - SuccessState: { - Type: 'Succeed', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Machine: { + Type: 'AWS::StepFunctions::StateMachine', + Properties: { + DefinitionString: { + 'Fn::Join': [ + '\n', + [ + '{', + ' "StartAt": "SuccessState",', + ' "States": {', + ' "SuccessState": {', + ' "Type": "Succeed"', + ' }', + ' }', + '}', + ], + ], + }, + StateMachineName: 'my-machine', + }, + }, }, }, - }, null, 2), - stateMachineArn: 'arn:aws:states:here:123456789012:stateMachine:my-machine', - }); -}); - -test('calls the updateStateMachine() API when it receives a change to the definitionString in a state machine that has no name', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Machine: { - Type: 'AWS::StepFunctions::StateMachine', - Properties: { - DefinitionString: '{ "Prop" : "old-value" }', + }); + + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateMachineDefinition).toHaveBeenCalledWith({ + definition: JSON.stringify({ + StartAt: 'SuccessState', + States: { + SuccessState: { + Type: 'Succeed', + }, }, - }, - }, + }, null, 2), + stateMachineArn: 'arn:aws:states:here:123456789012:stateMachine:my-machine', + }); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('calls the updateStateMachine() API when it receives a change to the definitionString in a state machine that has no name', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Machine: { Type: 'AWS::StepFunctions::StateMachine', Properties: { - DefinitionString: '{ "Prop" : "new-value" }', + DefinitionString: '{ "Prop" : "old-value" }', }, }, }, - }, - }); - - // WHEN - setup.pushStackResourceSummaries(setup.stackSummaryOf('Machine', 'AWS::StepFunctions::StateMachine', 'arn:aws:states:here:123456789012:stateMachine:my-machine')); - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateMachineDefinition).toHaveBeenCalledWith({ - definition: '{ "Prop" : "new-value" }', - stateMachineArn: 'arn:aws:states:here:123456789012:stateMachine:my-machine', - }); -}); - -test('does not call the updateStateMachine() API when it receives a change to a property that is not the definitionString in a state machine', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Machine: { - Type: 'AWS::StepFunctions::StateMachine', - Properties: { - DefinitionString: '{ "Prop" : "old-value" }', - LoggingConfiguration: { // non-definitionString property - IncludeExecutionData: true, + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Machine: { + Type: 'AWS::StepFunctions::StateMachine', + Properties: { + DefinitionString: '{ "Prop" : "new-value" }', + }, }, }, }, - }, + }); + + // WHEN + setup.pushStackResourceSummaries(setup.stackSummaryOf('Machine', 'AWS::StepFunctions::StateMachine', 'arn:aws:states:here:123456789012:stateMachine:my-machine')); + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateMachineDefinition).toHaveBeenCalledWith({ + definition: '{ "Prop" : "new-value" }', + stateMachineArn: 'arn:aws:states:here:123456789012:stateMachine:my-machine', + }); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test(`does not call the updateStateMachine() API when it receives a change to a property that is not the definitionString in a state machine + alongside a hotswappable change in CLASSIC mode but does in HOTSWAP_ONLY mode`, + async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Machine: { Type: 'AWS::StepFunctions::StateMachine', Properties: { - DefinitionString: '{ "Prop" : "new-value" }', - LoggingConfiguration: { - IncludeExecutionData: false, + DefinitionString: '{ "Prop" : "old-value" }', + LoggingConfiguration: { // non-definitionString property + IncludeExecutionData: true, }, }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); -}); - -test('does not call the updateStateMachine() API when a resource has a DefinitionString property but is not an AWS::StepFunctions::StateMachine is changed', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Machine: { - Type: 'AWS::NotStepFunctions::NotStateMachine', - Properties: { - DefinitionString: '{ Prop: "old-value" }', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Machine: { + Type: 'AWS::StepFunctions::StateMachine', + Properties: { + DefinitionString: '{ "Prop" : "new-value" }', + LoggingConfiguration: { + IncludeExecutionData: false, + }, + }, + }, }, }, - }, - }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + }); + + setup.pushStackResourceSummaries(setup.stackSummaryOf('Machine', 'AWS::StepFunctions::StateMachine', 'arn:aws:states:here:123456789012:stateMachine:my-machine')); + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateMachineDefinition).toHaveBeenCalledWith({ + definition: '{ "Prop" : "new-value" }', + stateMachineArn: 'arn:aws:states:here:123456789012:stateMachine:my-machine', + }); + } + }); + + test('does not call the updateStateMachine() API when a resource has a DefinitionString property but is not an AWS::StepFunctions::StateMachine is changed', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Machine: { Type: 'AWS::NotStepFunctions::NotStateMachine', Properties: { - DefinitionString: '{ Prop: "new-value" }', + DefinitionString: '{ Prop: "old-value" }', }, }, }, - }, - }); - - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).toBeUndefined(); - expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); -}); - -test('can correctly hotswap old style synth changes', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Parameters: { AssetParam1: { Type: 'String' } }, - Resources: { - Machine: { - Type: 'AWS::StepFunctions::StateMachine', - Properties: { - DefinitionString: { Ref: 'AssetParam1' }, - StateMachineName: 'machine-name', + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Machine: { + Type: 'AWS::NotStepFunctions::NotStateMachine', + Properties: { + DefinitionString: '{ Prop: "new-value" }', + }, + }, }, }, - }, - }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { - Parameters: { AssetParam2: { Type: String } }, + }); + + if (hotswapMode === HotswapMode.FALL_BACK) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).toBeUndefined(); + expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); + } else if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(deployStackResult?.noOp).toEqual(true); + expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); + } + }); + + test('can correctly hotswap old style synth changes', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ + Parameters: { AssetParam1: { Type: 'String' } }, Resources: { Machine: { Type: 'AWS::StepFunctions::StateMachine', Properties: { - DefinitionString: { Ref: 'AssetParam2' }, + DefinitionString: { Ref: 'AssetParam1' }, StateMachineName: 'machine-name', }, }, }, - }, - }); - - // WHEN - setup.pushStackResourceSummaries(setup.stackSummaryOf('Machine', 'AWS::StepFunctions::StateMachine', 'arn:aws:states:here:123456789012:stateMachine:my-machine')); - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact, { AssetParam2: 'asset-param-2' }); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateMachineDefinition).toHaveBeenCalledWith({ - definition: 'asset-param-2', - stateMachineArn: 'arn:aws:states:here:123456789012:stateMachine:machine-name', - }); -}); - -test('calls the updateStateMachine() API when it receives a change to the definitionString that uses Attributes in a state machine', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - }, - Machine: { - Type: 'AWS::StepFunctions::StateMachine', - Properties: { - DefinitionString: { - 'Fn::Join': [ - '\n', - [ - '{', - ' "StartAt" : "SuccessState"', - ' "States" : {', - ' "SuccessState": {', - ' "Type": "Succeed"', - ' }', - ' }', - '}', - ], - ], + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Parameters: { AssetParam2: { Type: String } }, + Resources: { + Machine: { + Type: 'AWS::StepFunctions::StateMachine', + Properties: { + DefinitionString: { Ref: 'AssetParam2' }, + StateMachineName: 'machine-name', + }, }, - StateMachineName: 'my-machine', }, }, - }, + }); + + // WHEN + setup.pushStackResourceSummaries(setup.stackSummaryOf('Machine', 'AWS::StepFunctions::StateMachine', 'arn:aws:states:here:123456789012:stateMachine:my-machine')); + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact, { AssetParam2: 'asset-param-2' }); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateMachineDefinition).toHaveBeenCalledWith({ + definition: 'asset-param-2', + stateMachineArn: 'arn:aws:states:here:123456789012:stateMachine:machine-name', + }); }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('calls the updateStateMachine() API when it receives a change to the definitionString that uses Attributes in a state machine', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Func: { Type: 'AWS::Lambda::Function', @@ -333,10 +342,16 @@ test('calls the updateStateMachine() API when it receives a change to the defini Properties: { DefinitionString: { 'Fn::Join': [ - '', + '\n', [ - '"Resource": ', - { 'Fn::GetAtt': ['Func', 'Arn'] }, + '{', + ' "StartAt" : "SuccessState"', + ' "States" : {', + ' "SuccessState": {', + ' "Type": "Succeed"', + ' }', + ' }', + '}', ], ], }, @@ -344,52 +359,50 @@ test('calls the updateStateMachine() API when it receives a change to the defini }, }, }, - }, - }); - - // WHEN - setup.pushStackResourceSummaries( - setup.stackSummaryOf('Machine', 'AWS::StepFunctions::StateMachine', 'arn:aws:states:here:123456789012:stateMachine:my-machine'), - setup.stackSummaryOf('Func', 'AWS::Lambda::Function', 'my-func'), - ); - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateMachineDefinition).toHaveBeenCalledWith({ - definition: '"Resource": arn:aws:lambda:here:123456789012:function:my-func', - stateMachineArn: 'arn:aws:states:here:123456789012:stateMachine:my-machine', - }); -}); - -test("will not perform a hotswap deployment if it cannot find a Ref target (outside the state machine's name)", async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Parameters: { - Param1: { Type: 'String' }, - }, - Resources: { - Machine: { - Type: 'AWS::StepFunctions::StateMachine', - Properties: { - DefinitionString: { - 'Fn::Join': [ - '', - [ - '{ Prop: "old-value" }, ', - '{ "Param" : ', - { 'Fn::Sub': '${Param1}' }, - ' }', - ], - ], + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + }, + Machine: { + Type: 'AWS::StepFunctions::StateMachine', + Properties: { + DefinitionString: { + 'Fn::Join': [ + '', + [ + '"Resource": ', + { 'Fn::GetAtt': ['Func', 'Arn'] }, + ], + ], + }, + StateMachineName: 'my-machine', + }, }, }, }, - }, - }); - setup.pushStackResourceSummaries(setup.stackSummaryOf('Machine', 'AWS::StepFunctions::StateMachine', 'my-machine')); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + }); + + // WHEN + setup.pushStackResourceSummaries( + setup.stackSummaryOf('Machine', 'AWS::StepFunctions::StateMachine', 'arn:aws:states:here:123456789012:stateMachine:my-machine'), + setup.stackSummaryOf('Func', 'AWS::Lambda::Function', 'my-func'), + ); + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(mockUpdateMachineDefinition).toHaveBeenCalledWith({ + definition: '"Resource": arn:aws:lambda:here:123456789012:function:my-func', + stateMachineArn: 'arn:aws:states:here:123456789012:stateMachine:my-machine', + }); + }); + + test("will not perform a hotswap deployment if it cannot find a Ref target (outside the state machine's name)", async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Parameters: { Param1: { Type: 'String' }, }, @@ -401,7 +414,7 @@ test("will not perform a hotswap deployment if it cannot find a Ref target (outs 'Fn::Join': [ '', [ - '{ Prop: "new-value" }, ', + '{ Prop: "old-value" }, ', '{ "Param" : ', { 'Fn::Sub': '${Param1}' }, ' }', @@ -411,47 +424,43 @@ test("will not perform a hotswap deployment if it cannot find a Ref target (outs }, }, }, - }, - }); - - // THEN - await expect(() => - hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact), - ).rejects.toThrow(/Parameter or resource 'Param1' could not be found for evaluation/); -}); - -test("will not perform a hotswap deployment if it doesn't know how to handle a specific attribute (outside the state machines's name)", async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Bucket: { - Type: 'AWS::S3::Bucket', - }, - Machine: { - Type: 'AWS::StepFunctions::StateMachine', - Properties: { - DefinitionString: { - 'Fn::Join': [ - '', - [ - '{ Prop: "old-value" }, ', - '{ "S3Bucket" : ', - { 'Fn::GetAtt': ['Bucket', 'UnknownAttribute'] }, - ' }', - ], - ], + }); + setup.pushStackResourceSummaries(setup.stackSummaryOf('Machine', 'AWS::StepFunctions::StateMachine', 'my-machine')); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Parameters: { + Param1: { Type: 'String' }, + }, + Resources: { + Machine: { + Type: 'AWS::StepFunctions::StateMachine', + Properties: { + DefinitionString: { + 'Fn::Join': [ + '', + [ + '{ Prop: "new-value" }, ', + '{ "Param" : ', + { 'Fn::Sub': '${Param1}' }, + ' }', + ], + ], + }, + }, }, - StateMachineName: 'my-machine', }, }, - }, + }); + + // THEN + await expect(() => + hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact), + ).rejects.toThrow(/Parameter or resource 'Param1' could not be found for evaluation/); }); - setup.pushStackResourceSummaries( - setup.stackSummaryOf('Machine', 'AWS::StepFunctions::StateMachine', 'arn:aws:states:here:123456789012:stateMachine:my-machine'), - setup.stackSummaryOf('Bucket', 'AWS::S3::Bucket', 'my-bucket'), - ); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test("will not perform a hotswap deployment if it doesn't know how to handle a specific attribute (outside the state machines's name)", async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Bucket: { Type: 'AWS::S3::Bucket', @@ -463,7 +472,7 @@ test("will not perform a hotswap deployment if it doesn't know how to handle a s 'Fn::Join': [ '', [ - '{ Prop: "new-value" }, ', + '{ Prop: "old-value" }, ', '{ "S3Bucket" : ', { 'Fn::GetAtt': ['Bucket', 'UnknownAttribute'] }, ' }', @@ -474,49 +483,47 @@ test("will not perform a hotswap deployment if it doesn't know how to handle a s }, }, }, - }, - }); - - // THEN - await expect(() => - hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact), - ).rejects.toThrow("We don't support the 'UnknownAttribute' attribute of the 'AWS::S3::Bucket' resource. This is a CDK limitation. Please report it at https://github.com/aws/aws-cdk/issues/new/choose"); -}); - -test('knows how to handle attributes of the AWS::Events::EventBus resource', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - EventBus: { - Type: 'AWS::Events::EventBus', - Properties: { - Name: 'my-event-bus', - }, - }, - Machine: { - Type: 'AWS::StepFunctions::StateMachine', - Properties: { - DefinitionString: { - 'Fn::Join': ['', [ - '{"EventBus1Arn":"', - { 'Fn::GetAtt': ['EventBus', 'Arn'] }, - '","EventBus1Name":"', - { 'Fn::GetAtt': ['EventBus', 'Name'] }, - '","EventBus1Ref":"', - { Ref: 'EventBus' }, - '"}', - ]], - }, - StateMachineName: 'my-machine', + }); + setup.pushStackResourceSummaries( + setup.stackSummaryOf('Machine', 'AWS::StepFunctions::StateMachine', 'arn:aws:states:here:123456789012:stateMachine:my-machine'), + setup.stackSummaryOf('Bucket', 'AWS::S3::Bucket', 'my-bucket'), + ); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Bucket: { + Type: 'AWS::S3::Bucket', + }, + Machine: { + Type: 'AWS::StepFunctions::StateMachine', + Properties: { + DefinitionString: { + 'Fn::Join': [ + '', + [ + '{ Prop: "new-value" }, ', + '{ "S3Bucket" : ', + { 'Fn::GetAtt': ['Bucket', 'UnknownAttribute'] }, + ' }', + ], + ], + }, + StateMachineName: 'my-machine', + }, + }, }, }, - }, + }); + + // THEN + await expect(() => + hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact), + ).rejects.toThrow("We don't support the 'UnknownAttribute' attribute of the 'AWS::S3::Bucket' resource. This is a CDK limitation. Please report it at https://github.com/aws/aws-cdk/issues/new/choose"); }); - setup.pushStackResourceSummaries( - setup.stackSummaryOf('EventBus', 'AWS::Events::EventBus', 'my-event-bus'), - ); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('knows how to handle attributes of the AWS::Events::EventBus resource', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { EventBus: { Type: 'AWS::Events::EventBus', @@ -529,11 +536,11 @@ test('knows how to handle attributes of the AWS::Events::EventBus resource', asy Properties: { DefinitionString: { 'Fn::Join': ['', [ - '{"EventBus2Arn":"', + '{"EventBus1Arn":"', { 'Fn::GetAtt': ['EventBus', 'Arn'] }, - '","EventBus2Name":"', + '","EventBus1Name":"', { 'Fn::GetAtt': ['EventBus', 'Name'] }, - '","EventBus2Ref":"', + '","EventBus1Ref":"', { Ref: 'EventBus' }, '"}', ]], @@ -542,55 +549,57 @@ test('knows how to handle attributes of the AWS::Events::EventBus resource', asy }, }, }, - }, - }); - - // THEN - const result = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); - - expect(result).not.toBeUndefined(); - expect(mockUpdateMachineDefinition).toHaveBeenCalledWith({ - stateMachineArn: 'arn:aws:states:here:123456789012:stateMachine:my-machine', - definition: JSON.stringify({ - EventBus2Arn: 'arn:aws:events:here:123456789012:event-bus/my-event-bus', - EventBus2Name: 'my-event-bus', - EventBus2Ref: 'my-event-bus', - }), - }); -}); - -test('knows how to handle attributes of the AWS::DynamoDB::Table resource', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Table: { - Type: 'AWS::DynamoDB::Table', - Properties: { - KeySchema: [{ - AttributeName: 'name', - KeyType: 'HASH', - }], - AttributeDefinitions: [{ - AttributeName: 'name', - AttributeType: 'S', - }], - BillingMode: 'PAY_PER_REQUEST', - }, - }, - Machine: { - Type: 'AWS::StepFunctions::StateMachine', - Properties: { - DefinitionString: '{}', - StateMachineName: 'my-machine', + }); + setup.pushStackResourceSummaries( + setup.stackSummaryOf('EventBus', 'AWS::Events::EventBus', 'my-event-bus'), + ); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + EventBus: { + Type: 'AWS::Events::EventBus', + Properties: { + Name: 'my-event-bus', + }, + }, + Machine: { + Type: 'AWS::StepFunctions::StateMachine', + Properties: { + DefinitionString: { + 'Fn::Join': ['', [ + '{"EventBus2Arn":"', + { 'Fn::GetAtt': ['EventBus', 'Arn'] }, + '","EventBus2Name":"', + { 'Fn::GetAtt': ['EventBus', 'Name'] }, + '","EventBus2Ref":"', + { Ref: 'EventBus' }, + '"}', + ]], + }, + StateMachineName: 'my-machine', + }, + }, }, }, - }, + }); + + // THEN + const result = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); + + expect(result).not.toBeUndefined(); + expect(mockUpdateMachineDefinition).toHaveBeenCalledWith({ + stateMachineArn: 'arn:aws:states:here:123456789012:stateMachine:my-machine', + definition: JSON.stringify({ + EventBus2Arn: 'arn:aws:events:here:123456789012:event-bus/my-event-bus', + EventBus2Name: 'my-event-bus', + EventBus2Ref: 'my-event-bus', + }), + }); }); - setup.pushStackResourceSummaries( - setup.stackSummaryOf('Table', 'AWS::DynamoDB::Table', 'my-dynamodb-table'), - ); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + + test('knows how to handle attributes of the AWS::DynamoDB::Table resource', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Table: { Type: 'AWS::DynamoDB::Table', @@ -609,51 +618,67 @@ test('knows how to handle attributes of the AWS::DynamoDB::Table resource', asyn Machine: { Type: 'AWS::StepFunctions::StateMachine', Properties: { - DefinitionString: { - 'Fn::Join': ['', [ - '{"TableName":"', - { Ref: 'Table' }, - '","TableArn":"', - { 'Fn::GetAtt': ['Table', 'Arn'] }, - '"}', - ]], - }, + DefinitionString: '{}', StateMachineName: 'my-machine', }, }, }, - }, - }); + }); + setup.pushStackResourceSummaries( + setup.stackSummaryOf('Table', 'AWS::DynamoDB::Table', 'my-dynamodb-table'), + ); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Table: { + Type: 'AWS::DynamoDB::Table', + Properties: { + KeySchema: [{ + AttributeName: 'name', + KeyType: 'HASH', + }], + AttributeDefinitions: [{ + AttributeName: 'name', + AttributeType: 'S', + }], + BillingMode: 'PAY_PER_REQUEST', + }, + }, + Machine: { + Type: 'AWS::StepFunctions::StateMachine', + Properties: { + DefinitionString: { + 'Fn::Join': ['', [ + '{"TableName":"', + { Ref: 'Table' }, + '","TableArn":"', + { 'Fn::GetAtt': ['Table', 'Arn'] }, + '"}', + ]], + }, + StateMachineName: 'my-machine', + }, + }, + }, + }, + }); - // THEN - const result = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + // THEN + const result = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - expect(result).not.toBeUndefined(); - expect(mockUpdateMachineDefinition).toHaveBeenCalledWith({ - stateMachineArn: 'arn:aws:states:here:123456789012:stateMachine:my-machine', - definition: JSON.stringify({ - TableName: 'my-dynamodb-table', - TableArn: 'arn:aws:dynamodb:here:123456789012:table/my-dynamodb-table', - }), + expect(result).not.toBeUndefined(); + expect(mockUpdateMachineDefinition).toHaveBeenCalledWith({ + stateMachineArn: 'arn:aws:states:here:123456789012:stateMachine:my-machine', + definition: JSON.stringify({ + TableName: 'my-dynamodb-table', + TableArn: 'arn:aws:dynamodb:here:123456789012:table/my-dynamodb-table', + }), + }); }); -}); -test('does not explode if the DependsOn changes', async () => { - // GIVEN - setup.setCurrentCfnStackTemplate({ - Resources: { - Machine: { - Type: 'AWS::StepFunctions::StateMachine', - Properties: { - DefinitionString: '{ Prop: "old-value" }', - StateMachineName: 'my-machine', - }, - DependsOn: ['abc'], - }, - }, - }); - const cdkStackArtifact = setup.cdkStackArtifactOf({ - template: { + test('does not explode if the DependsOn changes', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ Resources: { Machine: { Type: 'AWS::StepFunctions::StateMachine', @@ -661,16 +686,31 @@ test('does not explode if the DependsOn changes', async () => { DefinitionString: '{ Prop: "old-value" }', StateMachineName: 'my-machine', }, + DependsOn: ['abc'], }, - DependsOn: ['xyz'], }, - }, - }); + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Machine: { + Type: 'AWS::StepFunctions::StateMachine', + Properties: { + DefinitionString: '{ Prop: "old-value" }', + StateMachineName: 'my-machine', + }, + }, + DependsOn: ['xyz'], + }, + }, + }); - // WHEN - const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + // WHEN + const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); - // THEN - expect(deployStackResult).not.toBeUndefined(); - expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); -}); \ No newline at end of file + // THEN + expect(deployStackResult).not.toBeUndefined(); + expect(deployStackResult?.noOp).toEqual(true); + expect(mockUpdateMachineDefinition).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index d2a8000464281..68319848f7f08 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -67,6 +67,7 @@ import { RequireApproval } from '../lib/diff'; import { flatten } from '../lib/util'; import { instanceMockFrom, MockCloudExecutable, TestStackArtifact, withMocked } from './util'; import { MockSdkProvider } from './util/mock-sdk'; +import { HotswapMode } from '../lib/api/hotswap/common'; let cloudExecutable: MockCloudExecutable; let bootstrapper: jest.Mocked; @@ -196,6 +197,7 @@ describe('readCurrentTemplate', () => { // WHEN await cdkToolkit.deploy({ selector: { patterns: ['Test-Stack-C'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, }); // THEN @@ -229,6 +231,7 @@ describe('readCurrentTemplate', () => { // WHEN await cdkToolkit.deploy({ selector: { patterns: ['Test-Stack-C'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, }); // THEN @@ -263,6 +266,7 @@ describe('readCurrentTemplate', () => { // WHEN await cdkToolkit.deploy({ selector: { patterns: ['Test-Stack-C'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, }); // THEN @@ -300,6 +304,7 @@ describe('readCurrentTemplate', () => { // WHEN await cdkToolkit.deploy({ selector: { patterns: ['Test-Stack-C'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, }); // THEN @@ -336,6 +341,7 @@ describe('readCurrentTemplate', () => { // WHEN await cdkToolkit.deploy({ selector: { patterns: ['Test-Stack-C'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, }); // THEN @@ -369,6 +375,7 @@ describe('readCurrentTemplate', () => { // WHEN await cdkToolkit.deploy({ selector: { patterns: ['Test-Stack-A'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, }); // THEN @@ -391,7 +398,10 @@ describe('deploy', () => { const toolkit = defaultToolkitSetup(); // WHEN - await expect(() => toolkit.deploy({ selector: { patterns: ['Test-Stack-D'] } })).rejects.toThrow('No stacks match the name(s) Test-Stack-D'); + await expect(() => toolkit.deploy({ + selector: { patterns: ['Test-Stack-D'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, + })).rejects.toThrow('No stacks match the name(s) Test-Stack-D'); }); describe('with hotswap deployment', () => { @@ -415,12 +425,12 @@ describe('deploy', () => { await cdkToolkit.deploy({ selector: { patterns: ['Test-Stack-A-Display-Name'] }, requireApproval: RequireApproval.Never, - hotswap: true, + hotswap: HotswapMode.FALL_BACK, }); // THEN expect(mockCfnDeployments.deployStack).toHaveBeenCalledWith(expect.objectContaining({ - hotswap: true, + hotswap: HotswapMode.FALL_BACK, })); }); }); @@ -431,7 +441,10 @@ describe('deploy', () => { const toolkit = defaultToolkitSetup(); // WHEN - await toolkit.deploy({ selector: { patterns: ['Test-Stack-A', 'Test-Stack-B'] } }); + await toolkit.deploy({ + selector: { patterns: ['Test-Stack-A', 'Test-Stack-B'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }); }); test('with stacks all stacks specified as double wildcard', async () => { @@ -439,7 +452,10 @@ describe('deploy', () => { const toolkit = defaultToolkitSetup(); // WHEN - await toolkit.deploy({ selector: { patterns: ['**'] } }); + await toolkit.deploy({ + selector: { patterns: ['**'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }); }); @@ -448,7 +464,10 @@ describe('deploy', () => { const toolkit = defaultToolkitSetup(); // WHEN - await toolkit.deploy({ selector: { patterns: ['Test-Stack-A-Display-Name'] } }); + await toolkit.deploy({ + selector: { patterns: ['Test-Stack-A-Display-Name'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }); }); test('with stacks all stacks specified as wildcard', async () => { @@ -456,7 +475,10 @@ describe('deploy', () => { const toolkit = defaultToolkitSetup(); // WHEN - await toolkit.deploy({ selector: { patterns: ['*'] } }); + await toolkit.deploy({ + selector: { patterns: ['*'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }); }); test('with sns notification arns', async () => { @@ -479,6 +501,7 @@ describe('deploy', () => { await toolkit.deploy({ selector: { patterns: ['Test-Stack-A', 'Test-Stack-B'] }, notificationArns, + hotswap: HotswapMode.FULL_DEPLOYMENT, }); }); @@ -499,6 +522,7 @@ describe('deploy', () => { toolkit.deploy({ selector: { patterns: ['Test-Stack-A'] }, notificationArns, + hotswap: HotswapMode.FULL_DEPLOYMENT, }), ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); @@ -580,6 +604,7 @@ describe('deploy', () => { await toolkit.deploy({ selector: { patterns: ['Test-Stack-Asset'] }, assetParallelism: false, + hotswap: HotswapMode.FULL_DEPLOYMENT, }); expect(mockBuildStackAssets).toHaveBeenCalledWith(expect.objectContaining({ @@ -610,6 +635,7 @@ describe('deploy', () => { await toolkit.deploy({ selector: { patterns: ['Test-Stack-Asset'] }, assetBuildTime: AssetBuildTime.JUST_IN_TIME, + hotswap: HotswapMode.FULL_DEPLOYMENT, }); expect(mockBuildStackAssets).not.toHaveBeenCalled(); @@ -638,7 +664,10 @@ describe('watch', () => { const toolkit = defaultToolkitSetup(); await expect(() => { - return toolkit.watch({ selector: { patterns: [] } }); + return toolkit.watch({ + selector: { patterns: [] }, + hotswap: HotswapMode.HOTSWAP_ONLY, + }); }).rejects.toThrow("Cannot use the 'watch' command without specifying at least one directory to monitor. " + 'Make sure to add a "watch" key to your cdk.json'); }); @@ -647,7 +676,10 @@ describe('watch', () => { cloudExecutable.configuration.settings.set(['watch'], {}); const toolkit = defaultToolkitSetup(); - await toolkit.watch({ selector: { patterns: [] } }); + await toolkit.watch({ + selector: { patterns: [] }, + hotswap: HotswapMode.HOTSWAP_ONLY, + }); const includeArgs = fakeChokidarWatch.includeArgs; expect(includeArgs.length).toBe(1); @@ -659,7 +691,10 @@ describe('watch', () => { }); const toolkit = defaultToolkitSetup(); - await toolkit.watch({ selector: { patterns: [] } }); + await toolkit.watch({ + selector: { patterns: [] }, + hotswap: HotswapMode.HOTSWAP_ONLY, + }); expect(fakeChokidarWatch.includeArgs).toStrictEqual(['my-dir']); }); @@ -670,7 +705,10 @@ describe('watch', () => { }); const toolkit = defaultToolkitSetup(); - await toolkit.watch({ selector: { patterns: [] } }); + await toolkit.watch({ + selector: { patterns: [] }, + hotswap: HotswapMode.HOTSWAP_ONLY, + }); expect(fakeChokidarWatch.includeArgs).toStrictEqual(['my-dir1', '**/my-dir2/*']); }); @@ -680,7 +718,10 @@ describe('watch', () => { cloudExecutable.configuration.settings.set(['output'], 'cdk.out'); const toolkit = defaultToolkitSetup(); - await toolkit.watch({ selector: { patterns: [] } }); + await toolkit.watch({ + selector: { patterns: [] }, + hotswap: HotswapMode.HOTSWAP_ONLY, + }); expect(fakeChokidarWatch.excludeArgs).toStrictEqual([ 'cdk.out/**', @@ -696,7 +737,10 @@ describe('watch', () => { }); const toolkit = defaultToolkitSetup(); - await toolkit.watch({ selector: { patterns: [] } }); + await toolkit.watch({ + selector: { patterns: [] }, + hotswap: HotswapMode.HOTSWAP_ONLY, + }); const excludeArgs = fakeChokidarWatch.excludeArgs; expect(excludeArgs.length).toBe(5); @@ -709,7 +753,10 @@ describe('watch', () => { }); const toolkit = defaultToolkitSetup(); - await toolkit.watch({ selector: { patterns: [] } }); + await toolkit.watch({ + selector: { patterns: [] }, + hotswap: HotswapMode.HOTSWAP_ONLY, + }); const excludeArgs = fakeChokidarWatch.excludeArgs; expect(excludeArgs.length).toBe(6); @@ -723,12 +770,67 @@ describe('watch', () => { const cdkDeployMock = jest.fn(); toolkit.deploy = cdkDeployMock; - await toolkit.watch({ selector: { patterns: [] }, concurrency: 3 }); + await toolkit.watch({ + selector: { patterns: [] }, + concurrency: 3, + hotswap: HotswapMode.HOTSWAP_ONLY, + }); fakeChokidarWatcherOn.readyCallback(); expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ concurrency: 3 })); }); + describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hotswapMode) => { + test('passes through the correct hotswap mode to deployStack()', async () => { + cloudExecutable.configuration.settings.set(['watch'], {}); + const toolkit = defaultToolkitSetup(); + const cdkDeployMock = jest.fn(); + toolkit.deploy = cdkDeployMock; + + await toolkit.watch({ selector: { patterns: [] }, hotswap: hotswapMode }); + fakeChokidarWatcherOn.readyCallback(); + + expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: hotswapMode })); + }); + }); + + test('respects HotswapMode.HOTSWAP_ONLY', async () => { + cloudExecutable.configuration.settings.set(['watch'], {}); + const toolkit = defaultToolkitSetup(); + const cdkDeployMock = jest.fn(); + toolkit.deploy = cdkDeployMock; + + await toolkit.watch({ selector: { patterns: [] }, hotswap: HotswapMode.HOTSWAP_ONLY }); + fakeChokidarWatcherOn.readyCallback(); + + expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: HotswapMode.HOTSWAP_ONLY })); + }); + + test('respects HotswapMode.FALL_BACK', async () => { + cloudExecutable.configuration.settings.set(['watch'], {}); + const toolkit = defaultToolkitSetup(); + const cdkDeployMock = jest.fn(); + toolkit.deploy = cdkDeployMock; + + await toolkit.watch({ selector: { patterns: [] }, hotswap: HotswapMode.FALL_BACK }); + fakeChokidarWatcherOn.readyCallback(); + + expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: HotswapMode.FALL_BACK })); + }); + + + test('respects HotswapMode.FULL_DEPLOYMENT', async () => { + cloudExecutable.configuration.settings.set(['watch'], {}); + const toolkit = defaultToolkitSetup(); + const cdkDeployMock = jest.fn(); + toolkit.deploy = cdkDeployMock; + + await toolkit.watch({ selector: { patterns: [] }, hotswap: HotswapMode.FULL_DEPLOYMENT }); + fakeChokidarWatcherOn.readyCallback(); + + expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: HotswapMode.FULL_DEPLOYMENT })); + }); + describe('with file change events', () => { let toolkit: CdkToolkit; let cdkDeployMock: jest.Mock; @@ -738,7 +840,10 @@ describe('watch', () => { toolkit = defaultToolkitSetup(); cdkDeployMock = jest.fn(); toolkit.deploy = cdkDeployMock; - await toolkit.watch({ selector: { patterns: [] } }); + await toolkit.watch({ + selector: { patterns: [] }, + hotswap: HotswapMode.HOTSWAP_ONLY, + }); }); test("does not trigger a 'deploy' before the 'ready' event has fired", async () => { diff --git a/packages/cdk-assets/lib/private/archive.ts b/packages/cdk-assets/lib/private/archive.ts index b54de7590f67b..5f7be17232859 100644 --- a/packages/cdk-assets/lib/private/archive.ts +++ b/packages/cdk-assets/lib/private/archive.ts @@ -1,4 +1,3 @@ -import { randomUUID } from 'crypto'; import { createWriteStream, promises as fs } from 'fs'; import * as path from 'path'; import * as glob from 'glob'; @@ -12,7 +11,7 @@ type Logger = (x: string) => void; export async function zipDirectory(directory: string, outputFile: string, logger: Logger): Promise { // We write to a temporary file and rename at the last moment. This is so that if we are // interrupted during this process, we don't leave a half-finished file in the target location. - const temporaryOutputFile = `${outputFile}.${randomUUID()}._tmp`; + const temporaryOutputFile = `${outputFile}.${randomString()}._tmp`; await writeZipFile(directory, temporaryOutputFile); await moveIntoPlace(temporaryOutputFile, outputFile, logger); } @@ -98,3 +97,7 @@ async function pathExists(x: string) { throw e; } } + +function randomString() { + return Math.random().toString(36).replace(/[^a-z0-9]+/g, ''); +} \ No newline at end of file diff --git a/packages/cdk-assets/lib/private/docker.ts b/packages/cdk-assets/lib/private/docker.ts index 4a812aba64a26..7cca00a2d0cbd 100644 --- a/packages/cdk-assets/lib/private/docker.ts +++ b/packages/cdk-assets/lib/private/docker.ts @@ -15,6 +15,7 @@ interface BuildOptions { readonly target?: string; readonly file?: string; readonly buildArgs?: Record; + readonly buildSecrets?: Record; readonly networkMode?: string; readonly platform?: string; readonly outputs?: string[]; @@ -54,6 +55,7 @@ export class Docker { const buildCommand = [ 'build', ...flatten(Object.entries(options.buildArgs || {}).map(([k, v]) => ['--build-arg', `${k}=${v}`])), + ...flatten(Object.entries(options.buildSecrets || {}).map(([k, v]) => ['--secret', `id=${k},${v}`])), '--tag', options.tag, ...options.target ? ['--target', options.target] : [], ...options.file ? ['--file', options.file] : [], diff --git a/packages/cdk-assets/lib/private/handlers/container-images.ts b/packages/cdk-assets/lib/private/handlers/container-images.ts index 750247f4bbab0..811d02e1a8d01 100644 --- a/packages/cdk-assets/lib/private/handlers/container-images.ts +++ b/packages/cdk-assets/lib/private/handlers/container-images.ts @@ -167,6 +167,7 @@ class ContainerImageBuilder { directory: fullPath, tag: localTagName, buildArgs: source.dockerBuildArgs, + buildSecrets: source.dockerBuildSecrets, target: source.dockerBuildTarget, file: source.dockerFile, networkMode: source.networkMode, diff --git a/tools/@aws-cdk/node-bundle/package.json b/tools/@aws-cdk/node-bundle/package.json index 1a7db6f2fbeb4..df3db5b616c8d 100644 --- a/tools/@aws-cdk/node-bundle/package.json +++ b/tools/@aws-cdk/node-bundle/package.json @@ -40,7 +40,7 @@ "jest-junit": "^13", "json-schema": "^0.4.0", "npm-check-updates": "^12", - "projen": "^0.67.24", + "projen": "^0.67.29", "standard-version": "^9", "ts-jest": "^27", "typescript": "^4.5.5" diff --git a/tools/@aws-cdk/ubergen/bin/ubergen.ts b/tools/@aws-cdk/ubergen/bin/ubergen.ts index 6937a1d3db1d4..e9f072ea42eb7 100644 --- a/tools/@aws-cdk/ubergen/bin/ubergen.ts +++ b/tools/@aws-cdk/ubergen/bin/ubergen.ts @@ -1,688 +1,10 @@ -import * as console from 'console'; -import * as path from 'path'; -import * as process from 'process'; -import cfn2ts from '@aws-cdk/cfn2ts'; -import * as pkglint from '@aws-cdk/pkglint'; -import * as awsCdkMigration from 'aws-cdk-migration'; -import * as fs from 'fs-extra'; - - -// The directory where our 'package.json' lives -const MONOPACKAGE_ROOT = process.cwd(); - -const ROOT_PATH = findWorkspacePath(); -const UBER_PACKAGE_JSON_PATH = path.join(MONOPACKAGE_ROOT, 'package.json'); - -const EXCLUDED_PACKAGES = ['@aws-cdk/example-construct-library']; - -async function main() { - console.log(`🌴 workspace root path is: ${ROOT_PATH}`); - const uberPackageJson = await fs.readJson(UBER_PACKAGE_JSON_PATH) as PackageJson; - const libraries = await findLibrariesToPackage(uberPackageJson); - await verifyDependencies(uberPackageJson, libraries); - await prepareSourceFiles(libraries, uberPackageJson); - await combineRosettaFixtures(libraries, uberPackageJson); - - // if explicitExports is set to `false`, remove the "exports" section from package.json - const explicitExports = uberPackageJson.ubergen?.explicitExports ?? true; - if (!explicitExports) { - delete uberPackageJson.exports; - } - - // Rewrite package.json (exports will have changed) - await fs.writeJson(UBER_PACKAGE_JSON_PATH, uberPackageJson, { spaces: 2 }); -} +import { main } from '../lib'; main().then( () => process.exit(0), (err) => { + // eslint-disable-next-line no-console console.error('❌ An error occurred: ', err.stack); process.exit(1); }, ); - -interface LibraryReference { - readonly packageJson: PackageJson; - readonly root: string; - readonly shortName: string; -} - -type Export = string | { - readonly types?: string; - readonly import?: string; - readonly require?: string; -}; - -interface PackageJson { - readonly main?: string; - readonly description?: string; - readonly bundleDependencies?: readonly string[]; - readonly bundledDependencies?: readonly string[]; - readonly dependencies?: { readonly [name: string]: string }; - readonly devDependencies?: { readonly [name: string]: string }; - readonly jsii: { - readonly targets?: { - readonly dotnet?: { - readonly namespace: string; - readonly [key: string]: unknown; - }, - readonly java?: { - readonly package: string; - readonly [key: string]: unknown; - }, - readonly python?: { - readonly module: string; - readonly [key: string]: unknown; - }, - readonly [language: string]: unknown, - }, - }; - readonly name: string; - readonly types: string; - readonly version: string; - readonly stability: string; - readonly [key: string]: unknown; - readonly 'cdk-build'?: { - readonly cloudformation: string[] | string; - }; - readonly ubergen?: { - readonly deprecatedPackages?: readonly string[]; - readonly excludeExperimentalModules?: boolean; - - /** - * The directory where we're going to collect all the libraries. - * - * @default - root of the ubergen package - */ - readonly libRoot?: string; - - /** - * Adds an `exports` section to the ubergen package.json file to ensure that - * consumers won't be able to accidentally import a private file. - * - * @default true - */ - readonly explicitExports?: boolean; - - /** - * An exports section that should be ignored for v1 but included for ubergen - */ - readonly exports?: Record; - }; - exports?: Record; -} - -/** - * Find the workspace root path. Walk up the directory tree until you find lerna.json - */ -function findWorkspacePath(): string { - - return _findRootPath(process.cwd()); - - function _findRootPath(part: string): string { - if (part === path.resolve(part, '..')) { - throw new Error('couldn\'t find a \'lerna.json\' file when walking up the directory tree, are you in a aws-cdk project?'); - } - - if (fs.existsSync(path.resolve(part, 'lerna.json'))) { - return part; - } - - return _findRootPath(path.resolve(part, '..')); - } -} - -async function findLibrariesToPackage(uberPackageJson: PackageJson): Promise { - console.log('🔍 Discovering libraries that need packaging...'); - - const deprecatedPackages = uberPackageJson.ubergen?.deprecatedPackages; - const result = new Array(); - const librariesRoot = path.resolve(ROOT_PATH, 'packages', '@aws-cdk'); - - for (const dir of await fs.readdir(librariesRoot)) { - const packageJson = await fs.readJson(path.resolve(librariesRoot, dir, 'package.json')); - - if (packageJson.ubergen?.exclude || EXCLUDED_PACKAGES.includes(packageJson.name)) { - console.log(`\t⚠️ Skipping (ubergen excluded): ${packageJson.name}`); - continue; - } else if (packageJson.jsii == null ) { - console.log(`\t⚠️ Skipping (not jsii-enabled): ${packageJson.name}`); - continue; - } else if (deprecatedPackages) { - if (deprecatedPackages.some(packageName => packageName === packageJson.name)) { - console.log(`\t⚠️ Skipping (ubergen deprecated): ${packageJson.name}`); - continue; - } - } else if (packageJson.deprecated) { - console.log(`\t⚠️ Skipping (deprecated): ${packageJson.name}`); - continue; - } - result.push({ - packageJson, - root: path.join(librariesRoot, dir), - shortName: packageJson.name.slice('@aws-cdk/'.length), - }); - } - - console.log(`\tℹ️ Found ${result.length} relevant packages!`); - - return result; -} - -async function verifyDependencies(packageJson: any, libraries: readonly LibraryReference[]): Promise { - console.log('🧐 Verifying dependencies are complete...'); - - let changed = false; - const toBundle: Record = {}; - - for (const library of libraries) { - for (const depName of library.packageJson.bundleDependencies ?? library.packageJson.bundledDependencies ?? []) { - const requiredVersion = library.packageJson.devDependencies?.[depName] - ?? library.packageJson.dependencies?.[depName] - ?? '*'; - if (toBundle[depName] != null && toBundle[depName] !== requiredVersion) { - throw new Error(`Required to bundle different versions of ${depName}: ${toBundle[depName]} and ${requiredVersion}.`); - } - toBundle[depName] = requiredVersion; - } - - if (library.packageJson.name in packageJson.devDependencies) { - const existingVersion = packageJson.devDependencies[library.packageJson.name]; - if (existingVersion !== library.packageJson.version) { - console.log(`\t⚠️ Incorrect dependency: ${library.packageJson.name} (expected ${library.packageJson.version}, found ${packageJson.devDependencies[library.packageJson.name]})`); - packageJson.devDependencies[library.packageJson.name] = library.packageJson.version; - changed = true; - } - continue; - } - console.log(`\t⚠️ Missing dependency: ${library.packageJson.name}`); - changed = true; - packageJson.devDependencies = sortObject({ - ...packageJson.devDependencies ?? {}, - [library.packageJson.name]: library.packageJson.version, - }); - } - const workspacePath = path.resolve(ROOT_PATH, 'package.json'); - const workspace = await fs.readJson(workspacePath); - let workspaceChanged = false; - - const spuriousBundledDeps = new Set(packageJson.bundledDependencies ?? []); - for (const [name, version] of Object.entries(toBundle)) { - spuriousBundledDeps.delete(name); - - const nohoist = `${packageJson.name}/${name}`; - if (!workspace.workspaces.nohoist?.includes(nohoist)) { - console.log(`\t⚠️ Missing yarn workspace nohoist: ${nohoist}`); - workspace.workspaces.nohoist = Array.from(new Set([ - ...workspace.workspaces.nohoist ?? [], - nohoist, - `${nohoist}/**`, - ])).sort(); - workspaceChanged = true; - } - - if (!(packageJson.bundledDependencies?.includes(name))) { - console.log(`\t⚠️ Missing bundled dependency: ${name} at ${version}`); - packageJson.bundledDependencies = [ - ...packageJson.bundledDependencies ?? [], - name, - ].sort(); - changed = true; - } - - if (packageJson.dependencies?.[name] !== version) { - console.log(`\t⚠️ Missing or incorrect dependency: ${name} at ${version}`); - packageJson.dependencies = sortObject({ - ...packageJson.dependencies ?? {}, - [name]: version, - }); - changed = true; - } - } - packageJson.bundledDependencies = packageJson.bundledDependencies?.filter((dep: string) => !spuriousBundledDeps.has(dep)); - for (const toRemove of Array.from(spuriousBundledDeps)) { - delete packageJson.dependencies[toRemove]; - changed = true; - } - - if (workspaceChanged) { - await fs.writeFile(workspacePath, JSON.stringify(workspace, null, 2) + '\n', { encoding: 'utf-8' }); - console.log('\t❌ Updated the yarn workspace configuration. Re-run "yarn install", and commit the changes.'); - } - - if (changed) { - await fs.writeFile(UBER_PACKAGE_JSON_PATH, JSON.stringify(packageJson, null, 2) + '\n', { encoding: 'utf8' }); - - throw new Error('Fixed dependency inconsistencies. Commit the updated package.json file.'); - } - console.log('\t✅ Dependencies are correct!'); -} - -async function prepareSourceFiles(libraries: readonly LibraryReference[], packageJson: PackageJson) { - console.log('📝 Preparing source files...'); - - if (packageJson.ubergen?.excludeExperimentalModules) { - console.log('\t 👩🏻‍🔬 \'excludeExperimentalModules\' enabled. Regenerating all experimental modules as L1s using cfn2ts...'); - } - - const libRoot = resolveLibRoot(packageJson); - - // Should not remove collection directory if we're currently in it. The OS would be unhappy. - if (libRoot !== process.cwd()) { - await fs.remove(libRoot); - } - - // Control 'exports' field of the 'package.json'. This will control what kind of 'import' statements are - // allowed for this package: we only want to allow the exact import statements that we want to support. - packageJson.exports = { - '.': { - types: './index.d.ts', - import: './index.js', - require: './lazy-index.js', - }, - - // We need to expose 'package.json' and '.jsii' because 'jsii' and 'jsii-reflect' load them using - // require(). (-_-). Can be removed after https://github.com/aws/jsii/pull/3205 gets merged. - './package.json': './package.json', - './.jsii': './.jsii', - - // This is necessary to support jsii cross-module warnings - './.warnings.jsii.js': './.warnings.jsii.js', - }; - - // We use the index.ts to compile type definitions. - // - // We build two indexes: one for eager loading (used by ESM modules), and one - // for lazy loading (used by CJS modules). The lazy loading will result in faster - // loading times, because we don't have to load and parse all submodules right away, - // but is not compatible with ESM's loading algorithm. - // - // This improves AWS CDK app performance by ~400ms. - const indexStatements = new Array(); - const lazyExports = new Array(); - - for (const library of libraries) { - const libDir = path.join(libRoot, library.shortName); - const copied = await transformPackage(library, packageJson, libDir, libraries); - - if (!copied) { - continue; - } - if (library.shortName === 'core') { - indexStatements.push(`export * from './${library.shortName}';`); - lazyExports.unshift(`export * from './${library.shortName}';`); - } else { - const exportName = library.shortName.replace(/-/g, '_'); - - indexStatements.push(`export * as ${exportName} from './${library.shortName}';`); - lazyExports.push(`Object.defineProperty(exports, '${exportName}', { get: function () { return require('./${library.shortName}'); } });`); - } - copySubmoduleExports(packageJson.exports, library, library.shortName); - } - - // make the exports.ts file pass linting - lazyExports.unshift('/* eslint-disable @typescript-eslint/no-require-imports */'); - - await fs.writeFile(path.join(libRoot, 'index.ts'), indexStatements.join('\n'), { encoding: 'utf8' }); - await fs.writeFile(path.join(libRoot, 'lazy-index.ts'), lazyExports.join('\n'), { encoding: 'utf8' }); - - console.log('\t🍺 Success!'); -} - -/** - * Copy the sublibrary's exports into the 'exports' of the main library. - * - * Replace the original 'main' export with an export of the new '/index.ts` file we've written - * in 'transformPackage'. - */ -function copySubmoduleExports(targetExports: Record, library: LibraryReference, subdirectory: string) { - const visibleName = library.shortName; - - // Do both REAL "exports" section, as well as virtual, ubergen-only "exports" section - for (const exportSet of [library.packageJson.exports, library.packageJson.ubergen?.exports]) { - for (const [relPath, relSource] of Object.entries(exportSet ?? {})) { - targetExports[`./${unixPath(path.join(visibleName, relPath))}`] = resolveExport(relSource); - } - } - - function resolveExport(exp: A): A { - if (typeof exp === 'string') { - return `./${unixPath(path.join(subdirectory, exp))}` as any; - } else { - return Object.fromEntries(Object.entries(exp).map(([k, v]) => [k, v ? resolveExport(v) : undefined])) as any; - } - } - - if (visibleName !== 'core') { - // If there was an export for '.' in the original submodule, this assignment will overwrite it, - // which is exactly what we want. - targetExports[`./${unixPath(visibleName)}`] = `./${unixPath(subdirectory)}/index.js`; - } -} - -async function combineRosettaFixtures(libraries: readonly LibraryReference[], uberPackageJson: PackageJson) { - console.log('📝 Combining Rosetta fixtures...'); - - const uberRosettaDir = path.resolve(MONOPACKAGE_ROOT, 'rosetta'); - await fs.remove(uberRosettaDir); - await fs.mkdir(uberRosettaDir); - - for (const library of libraries) { - const packageRosettaDir = path.join(library.root, 'rosetta'); - const uberRosettaTargetDir = library.shortName === 'core' ? uberRosettaDir : path.join(uberRosettaDir, library.shortName.replace(/-/g, '_')); - if (await fs.pathExists(packageRosettaDir)) { - if (!fs.existsSync(uberRosettaTargetDir)) { - await fs.mkdir(uberRosettaTargetDir); - } - const files = await fs.readdir(packageRosettaDir); - for (const file of files) { - await fs.writeFile( - path.join(uberRosettaTargetDir, file), - await rewriteRosettaFixtureImports( - path.join(packageRosettaDir, file), - uberPackageJson.name, - ), - { encoding: 'utf8' }, - ); - } - } - } - - console.log('\t🍺 Success!'); -} - -async function transformPackage( - library: LibraryReference, - uberPackageJson: PackageJson, - destination: string, - allLibraries: readonly LibraryReference[], -) { - await fs.mkdirp(destination); - - if (uberPackageJson.ubergen?.excludeExperimentalModules && library.packageJson.stability === 'experimental') { - // when stripExperimental is enabled, we only want to add the L1s of experimental modules. - let cfnScopes = library.packageJson['cdk-build']?.cloudformation; - - if (cfnScopes === undefined) { - return false; - } - cfnScopes = Array.isArray(cfnScopes) ? cfnScopes : [cfnScopes]; - - const destinationLib = path.join(destination, 'lib'); - await fs.mkdirp(destinationLib); - await cfn2ts(cfnScopes, destinationLib); - - // We know what this is going to be, so predict it - const alphaPackageName = hasL2s(library) ? `${library.packageJson.name}-alpha` : undefined; - - // create a lib/index.ts which only exports the generated files - fs.writeFileSync(path.join(destinationLib, 'index.ts'), - /// logic copied from `create-missing-libraries.ts` - cfnScopes.map(s => (s === 'AWS::Serverless' ? 'AWS::SAM' : s).split('::')[1].toLocaleLowerCase()) - .map(s => `export * from './${s}.generated';`) - .join('\n')); - await pkglint.createLibraryReadme(cfnScopes[0], path.join(destination, 'README.md'), alphaPackageName); - - await copyOrTransformFiles(destination, destination, allLibraries, uberPackageJson); - } else { - await copyOrTransformFiles(library.root, destination, allLibraries, uberPackageJson); - await copyLiterateSources(path.join(library.root, 'test'), path.join(destination, 'test'), allLibraries, uberPackageJson); - } - - await fs.writeFile( - path.join(destination, 'index.ts'), - `export * from './${library.packageJson.types.replace(/(\/index)?(\.d)?\.ts$/, '')}';\n`, - { encoding: 'utf8' }, - ); - - if (library.shortName !== 'core') { - const config = uberPackageJson.jsii.targets; - await fs.writeJson( - path.join(destination, '.jsiirc.json'), - { - targets: transformTargets(config, library.packageJson.jsii.targets), - }, - { spaces: 2 }, - ); - } - - // if libRoot is _not_ under the root of the package, generate a file at the - // root that will refer to the one under lib/ so that users can still import - // from "aws-cdk-lib/aws-lambda". - const relativeLibRoot = uberPackageJson.ubergen?.libRoot; - if (relativeLibRoot && relativeLibRoot !== '.') { - await fs.writeFile( - path.resolve(MONOPACKAGE_ROOT, `${library.shortName}.ts`), - `export * from './${relativeLibRoot}/${library.shortName}';\n`, - { encoding: 'utf8' }, - ); - } - - return true; -} - -/** - * Return whether a package has L2s - * - * We determine this on the cheap: the answer is yes if the package has - * any .ts files in the `lib` directory other than `index.ts` and `*.generated.ts`. - */ -function hasL2s(library: LibraryReference) { - try { - const sourceFiles = fs.readdirSync(path.join(library.root, 'lib')).filter(n => n.endsWith('.ts') && !n.endsWith('.d.ts')); - return sourceFiles.some(n => n !== 'index.ts' && !n.includes('.generated.')); - } catch (e) { - if (e.code === 'ENOENT') { return false; } - - throw e; - } -} - -function transformTargets(monoConfig: PackageJson['jsii']['targets'], targets: PackageJson['jsii']['targets']): PackageJson['jsii']['targets'] { - if (targets == null) { return targets; } - - const result: Record = {}; - for (const [language, config] of Object.entries(targets)) { - switch (language) { - case 'dotnet': - if (monoConfig?.dotnet != null) { - result[language] = { - namespace: (config as any).namespace, - }; - } - break; - case 'java': - if (monoConfig?.java != null) { - result[language] = { - package: (config as any).package, - }; - } - break; - case 'python': - if (monoConfig?.python != null) { - result[language] = { - module: `${monoConfig.python.module}.${(config as any).module.replace(/^aws_cdk\./, '')}`, - }; - } - break; - default: - throw new Error(`Unsupported language for submodule configuration translation: ${language}`); - } - } - - return result; -} - -async function copyOrTransformFiles(from: string, to: string, libraries: readonly LibraryReference[], uberPackageJson: PackageJson) { - const libRoot = resolveLibRoot(uberPackageJson); - const promises = (await fs.readdir(from)).map(async name => { - if (shouldIgnoreFile(name)) { return; } - - if (name.endsWith('.d.ts') || name.endsWith('.js')) { - if (await fs.pathExists(path.join(from, name.replace(/\.(d\.ts|js)$/, '.ts')))) { - // We won't copy .d.ts and .js files with a corresponding .ts file - return; - } - } - - const source = path.join(from, name); - const destination = path.join(to, name); - - const stat = await fs.stat(source); - if (stat.isDirectory()) { - await fs.mkdirp(destination); - return copyOrTransformFiles(source, destination, libraries, uberPackageJson); - } - - if (name.endsWith('.ts')) { - return fs.writeFile( - destination, - await rewriteLibraryImports(source, to, libRoot, libraries), - { encoding: 'utf8' }, - ); - } else if (name === 'cfn-types-2-classes.json') { - // This is a special file used by the cloudformation-include module that contains mappings - // of CFN resource types to the fully-qualified class names of the CDK L1 classes. - // We need to rewrite it to refer to the uberpackage instead of the individual packages - const cfnTypes2Classes: { [key: string]: string } = await fs.readJson(source); - for (const cfnType of Object.keys(cfnTypes2Classes)) { - const fqn = cfnTypes2Classes[cfnType]; - // replace @aws-cdk/aws- with /aws-, - // except for @aws-cdk/core, which maps just to the name of the uberpackage - cfnTypes2Classes[cfnType] = fqn.startsWith('@aws-cdk/core.') - ? fqn.replace('@aws-cdk/core', uberPackageJson.name) - : fqn.replace('@aws-cdk', uberPackageJson.name); - } - await fs.writeJson(destination, cfnTypes2Classes, { spaces: 2 }); - } else if (name === 'README.md') { - // Rewrite the README to both adjust imports and remove the redundant stability banner. - // (All modules included in ubergen-ed packages must be stable, so the banner is unnecessary.) - const newReadme = (await rewriteReadmeImports(source, uberPackageJson.name)) - .replace(/[\s\S]+/gm, ''); - - return fs.writeFile( - destination, - newReadme, - { encoding: 'utf8' }, - ); - } else { - return fs.copyFile(source, destination); - } - }); - - await Promise.all(promises); -} - -async function copyLiterateSources(from: string, to: string, libraries: readonly LibraryReference[], uberPackageJson: PackageJson) { - const libRoot = resolveLibRoot(uberPackageJson); - await Promise.all((await fs.readdir(from)).flatMap(async name => { - const source = path.join(from, name); - const stat = await fs.stat(source); - - if (stat.isDirectory()) { - await copyLiterateSources(source, path.join(to, name), libraries, uberPackageJson); - return; - } - - if (!name.endsWith('.lit.ts')) { - return []; - } - - await fs.mkdirp(to); - - return fs.writeFile( - path.join(to, name), - await rewriteLibraryImports(path.join(from, name), to, libRoot, libraries), - { encoding: 'utf8' }, - ); - })); -} - -/** - * Rewrites the imports in README.md from v1 ('@aws-cdk') to v2 ('aws-cdk-lib'). - */ -async function rewriteReadmeImports(fromFile: string, libName: string): Promise { - const sourceCode = await fs.readFile(fromFile, { encoding: 'utf8' }); - return awsCdkMigration.rewriteReadmeImports(sourceCode, libName); -} - -/** - * Rewrites imports in libaries, using the relative path (i.e. '../../assertions'). - */ -async function rewriteLibraryImports(fromFile: string, targetDir: string, libRoot: string, libraries: readonly LibraryReference[]): Promise { - const source = await fs.readFile(fromFile, { encoding: 'utf8' }); - return awsCdkMigration.rewriteImports(source, relativeImport); - - function relativeImport(modulePath: string): string | undefined { - const sourceLibrary = libraries.find( - lib => - modulePath === lib.packageJson.name || - modulePath.startsWith(`${lib.packageJson.name}/`), - ); - if (sourceLibrary == null) { return undefined; } - - const importedFile = modulePath === sourceLibrary.packageJson.name - ? path.join(libRoot, sourceLibrary.shortName) - : path.join(libRoot, sourceLibrary.shortName, modulePath.slice(sourceLibrary.packageJson.name.length + 1)); - - return path.relative(targetDir, importedFile); - } -} - -/** - * Rewrites imports in rosetta fixtures, using the external path (i.e. 'aws-cdk-lib/assertions'). - */ -async function rewriteRosettaFixtureImports(fromFile: string, libName: string): Promise { - const source = await fs.readFile(fromFile, { encoding: 'utf8' }); - return awsCdkMigration.rewriteMonoPackageImports(source, libName); -} - -const IGNORED_FILE_NAMES = new Set([ - '.eslintrc.js', - '.gitignore', - '.jest.config.js', - '.jsii', - '.npmignore', - 'node_modules', - 'package.json', - 'test', - 'tsconfig.json', - 'tsconfig.tsbuildinfo', - 'LICENSE', - 'NOTICE', -]); - -function shouldIgnoreFile(name: string): boolean { - return IGNORED_FILE_NAMES.has(name); -} - -function sortObject(obj: Record): Record { - const result: Record = {}; - - for (const [key, value] of Object.entries(obj).sort((l, r) => l[0].localeCompare(r[0]))) { - result[key] = value; - } - - return result; -} - -/** - * Turn potential backslashes into forward slashes - */ -function unixPath(x: string) { - return x.replace(/\\/g, '/'); -} - -/** - * Resolves the directory where we're going to collect all the libraries. - * - * By default, this is purposely the same as the monopackage root so that our - * two import styles resolve to the same files but it can be overridden by - * seeting `ubergen.libRoot` in the package.json of the uber package. - * - * @param uberPackageJson package.json contents of the uber package - * @returns The directory where we should collect all the libraries. - */ -function resolveLibRoot(uberPackageJson: PackageJson): string { - return path.resolve(uberPackageJson.ubergen?.libRoot ?? MONOPACKAGE_ROOT); -} diff --git a/tools/@aws-cdk/ubergen/lib/index.ts b/tools/@aws-cdk/ubergen/lib/index.ts new file mode 100644 index 0000000000000..71882a928ca7a --- /dev/null +++ b/tools/@aws-cdk/ubergen/lib/index.ts @@ -0,0 +1,679 @@ +import * as console from 'console'; +import * as path from 'path'; +import * as process from 'process'; +import cfn2ts from '@aws-cdk/cfn2ts'; +import * as pkglint from '@aws-cdk/pkglint'; +import * as awsCdkMigration from 'aws-cdk-migration'; +import * as fs from 'fs-extra'; + +// The directory where our 'package.json' lives +const MONOPACKAGE_ROOT = process.cwd(); + +const ROOT_PATH = findWorkspacePath(); +const UBER_PACKAGE_JSON_PATH = path.join(MONOPACKAGE_ROOT, 'package.json'); + +const EXCLUDED_PACKAGES = ['@aws-cdk/example-construct-library']; + +export async function main() { + console.log(`🌴 workspace root path is: ${ROOT_PATH}`); + const uberPackageJson = await fs.readJson(UBER_PACKAGE_JSON_PATH) as PackageJson; + const libraries = await findLibrariesToPackage(uberPackageJson); + await verifyDependencies(uberPackageJson, libraries); + await prepareSourceFiles(libraries, uberPackageJson); + await combineRosettaFixtures(libraries, uberPackageJson); + + // if explicitExports is set to `false`, remove the "exports" section from package.json + const explicitExports = uberPackageJson.ubergen?.explicitExports ?? true; + if (!explicitExports) { + delete uberPackageJson.exports; + } + + // Rewrite package.json (exports will have changed) + await fs.writeJson(UBER_PACKAGE_JSON_PATH, uberPackageJson, { spaces: 2 }); +} + +interface LibraryReference { + readonly packageJson: PackageJson; + readonly root: string; + readonly shortName: string; +} + +type Export = string | { + readonly types?: string; + readonly import?: string; + readonly require?: string; +}; + +interface PackageJson { + readonly main?: string; + readonly description?: string; + readonly bundleDependencies?: readonly string[]; + readonly bundledDependencies?: readonly string[]; + readonly dependencies?: { readonly [name: string]: string }; + readonly devDependencies?: { readonly [name: string]: string }; + readonly jsii: { + readonly targets?: { + readonly dotnet?: { + readonly namespace: string; + readonly [key: string]: unknown; + }, + readonly java?: { + readonly package: string; + readonly [key: string]: unknown; + }, + readonly python?: { + readonly module: string; + readonly [key: string]: unknown; + }, + readonly [language: string]: unknown, + }, + }; + readonly name: string; + readonly types: string; + readonly version: string; + readonly stability: string; + readonly [key: string]: unknown; + readonly 'cdk-build'?: { + readonly cloudformation: string[] | string; + }; + readonly ubergen?: { + readonly deprecatedPackages?: readonly string[]; + readonly excludeExperimentalModules?: boolean; + + /** + * The directory where we're going to collect all the libraries. + * + * @default - root of the ubergen package + */ + readonly libRoot?: string; + + /** + * Adds an `exports` section to the ubergen package.json file to ensure that + * consumers won't be able to accidentally import a private file. + * + * @default true + */ + readonly explicitExports?: boolean; + + /** + * An exports section that should be ignored for v1 but included for ubergen + */ + readonly exports?: Record; + }; + exports?: Record; +} + +/** + * Find the workspace root path. Walk up the directory tree until you find lerna.json + */ +function findWorkspacePath(): string { + + return _findRootPath(process.cwd()); + + function _findRootPath(part: string): string { + if (part === path.resolve(part, '..')) { + throw new Error('couldn\'t find a \'lerna.json\' file when walking up the directory tree, are you in a aws-cdk project?'); + } + + if (fs.existsSync(path.resolve(part, 'lerna.json'))) { + return part; + } + + return _findRootPath(path.resolve(part, '..')); + } +} + +async function findLibrariesToPackage(uberPackageJson: PackageJson): Promise { + console.log('🔍 Discovering libraries that need packaging...'); + + const deprecatedPackages = uberPackageJson.ubergen?.deprecatedPackages; + const result = new Array(); + const librariesRoot = path.resolve(ROOT_PATH, 'packages', '@aws-cdk'); + + for (const dir of await fs.readdir(librariesRoot)) { + const packageJson = await fs.readJson(path.resolve(librariesRoot, dir, 'package.json')); + + if (packageJson.ubergen?.exclude || EXCLUDED_PACKAGES.includes(packageJson.name)) { + console.log(`\t⚠️ Skipping (ubergen excluded): ${packageJson.name}`); + continue; + } else if (packageJson.jsii == null ) { + console.log(`\t⚠️ Skipping (not jsii-enabled): ${packageJson.name}`); + continue; + } else if (deprecatedPackages) { + if (deprecatedPackages.some(packageName => packageName === packageJson.name)) { + console.log(`\t⚠️ Skipping (ubergen deprecated): ${packageJson.name}`); + continue; + } + } else if (packageJson.deprecated) { + console.log(`\t⚠️ Skipping (deprecated): ${packageJson.name}`); + continue; + } + result.push({ + packageJson, + root: path.join(librariesRoot, dir), + shortName: packageJson.name.slice('@aws-cdk/'.length), + }); + } + + console.log(`\tℹ️ Found ${result.length} relevant packages!`); + + return result; +} + +async function verifyDependencies(packageJson: any, libraries: readonly LibraryReference[]): Promise { + console.log('🧐 Verifying dependencies are complete...'); + + let changed = false; + const toBundle: Record = {}; + + for (const library of libraries) { + for (const depName of library.packageJson.bundleDependencies ?? library.packageJson.bundledDependencies ?? []) { + const requiredVersion = library.packageJson.devDependencies?.[depName] + ?? library.packageJson.dependencies?.[depName] + ?? '*'; + if (toBundle[depName] != null && toBundle[depName] !== requiredVersion) { + throw new Error(`Required to bundle different versions of ${depName}: ${toBundle[depName]} and ${requiredVersion}.`); + } + toBundle[depName] = requiredVersion; + } + + if (library.packageJson.name in packageJson.devDependencies) { + const existingVersion = packageJson.devDependencies[library.packageJson.name]; + if (existingVersion !== library.packageJson.version) { + console.log(`\t⚠️ Incorrect dependency: ${library.packageJson.name} (expected ${library.packageJson.version}, found ${packageJson.devDependencies[library.packageJson.name]})`); + packageJson.devDependencies[library.packageJson.name] = library.packageJson.version; + changed = true; + } + continue; + } + console.log(`\t⚠️ Missing dependency: ${library.packageJson.name}`); + changed = true; + packageJson.devDependencies = sortObject({ + ...packageJson.devDependencies ?? {}, + [library.packageJson.name]: library.packageJson.version, + }); + } + const workspacePath = path.resolve(ROOT_PATH, 'package.json'); + const workspace = await fs.readJson(workspacePath); + let workspaceChanged = false; + + const spuriousBundledDeps = new Set(packageJson.bundledDependencies ?? []); + for (const [name, version] of Object.entries(toBundle)) { + spuriousBundledDeps.delete(name); + + const nohoist = `${packageJson.name}/${name}`; + if (!workspace.workspaces.nohoist?.includes(nohoist)) { + console.log(`\t⚠️ Missing yarn workspace nohoist: ${nohoist}`); + workspace.workspaces.nohoist = Array.from(new Set([ + ...workspace.workspaces.nohoist ?? [], + nohoist, + `${nohoist}/**`, + ])).sort(); + workspaceChanged = true; + } + + if (!(packageJson.bundledDependencies?.includes(name))) { + console.log(`\t⚠️ Missing bundled dependency: ${name} at ${version}`); + packageJson.bundledDependencies = [ + ...packageJson.bundledDependencies ?? [], + name, + ].sort(); + changed = true; + } + + if (packageJson.dependencies?.[name] !== version) { + console.log(`\t⚠️ Missing or incorrect dependency: ${name} at ${version}`); + packageJson.dependencies = sortObject({ + ...packageJson.dependencies ?? {}, + [name]: version, + }); + changed = true; + } + } + packageJson.bundledDependencies = packageJson.bundledDependencies?.filter((dep: string) => !spuriousBundledDeps.has(dep)); + for (const toRemove of Array.from(spuriousBundledDeps)) { + delete packageJson.dependencies[toRemove]; + changed = true; + } + + if (workspaceChanged) { + await fs.writeFile(workspacePath, JSON.stringify(workspace, null, 2) + '\n', { encoding: 'utf-8' }); + console.log('\t❌ Updated the yarn workspace configuration. Re-run "yarn install", and commit the changes.'); + } + + if (changed) { + await fs.writeFile(UBER_PACKAGE_JSON_PATH, JSON.stringify(packageJson, null, 2) + '\n', { encoding: 'utf8' }); + + throw new Error('Fixed dependency inconsistencies. Commit the updated package.json file.'); + } + console.log('\t✅ Dependencies are correct!'); +} + +async function prepareSourceFiles(libraries: readonly LibraryReference[], packageJson: PackageJson) { + console.log('📝 Preparing source files...'); + + if (packageJson.ubergen?.excludeExperimentalModules) { + console.log('\t 👩🏻‍🔬 \'excludeExperimentalModules\' enabled. Regenerating all experimental modules as L1s using cfn2ts...'); + } + + const libRoot = resolveLibRoot(packageJson); + + // Should not remove collection directory if we're currently in it. The OS would be unhappy. + if (libRoot !== process.cwd()) { + await fs.remove(libRoot); + } + + // Control 'exports' field of the 'package.json'. This will control what kind of 'import' statements are + // allowed for this package: we only want to allow the exact import statements that we want to support. + packageJson.exports = { + '.': { + types: './index.d.ts', + import: './index.js', + require: './lazy-index.js', + }, + + // We need to expose 'package.json' and '.jsii' because 'jsii' and 'jsii-reflect' load them using + // require(). (-_-). Can be removed after https://github.com/aws/jsii/pull/3205 gets merged. + './package.json': './package.json', + './.jsii': './.jsii', + + // This is necessary to support jsii cross-module warnings + './.warnings.jsii.js': './.warnings.jsii.js', + }; + + // We use the index.ts to compile type definitions. + // + // We build two indexes: one for eager loading (used by ESM modules), and one + // for lazy loading (used by CJS modules). The lazy loading will result in faster + // loading times, because we don't have to load and parse all submodules right away, + // but is not compatible with ESM's loading algorithm. + // + // This improves AWS CDK app performance by ~400ms. + const indexStatements = new Array(); + const lazyExports = new Array(); + + for (const library of libraries) { + const libDir = path.join(libRoot, library.shortName); + const copied = await transformPackage(library, packageJson, libDir, libraries); + + if (!copied) { + continue; + } + if (library.shortName === 'core') { + indexStatements.push(`export * from './${library.shortName}';`); + lazyExports.unshift(`export * from './${library.shortName}';`); + } else { + const exportName = library.shortName.replace(/-/g, '_'); + + indexStatements.push(`export * as ${exportName} from './${library.shortName}';`); + lazyExports.push(`Object.defineProperty(exports, '${exportName}', { get: function () { return require('./${library.shortName}'); } });`); + } + copySubmoduleExports(packageJson.exports, library, library.shortName); + } + + // make the exports.ts file pass linting + lazyExports.unshift('/* eslint-disable @typescript-eslint/no-require-imports */'); + + await fs.writeFile(path.join(libRoot, 'index.ts'), indexStatements.join('\n'), { encoding: 'utf8' }); + await fs.writeFile(path.join(libRoot, 'lazy-index.ts'), lazyExports.join('\n'), { encoding: 'utf8' }); + + console.log('\t🍺 Success!'); +} + +/** + * Copy the sublibrary's exports into the 'exports' of the main library. + * + * Replace the original 'main' export with an export of the new '/index.ts` file we've written + * in 'transformPackage'. + */ +function copySubmoduleExports(targetExports: Record, library: LibraryReference, subdirectory: string) { + const visibleName = library.shortName; + + // Do both REAL "exports" section, as well as virtual, ubergen-only "exports" section + for (const exportSet of [library.packageJson.exports, library.packageJson.ubergen?.exports]) { + for (const [relPath, relSource] of Object.entries(exportSet ?? {})) { + targetExports[`./${unixPath(path.join(visibleName, relPath))}`] = resolveExport(relSource); + } + } + + function resolveExport(exp: A): A { + if (typeof exp === 'string') { + return `./${unixPath(path.join(subdirectory, exp))}` as any; + } else { + return Object.fromEntries(Object.entries(exp).map(([k, v]) => [k, v ? resolveExport(v) : undefined])) as any; + } + } + + if (visibleName !== 'core') { + // If there was an export for '.' in the original submodule, this assignment will overwrite it, + // which is exactly what we want. + targetExports[`./${unixPath(visibleName)}`] = `./${unixPath(subdirectory)}/index.js`; + } +} + +async function combineRosettaFixtures(libraries: readonly LibraryReference[], uberPackageJson: PackageJson) { + console.log('📝 Combining Rosetta fixtures...'); + + const uberRosettaDir = path.resolve(MONOPACKAGE_ROOT, 'rosetta'); + await fs.remove(uberRosettaDir); + await fs.mkdir(uberRosettaDir); + + for (const library of libraries) { + const packageRosettaDir = path.join(library.root, 'rosetta'); + const uberRosettaTargetDir = library.shortName === 'core' ? uberRosettaDir : path.join(uberRosettaDir, library.shortName.replace(/-/g, '_')); + if (await fs.pathExists(packageRosettaDir)) { + if (!fs.existsSync(uberRosettaTargetDir)) { + await fs.mkdir(uberRosettaTargetDir); + } + const files = await fs.readdir(packageRosettaDir); + for (const file of files) { + await fs.writeFile( + path.join(uberRosettaTargetDir, file), + await rewriteRosettaFixtureImports( + path.join(packageRosettaDir, file), + uberPackageJson.name, + ), + { encoding: 'utf8' }, + ); + } + } + } + + console.log('\t🍺 Success!'); +} + +async function transformPackage( + library: LibraryReference, + uberPackageJson: PackageJson, + destination: string, + allLibraries: readonly LibraryReference[], +) { + await fs.mkdirp(destination); + + if (uberPackageJson.ubergen?.excludeExperimentalModules && library.packageJson.stability === 'experimental') { + // when stripExperimental is enabled, we only want to add the L1s of experimental modules. + let cfnScopes = library.packageJson['cdk-build']?.cloudformation; + + if (cfnScopes === undefined) { + return false; + } + cfnScopes = Array.isArray(cfnScopes) ? cfnScopes : [cfnScopes]; + + const destinationLib = path.join(destination, 'lib'); + await fs.mkdirp(destinationLib); + await cfn2ts(cfnScopes, destinationLib); + + // We know what this is going to be, so predict it + const alphaPackageName = hasL2s(library) ? `${library.packageJson.name}-alpha` : undefined; + + // create a lib/index.ts which only exports the generated files + fs.writeFileSync(path.join(destinationLib, 'index.ts'), + /// logic copied from `create-missing-libraries.ts` + cfnScopes.map(s => (s === 'AWS::Serverless' ? 'AWS::SAM' : s).split('::')[1].toLocaleLowerCase()) + .map(s => `export * from './${s}.generated';`) + .join('\n')); + await pkglint.createLibraryReadme(cfnScopes[0], path.join(destination, 'README.md'), alphaPackageName); + + await copyOrTransformFiles(destination, destination, allLibraries, uberPackageJson); + } else { + await copyOrTransformFiles(library.root, destination, allLibraries, uberPackageJson); + await copyLiterateSources(path.join(library.root, 'test'), path.join(destination, 'test'), allLibraries, uberPackageJson); + } + + await fs.writeFile( + path.join(destination, 'index.ts'), + `export * from './${library.packageJson.types.replace(/(\/index)?(\.d)?\.ts$/, '')}';\n`, + { encoding: 'utf8' }, + ); + + if (library.shortName !== 'core') { + const config = uberPackageJson.jsii.targets; + await fs.writeJson( + path.join(destination, '.jsiirc.json'), + { + targets: transformTargets(config, library.packageJson.jsii.targets), + }, + { spaces: 2 }, + ); + } + + // if libRoot is _not_ under the root of the package, generate a file at the + // root that will refer to the one under lib/ so that users can still import + // from "aws-cdk-lib/aws-lambda". + const relativeLibRoot = uberPackageJson.ubergen?.libRoot; + if (relativeLibRoot && relativeLibRoot !== '.') { + await fs.writeFile( + path.resolve(MONOPACKAGE_ROOT, `${library.shortName}.ts`), + `export * from './${relativeLibRoot}/${library.shortName}';\n`, + { encoding: 'utf8' }, + ); + } + + return true; +} + +/** + * Return whether a package has L2s + * + * We determine this on the cheap: the answer is yes if the package has + * any .ts files in the `lib` directory other than `index.ts` and `*.generated.ts`. + */ +function hasL2s(library: LibraryReference) { + try { + const sourceFiles = fs.readdirSync(path.join(library.root, 'lib')).filter(n => n.endsWith('.ts') && !n.endsWith('.d.ts')); + return sourceFiles.some(n => n !== 'index.ts' && !n.includes('.generated.')); + } catch (e) { + if (e.code === 'ENOENT') { return false; } + + throw e; + } +} + +function transformTargets(monoConfig: PackageJson['jsii']['targets'], targets: PackageJson['jsii']['targets']): PackageJson['jsii']['targets'] { + if (targets == null) { return targets; } + + const result: Record = {}; + for (const [language, config] of Object.entries(targets)) { + switch (language) { + case 'dotnet': + if (monoConfig?.dotnet != null) { + result[language] = { + namespace: (config as any).namespace, + }; + } + break; + case 'java': + if (monoConfig?.java != null) { + result[language] = { + package: (config as any).package, + }; + } + break; + case 'python': + if (monoConfig?.python != null) { + result[language] = { + module: `${monoConfig.python.module}.${(config as any).module.replace(/^aws_cdk\./, '')}`, + }; + } + break; + default: + throw new Error(`Unsupported language for submodule configuration translation: ${language}`); + } + } + + return result; +} + +async function copyOrTransformFiles(from: string, to: string, libraries: readonly LibraryReference[], uberPackageJson: PackageJson) { + const libRoot = resolveLibRoot(uberPackageJson); + const promises = (await fs.readdir(from)).map(async name => { + if (shouldIgnoreFile(name)) { return; } + + if (name.endsWith('.d.ts') || name.endsWith('.js')) { + if (await fs.pathExists(path.join(from, name.replace(/\.(d\.ts|js)$/, '.ts')))) { + // We won't copy .d.ts and .js files with a corresponding .ts file + return; + } + } + + const source = path.join(from, name); + const destination = path.join(to, name); + + const stat = await fs.stat(source); + if (stat.isDirectory()) { + await fs.mkdirp(destination); + return copyOrTransformFiles(source, destination, libraries, uberPackageJson); + } + + if (name.endsWith('.ts')) { + return fs.writeFile( + destination, + await rewriteLibraryImports(source, to, libRoot, libraries), + { encoding: 'utf8' }, + ); + } else if (name === 'cfn-types-2-classes.json') { + // This is a special file used by the cloudformation-include module that contains mappings + // of CFN resource types to the fully-qualified class names of the CDK L1 classes. + // We need to rewrite it to refer to the uberpackage instead of the individual packages + const cfnTypes2Classes: { [key: string]: string } = await fs.readJson(source); + for (const cfnType of Object.keys(cfnTypes2Classes)) { + const fqn = cfnTypes2Classes[cfnType]; + // replace @aws-cdk/aws- with /aws-, + // except for @aws-cdk/core, which maps just to the name of the uberpackage + cfnTypes2Classes[cfnType] = fqn.startsWith('@aws-cdk/core.') + ? fqn.replace('@aws-cdk/core', uberPackageJson.name) + : fqn.replace('@aws-cdk', uberPackageJson.name); + } + await fs.writeJson(destination, cfnTypes2Classes, { spaces: 2 }); + } else if (name === 'README.md') { + // Rewrite the README to both adjust imports and remove the redundant stability banner. + // (All modules included in ubergen-ed packages must be stable, so the banner is unnecessary.) + const newReadme = (await rewriteReadmeImports(source, uberPackageJson.name)) + .replace(/[\s\S]+/gm, ''); + + return fs.writeFile( + destination, + newReadme, + { encoding: 'utf8' }, + ); + } else { + return fs.copyFile(source, destination); + } + }); + + await Promise.all(promises); +} + +async function copyLiterateSources(from: string, to: string, libraries: readonly LibraryReference[], uberPackageJson: PackageJson) { + const libRoot = resolveLibRoot(uberPackageJson); + await Promise.all((await fs.readdir(from)).flatMap(async name => { + const source = path.join(from, name); + const stat = await fs.stat(source); + + if (stat.isDirectory()) { + await copyLiterateSources(source, path.join(to, name), libraries, uberPackageJson); + return; + } + + if (!name.endsWith('.lit.ts')) { + return []; + } + + await fs.mkdirp(to); + + return fs.writeFile( + path.join(to, name), + await rewriteLibraryImports(path.join(from, name), to, libRoot, libraries), + { encoding: 'utf8' }, + ); + })); +} + +/** + * Rewrites the imports in README.md from v1 ('@aws-cdk') to v2 ('aws-cdk-lib'). + */ +async function rewriteReadmeImports(fromFile: string, libName: string): Promise { + const sourceCode = await fs.readFile(fromFile, { encoding: 'utf8' }); + return awsCdkMigration.rewriteReadmeImports(sourceCode, libName); +} + +/** + * Rewrites imports in libaries, using the relative path (i.e. '../../assertions'). + */ +async function rewriteLibraryImports(fromFile: string, targetDir: string, libRoot: string, libraries: readonly LibraryReference[]): Promise { + const source = await fs.readFile(fromFile, { encoding: 'utf8' }); + return awsCdkMigration.rewriteImports(source, relativeImport); + + function relativeImport(modulePath: string): string | undefined { + const sourceLibrary = libraries.find( + lib => + modulePath === lib.packageJson.name || + modulePath.startsWith(`${lib.packageJson.name}/`), + ); + if (sourceLibrary == null) { return undefined; } + + const importedFile = modulePath === sourceLibrary.packageJson.name + ? path.join(libRoot, sourceLibrary.shortName) + : path.join(libRoot, sourceLibrary.shortName, modulePath.slice(sourceLibrary.packageJson.name.length + 1)); + + return path.relative(targetDir, importedFile); + } +} + +/** + * Rewrites imports in rosetta fixtures, using the external path (i.e. 'aws-cdk-lib/assertions'). + */ +async function rewriteRosettaFixtureImports(fromFile: string, libName: string): Promise { + const source = await fs.readFile(fromFile, { encoding: 'utf8' }); + return awsCdkMigration.rewriteMonoPackageImports(source, libName); +} + +const IGNORED_FILE_NAMES = new Set([ + '.eslintrc.js', + '.gitignore', + '.jest.config.js', + '.jsii', + '.npmignore', + 'node_modules', + 'package.json', + 'test', + 'tsconfig.json', + 'tsconfig.tsbuildinfo', + 'LICENSE', + 'NOTICE', +]); + +function shouldIgnoreFile(name: string): boolean { + return IGNORED_FILE_NAMES.has(name); +} + +function sortObject(obj: Record): Record { + const result: Record = {}; + + for (const [key, value] of Object.entries(obj).sort((l, r) => l[0].localeCompare(r[0]))) { + result[key] = value; + } + + return result; +} + +/** + * Turn potential backslashes into forward slashes + */ +function unixPath(x: string) { + return x.replace(/\\/g, '/'); +} + +/** + * Resolves the directory where we're going to collect all the libraries. + * + * By default, this is purposely the same as the monopackage root so that our + * two import styles resolve to the same files but it can be overridden by + * seeting `ubergen.libRoot` in the package.json of the uber package. + * + * @param uberPackageJson package.json contents of the uber package + * @returns The directory where we should collect all the libraries. + */ +function resolveLibRoot(uberPackageJson: PackageJson): string { + return path.resolve(uberPackageJson.ubergen?.libRoot ?? MONOPACKAGE_ROOT); +} diff --git a/tools/@aws-cdk/ubergen/package.json b/tools/@aws-cdk/ubergen/package.json index ba97f42942f31..ce1e199b90ac9 100644 --- a/tools/@aws-cdk/ubergen/package.json +++ b/tools/@aws-cdk/ubergen/package.json @@ -3,6 +3,7 @@ "private": true, "version": "0.0.0", "description": "Generate an uber CDK package from all individual CDK construct libraries", + "main": "lib/index.js", "repository": { "type": "git", "url": "https://github.com/aws/aws-cdk.git", diff --git a/version.v2.json b/version.v2.json index ec4a15e30d957..b3cacf4bada01 100644 --- a/version.v2.json +++ b/version.v2.json @@ -1,4 +1,4 @@ { - "version": "2.63.0", - "alphaVersion": "2.63.0-alpha.0" + "version": "2.64.0", + "alphaVersion": "2.64.0-alpha.0" } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index a3e665f379dd0..67a75c52413e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -50,10 +50,10 @@ resolved "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v5/-/asset-node-proxy-agent-v5-2.0.42.tgz#e0833c219ba866eb2232a63dfc96c2a6db2f7394" integrity sha512-PxvP1UU2xa4k3Ea78DxAYY8ADvwWZ/nPu+xsjQLsT+MP+aB3RZ3pGc/fNlH7Rg56Zyb/j3GSdihAy4Oi5xa+TQ== -"@aws-cdk/lambda-layer-kubectl-v24@^2.0.83": - version "2.0.83" - resolved "https://registry.npmjs.org/@aws-cdk/lambda-layer-kubectl-v24/-/lambda-layer-kubectl-v24-2.0.83.tgz#0997586dd2fe4c506c10dfe723adaf2296b0c719" - integrity sha512-+TVKuQ2H0HF+Wzq2slKACpnLVF6uSh4qjIyI7NmbP373oYSd/cFPwd7FCwawTuJ7KSTiwUkj/qS6st2elFVTmg== +"@aws-cdk/lambda-layer-kubectl-v24@^2.0.85": + version "2.0.85" + resolved "https://registry.npmjs.org/@aws-cdk/lambda-layer-kubectl-v24/-/lambda-layer-kubectl-v24-2.0.85.tgz#a2620780c1879be5014174af5731ce931a1a6435" + integrity sha512-zvpyShtM26GH3COquqJ4L877fp0YrrD4cTyx9pQOQlqS44mmd20SYSBsMTVDDL2a9FEQoaZwhTQ2wrsiLE85vw== "@babel/code-frame@7.12.11": version "7.12.11" @@ -2341,14 +2341,15 @@ tsutils "^3.21.0" "@typescript-eslint/eslint-plugin@^5": - version "5.49.0" - resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.49.0.tgz#d0b4556f0792194bf0c2fb297897efa321492389" - integrity sha512-IhxabIpcf++TBaBa1h7jtOWyon80SXPRLDq0dVz5SLFC/eW6tofkw/O7Ar3lkx5z5U6wzbKDrl2larprp5kk5Q== + version "5.50.0" + resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.50.0.tgz#fb48c31cadc853ffc1dc35373f56b5e2a8908fe9" + integrity sha512-vwksQWSFZiUhgq3Kv7o1Jcj0DUNylwnIlGvKvLLYsq8pAWha6/WCnXUeaSoNNha/K7QSf2+jvmkxggC1u3pIwQ== dependencies: - "@typescript-eslint/scope-manager" "5.49.0" - "@typescript-eslint/type-utils" "5.49.0" - "@typescript-eslint/utils" "5.49.0" + "@typescript-eslint/scope-manager" "5.50.0" + "@typescript-eslint/type-utils" "5.50.0" + "@typescript-eslint/utils" "5.50.0" debug "^4.3.4" + grapheme-splitter "^1.0.4" ignore "^5.2.0" natural-compare-lite "^1.4.0" regexpp "^3.2.0" @@ -2378,13 +2379,13 @@ debug "^4.3.1" "@typescript-eslint/parser@^5": - version "5.49.0" - resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.49.0.tgz#d699734b2f20e16351e117417d34a2bc9d7c4b90" - integrity sha512-veDlZN9mUhGqU31Qiv2qEp+XrJj5fgZpJ8PW30sHU+j/8/e5ruAhLaVDAeznS7A7i4ucb/s8IozpDtt9NqCkZg== + version "5.50.0" + resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.50.0.tgz#a33f44b2cc83d1b7176ec854fbecd55605b0b032" + integrity sha512-KCcSyNaogUDftK2G9RXfQyOCt51uB5yqC6pkUYqhYh8Kgt+DwR5M0EwEAxGPy/+DH6hnmKeGsNhiZRQxjH71uQ== dependencies: - "@typescript-eslint/scope-manager" "5.49.0" - "@typescript-eslint/types" "5.49.0" - "@typescript-eslint/typescript-estree" "5.49.0" + "@typescript-eslint/scope-manager" "5.50.0" + "@typescript-eslint/types" "5.50.0" + "@typescript-eslint/typescript-estree" "5.50.0" debug "^4.3.4" "@typescript-eslint/scope-manager@4.33.0": @@ -2395,21 +2396,21 @@ "@typescript-eslint/types" "4.33.0" "@typescript-eslint/visitor-keys" "4.33.0" -"@typescript-eslint/scope-manager@5.49.0": - version "5.49.0" - resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.49.0.tgz#81b5d899cdae446c26ddf18bd47a2f5484a8af3e" - integrity sha512-clpROBOiMIzpbWNxCe1xDK14uPZh35u4QaZO1GddilEzoCLAEz4szb51rBpdgurs5k2YzPtJeTEN3qVbG+LRUQ== +"@typescript-eslint/scope-manager@5.50.0": + version "5.50.0" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.50.0.tgz#90b8a3b337ad2c52bbfe4eac38f9164614e40584" + integrity sha512-rt03kaX+iZrhssaT974BCmoUikYtZI24Vp/kwTSy841XhiYShlqoshRFDvN1FKKvU2S3gK+kcBW1EA7kNUrogg== dependencies: - "@typescript-eslint/types" "5.49.0" - "@typescript-eslint/visitor-keys" "5.49.0" + "@typescript-eslint/types" "5.50.0" + "@typescript-eslint/visitor-keys" "5.50.0" -"@typescript-eslint/type-utils@5.49.0": - version "5.49.0" - resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.49.0.tgz#8d5dcc8d422881e2ccf4ebdc6b1d4cc61aa64125" - integrity sha512-eUgLTYq0tR0FGU5g1YHm4rt5H/+V2IPVkP0cBmbhRyEmyGe4XvJ2YJ6sYTmONfjmdMqyMLad7SB8GvblbeESZA== +"@typescript-eslint/type-utils@5.50.0": + version "5.50.0" + resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.50.0.tgz#509d5cc9728d520008f7157b116a42c5460e7341" + integrity sha512-dcnXfZ6OGrNCO7E5UY/i0ktHb7Yx1fV6fnQGGrlnfDhilcs6n19eIRcvLBqx6OQkrPaFlDPk3OJ0WlzQfrV0bQ== dependencies: - "@typescript-eslint/typescript-estree" "5.49.0" - "@typescript-eslint/utils" "5.49.0" + "@typescript-eslint/typescript-estree" "5.50.0" + "@typescript-eslint/utils" "5.50.0" debug "^4.3.4" tsutils "^3.21.0" @@ -2418,10 +2419,10 @@ resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz#a1e59036a3b53ae8430ceebf2a919dc7f9af6d72" integrity sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ== -"@typescript-eslint/types@5.49.0": - version "5.49.0" - resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.49.0.tgz#ad66766cb36ca1c89fcb6ac8b87ec2e6dac435c3" - integrity sha512-7If46kusG+sSnEpu0yOz2xFv5nRz158nzEXnJFCGVEHWnuzolXKwrH5Bsf9zsNlOQkyZuk0BZKKoJQI+1JPBBg== +"@typescript-eslint/types@5.50.0": + version "5.50.0" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.50.0.tgz#c461d3671a6bec6c2f41f38ed60bd87aa8a30093" + integrity sha512-atruOuJpir4OtyNdKahiHZobPKFvZnBnfDiyEaBf6d9vy9visE7gDjlmhl+y29uxZ2ZDgvXijcungGFjGGex7w== "@typescript-eslint/typescript-estree@4.33.0", "@typescript-eslint/typescript-estree@^4.33.0": version "4.33.0" @@ -2436,29 +2437,29 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@5.49.0": - version "5.49.0" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.49.0.tgz#ebd6294c0ea97891fce6af536048181e23d729c8" - integrity sha512-PBdx+V7deZT/3GjNYPVQv1Nc0U46dAHbIuOG8AZ3on3vuEKiPDwFE/lG1snN2eUB9IhF7EyF7K1hmTcLztNIsA== +"@typescript-eslint/typescript-estree@5.50.0": + version "5.50.0" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.50.0.tgz#0b9b82975bdfa40db9a81fdabc7f93396867ea97" + integrity sha512-Gq4zapso+OtIZlv8YNAStFtT6d05zyVCK7Fx3h5inlLBx2hWuc/0465C2mg/EQDDU2LKe52+/jN4f0g9bd+kow== dependencies: - "@typescript-eslint/types" "5.49.0" - "@typescript-eslint/visitor-keys" "5.49.0" + "@typescript-eslint/types" "5.50.0" + "@typescript-eslint/visitor-keys" "5.50.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.49.0": - version "5.49.0" - resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.49.0.tgz#1c07923bc55ff7834dfcde487fff8d8624a87b32" - integrity sha512-cPJue/4Si25FViIb74sHCLtM4nTSBXtLx1d3/QT6mirQ/c65bV8arBEebBJJizfq8W2YyMoPI/WWPFWitmNqnQ== +"@typescript-eslint/utils@5.50.0": + version "5.50.0" + resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.50.0.tgz#807105f5ffb860644d30d201eefad7017b020816" + integrity sha512-v/AnUFImmh8G4PH0NDkf6wA8hujNNcrwtecqW4vtQ1UOSNBaZl49zP1SHoZ/06e+UiwzHpgb5zP5+hwlYYWYAw== dependencies: "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.49.0" - "@typescript-eslint/types" "5.49.0" - "@typescript-eslint/typescript-estree" "5.49.0" + "@typescript-eslint/scope-manager" "5.50.0" + "@typescript-eslint/types" "5.50.0" + "@typescript-eslint/typescript-estree" "5.50.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" semver "^7.3.7" @@ -2471,12 +2472,12 @@ "@typescript-eslint/types" "4.33.0" eslint-visitor-keys "^2.0.0" -"@typescript-eslint/visitor-keys@5.49.0": - version "5.49.0" - resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.49.0.tgz#2561c4da3f235f5c852759bf6c5faec7524f90fe" - integrity sha512-v9jBMjpNWyn8B6k/Mjt6VbUS4J1GvUlR4x3Y+ibnP1z7y7V4n0WRz+50DY6+Myj0UaXVSuUlHohO+eZ8IJEnkg== +"@typescript-eslint/visitor-keys@5.50.0": + version "5.50.0" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.50.0.tgz#b752ffc143841f3d7bc57d6dd01ac5c40f8c4903" + integrity sha512-cdMeD9HGu6EXIeGOh2yVW6oGf9wq8asBgZx7nsR/D36gTfQ0odE5kcRYe5M81vjEFAcPeugXrHg78Imu55F6gg== dependencies: - "@typescript-eslint/types" "5.49.0" + "@typescript-eslint/types" "5.50.0" eslint-visitor-keys "^3.3.0" "@xmldom/xmldom@^0.8.6": @@ -2900,9 +2901,9 @@ aws-sdk-mock@5.6.0: traverse "^0.6.6" aws-sdk@^2.1211.0, aws-sdk@^2.596.0, aws-sdk@^2.928.0: - version "2.1304.0" - resolved "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1304.0.tgz#92a57d96394185fbeb790a85e71c47154c1cd150" - integrity sha512-9mf2uafa2M9yFC5IlMe85TIc7OUo1HSProCQWzpRmAAYhcSwmfbRyt02Wtr5YSVvJJPmcSgcyI92snsQR1c3nw== + version "2.1306.0" + resolved "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1306.0.tgz#33a5f8d732c2bfb8bb0d8dd946163814f294242d" + integrity sha512-t3M04Nx+uHVYcRGZXoI0Dr24I722oslwkwGT/gFPR2+Aub5dQ88+BetCPBvg24sZDg2RNWNxepYk5YHuKV5Hrg== dependencies: buffer "4.9.2" events "1.1.1" @@ -3270,9 +3271,9 @@ camelcase@^6.2.0, camelcase@^6.3.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001449: - version "1.0.30001449" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001449.tgz#a8d11f6a814c75c9ce9d851dc53eb1d1dfbcd657" - integrity sha512-CPB+UL9XMT/Av+pJxCKGhdx+yg1hzplvFJQlJ2n68PyQGMz9L/E2zCyLdOL8uasbouTUgnPl+y0tccI/se+BEw== + version "1.0.30001450" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001450.tgz#022225b91200589196b814b51b1bbe45144cf74f" + integrity sha512-qMBmvmQmFXaSxexkjjfMvD5rnDL0+m+dUMZKoDYsGG8iZN29RuYh9eRoMvKsT6uMAWlyUUGDEQGJJYjzCIO9ew== case@1.6.3, case@^1.6.3: version "1.6.3" @@ -3284,10 +3285,10 @@ caseless@~0.12.0: resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== -cdk-generate-synthetic-examples@^0.1.138: - version "0.1.138" - resolved "https://registry.npmjs.org/cdk-generate-synthetic-examples/-/cdk-generate-synthetic-examples-0.1.138.tgz#628abbdaefd7fbb218b9b5a074e907bb2985d807" - integrity sha512-ZWCZzXE6FVgzJsGDsMfqWf1HXiTzTw9xxsiK5Ic4FCkgXggzKy+SLBG7eZ62PD5oLkUh2FI7B+SNhsgPXYE72A== +cdk-generate-synthetic-examples@^0.1.140: + version "0.1.140" + resolved "https://registry.npmjs.org/cdk-generate-synthetic-examples/-/cdk-generate-synthetic-examples-0.1.140.tgz#47c9fc4a5df85592b27ceeb2a30bd272c1a8461d" + integrity sha512-enpsr7itXdaWRbKOGoUcDQiNHttm8NqEAlQMGWSagHfkYAa3628tlb9WtimzQ/ATgDHgvnCRZoO5h9XD0Vbzjg== dependencies: "@jsii/spec" "^1.74.0" fs-extra "^10.1.0" @@ -3296,17 +3297,17 @@ cdk-generate-synthetic-examples@^0.1.138: jsii-rosetta "^1.74.0" yargs "^17.6.2" -cdk8s-plus-24@2.4.5: - version "2.4.5" - resolved "https://registry.npmjs.org/cdk8s-plus-24/-/cdk8s-plus-24-2.4.5.tgz#8090b24a720cfdc42456d6d8ab6810cc0f0b3bf1" - integrity sha512-q576npP/mqJeAHKdJ/4+DLY4M+D1tRlYxmQHMs9VD/otJsuNCLzxmkUiquywUmL1EGPno0ctlFkmGukHZ46X4Q== +cdk8s-plus-24@2.4.8: + version "2.4.8" + resolved "https://registry.npmjs.org/cdk8s-plus-24/-/cdk8s-plus-24-2.4.8.tgz#ea4d4715cd649feb954dc36e6ec58887faff4091" + integrity sha512-p2KTJ7YUfg2ohTZhgPxWIbYaIA9Tu19Af3qIfSqiEHYPKolujQrBlTXE7R30jfRbq7XWEDbK0hDp8rKnvoKPYw== dependencies: minimatch "^3.1.2" -cdk8s@^2.6.34: - version "2.6.34" - resolved "https://registry.npmjs.org/cdk8s/-/cdk8s-2.6.34.tgz#12a0ed57b9336fafb2b6984dad9da2035cdf24d1" - integrity sha512-fXPpA3XV83HmOSUzhUokwi3AYanOKN7FuhqxMjjn9SxZ+zLvJSrUtx6lUKziqDOqnn5JCFWT6SGDMpb3xmVGBg== +cdk8s@^2.6.36: + version "2.6.36" + resolved "https://registry.npmjs.org/cdk8s/-/cdk8s-2.6.36.tgz#3b6d025a1b2d019735f8f13ffcaebdc96d1b75f0" + integrity sha512-2Fj/oTsuXVsP8W7zQDYcesyu8AOQjvKAdHKyrjzozLAElWCoR75dEb63yYrglJKZrRIgD+ahZEtuzv0MeEMlIA== dependencies: fast-json-patch "^3.1.1" follow-redirects "^1.15.2" @@ -3711,9 +3712,9 @@ console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control- integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== constructs@^10.0.0: - version "10.1.235" - resolved "https://registry.npmjs.org/constructs/-/constructs-10.1.235.tgz#8051459c33a14f51ec428edb4321be55a7dffb94" - integrity sha512-d4YdcNyV0Id3rjsIuges4vCY1u4yGcr6SSx3OYtQG87NMOLpRCdibNAaWfj8pWJNqm7WHBc6bjfd8WX2nsEJjA== + version "10.1.237" + resolved "https://registry.npmjs.org/constructs/-/constructs-10.1.237.tgz#cec68cbd8726c6463298566a11a5e84d50e098ca" + integrity sha512-Y8HH/d5opBmp97C0BM8Ib24wIbA1nP6xzewnMlRilth/B1p29tvI+bR/o981z7gdgwLdVgs6kBrUXCHg+OQ/tQ== conventional-changelog-angular@^5.0.12: version "5.0.13" @@ -8110,11 +8111,9 @@ minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3, minipass@^3. yallist "^4.0.0" minipass@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/minipass/-/minipass-4.0.0.tgz#7cebb0f9fa7d56f0c5b17853cbe28838a8dbbd3b" - integrity sha512-g2Uuh2jEKoht+zvO6vJqXmYpflPqzRBT+Th2h01DKh5z7wbY/AZ2gCQ78cP70YoHPyFdY30YBV5WxgLOEwOykw== - dependencies: - yallist "^4.0.0" + version "4.0.1" + resolved "https://registry.npmjs.org/minipass/-/minipass-4.0.1.tgz#2b9408c6e81bb8b338d600fb3685e375a370a057" + integrity sha512-V9esFpNbK0arbN3fm2sxDKqMYgIp7XtVdE4Esj+PE4Qaaxdg1wIw48ITQIOn1sc8xXSmUviVL3cyjMqPlrVkiA== minizlib@^1.3.3: version "1.3.3" @@ -8290,9 +8289,9 @@ nock@^13.3.0: propagate "^2.0.0" node-fetch@^2.6.1, node-fetch@^2.6.7: - version "2.6.8" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz#a68d30b162bc1d8fd71a367e81b997e1f4d4937e" - integrity sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg== + version "2.6.9" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6" + integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg== dependencies: whatwg-url "^5.0.0" @@ -8358,9 +8357,9 @@ node-preload@^0.2.1: process-on-spawn "^1.0.0" node-releases@^2.0.8: - version "2.0.8" - resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz#0f349cdc8fcfa39a92ac0be9bc48b7706292b9ae" - integrity sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A== + version "2.0.9" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.9.tgz#fe66405285382b0c4ac6bcfbfbe7e8a510650b4d" + integrity sha512-2xfmOrRkGogbTK9R6Leda0DGiXeY3p2NJpy4+gNCffdUvV6mdEJnaDEic1i3Ec2djAo8jWYoJMR5PB0MSMpxUA== node-source-walk@^4.0.0, node-source-walk@^4.2.0, node-source-walk@^4.2.2: version "4.3.0" @@ -9456,10 +9455,10 @@ progress@^2.0.0, progress@^2.0.3: resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -projen@^0.67.24: - version "0.67.24" - resolved "https://registry.npmjs.org/projen/-/projen-0.67.24.tgz#23f0ed6a3dde862b07d31dd2bbcb874b730be224" - integrity sha512-JAPYnDrB4KZ1YoQj08AVZXecd9QdEcdYUIvvNXd504aJPMUwSZ4vh2oeCdmVsZFmXkvKyduc/MR+v+0vbvMFrQ== +projen@^0.67.29: + version "0.67.29" + resolved "https://registry.npmjs.org/projen/-/projen-0.67.29.tgz#981beec8db2658ab0e027a94c856aff6039cb016" + integrity sha512-6GWMdfkxjEwQPCTB+YgWl2nZMzfbYbmlRkxM9oLdZetpYJwg3T1fokwrnSWxYVOXQXdUoXl0LjkWJJO08+/5HQ== dependencies: "@iarna/toml" "^2.2.5" case "^1.6.3" @@ -11194,9 +11193,9 @@ typescript@^3.9.10, typescript@^3.9.5, typescript@^3.9.7, typescript@~3.9.10: integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== typescript@^4.5.5: - version "4.9.4" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" - integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== + version "4.9.5" + resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== typescript@~3.8.3: version "3.8.3"